Second Life of a Hungarian SharePoint Geek

September 13, 2011

Managed Client Object Model Internals – Creating custom client OM extensions

Filed under: Fiddler, Managed Client OM, SP 2010 — Tags: , , — Peter Holpar @ 22:50

In the previous parts of my managed client OM series I discussed the theory of the server side and client side of the SharePoint 2010 object model.

As I promised you, in the current post I’m trying to put theory into practice through creating the server and client side of a simple client OM extension based on the framework provided by the object model.

You can download the sample solution from here.

This post contains mainly code and a very minimal theory. If something is not clear, I suggest you to read and understand the former parts again. Hopefully you will get the answer there.

The Visual Studio 2010 solution introduced here consists of three project:

  • Server side code is packaged and deployed through a SharePoint project (ClientExtensionPackage).
  • Client side code is a class library (ClientExtension).
  • To test the working of the solution there is a console application (ClientExtensionConsole) that calls the client side components and through them the server side as well.

Let’s start with the server side (similar as there were the server side of the SharePoint API first, then came the client API). In the naming convention I tried to reflect the SharePoint nomenclature. It means that server side class names are prefixed with SS (similar to the SP prefix for SharePoint server side classes), client classes have no prefix.

Assume you have a very specific functionality that can be run only on server side. This time it will a simple GetMessage method that can be called with a string (name) parameter and returns a welcome message for that name.

  1. public String GetMessage(String name)
  2. {
  3.     return String.Format("Hello, {0}!", name);
  4. }

We wrap this functionality in the SSCustomClientObject class and decorates both the class and the method with the attributes required by the client OM. In this case the ServerTypeId is simply a random GUID.

  1. namespace ClientExtension.Server
  2. {
  3.     [ClientCallableType(Name = "CustomClientObject", ServerTypeId = "{E44FC83D-F555-4DC5-885D-88C1057A5E72}")]
  4.     public class SSCustomClientObject
  5.     {
  6.         [ClientCallable]
  7.         public String GetMessage(String name)
  8.         {
  9.             return String.Format("Hello, {0}!", name);
  10.         }
  11.  
  12.     }
  13. }

Of course, we need a custom context on the server side. This SSCustomContext class has another random ServerTypeId. We can access the current SSCustomContext instance through the static Current property, and once we have a context instance, we can access the SSCustomClientObject instance through the CustomClientObject property.

  1. namespace ClientExtension.Server
  2. {
  3.     [ClientCallableType(Name = "CustomRequestContext", ServerTypeId = "{DF694817-22BA-4952-A1E9-84C6E69709A8}", Internal = true)]
  4.     public class SSCustomContext
  5.     {
  6.         private static SSCustomContext _context = new SSCustomContext();
  7.         private SSCustomClientObject _customClientObject = new SSCustomClientObject();
  8.  
  9.         [ClientCallable]
  10.         public static SSCustomContext Current
  11.         {
  12.             get
  13.             {
  14.                 return _context;
  15.             }
  16.         }
  17.  
  18.         [ClientCallableConstraint(Type = ClientCallableConstraintType.NotNull), ClientCallable(Name = "CustomClientObject")]
  19.         public SSCustomClientObject CustomClientObject
  20.         {
  21.             get
  22.             {
  23.                 return _customClientObject;
  24.             }
  25.         }
  26.  
  27.     }
  28. }

To expose our server classes to the client side we have to create the corresponding proxy classes.

These proxy classes are inherited from the _ServerProxy base class. The classes are decorated with the ServerProxy attribute, having its underlyingType parameter the type of the server side class, and TargetTypeId matches the ServerTypeId of the server side class.

The proxy class for the SSCustomClientObject looks like this:

  1. namespace ClientExtension.Proxy
  2. {
  3.     [ServerProxy(typeof(SSCustomClientObject), TargetTypeId = "{E44FC83D-F555-4DC5-885D-88C1057A5E72}")]
  4.     public class SSCustomClientObject_Proxy : _ServerProxy
  5.     {
  6.         public override object InvokeMethod(object obj, string methodName, XmlNodeList xmlargs, ProxyContext proxyContext, out bool isVoid)
  7.         {
  8.             SSCustomClientObject customClientObject = obj as SSCustomClientObject;
  9.             if (customClientObject == null)
  10.             {
  11.                 throw new ArgumentNullException("obj");
  12.             }
  13.             switch (methodName)
  14.             {
  15.                 case "GetMessage":
  16.                     isVoid = false;
  17.                     base.CheckBlockedMethod("GetMessage", proxyContext);
  18.                     String message = GetMessage_MethodProxy(customClientObject, xmlargs, proxyContext);
  19.                     return message;
  20.             }
  21.             return base.InvokeMethod(obj, methodName, xmlargs, proxyContext, out isVoid);
  22.         }
  23.  
  24.  
  25.         private static String GetMessage_MethodProxy(SSCustomClientObject customClientObject, XmlNodeList xmlargs, ProxyContext proxyContext)
  26.         {
  27.             string name = DataConverter.ToString(_ServerProxy.GetArgument(xmlargs, 0), proxyContext);
  28.             return customClientObject.GetMessage(name);
  29.         }
  30.  
  31.     }
  32.  
  33. }

The proxy for the SSCustomContext class is the following:

  1. namespace ClientExtension.Proxy
  2. {
  3.     [ServerProxy(typeof(SSCustomContext), TargetTypeId = "{DF694817-22BA-4952-A1E9-84C6E69709A8}")]
  4.     public class SSCustomContext_Proxy : _ServerProxy
  5.     {
  6.  
  7.         private static string[] s_refProperties = new string[] { "CustomClientObject" };
  8.         private static Guid s_targetTypeId = new Guid("{DF694817-22BA-4952-A1E9-84C6E69709A8}");
  9.         private static string[] s_valueProperties = new string[0];
  10.  
  11.         public override object GetProperty(object obj, string propName, ProxyContext proxyContext)
  12.         {
  13.             SSCustomContext context = obj as SSCustomContext;
  14.             if (context == null)
  15.             {
  16.                 throw new ArgumentNullException("obj");
  17.             }
  18.             switch (propName)
  19.             {
  20.                 case "CustomClientObject":
  21.                     base.CheckBlockedGetProperty("CustomClientObject", proxyContext);
  22.                     return context.CustomClientObject;
  23.             }
  24.             return base.GetProperty(obj, propName, proxyContext);
  25.         }
  26.  
  27.  
  28.         public override object GetStaticProperty(string propName, ProxyContext proxyContext)
  29.         {
  30.             string str;
  31.             if (((str = propName) == null) || (str != "Current"))
  32.             {
  33.                 throw new ArgumentOutOfRangeException(propName);
  34.             }
  35.             base.CheckBlockedGetProperty("Current", proxyContext);
  36.             return SSCustomContext.Current;
  37.         }
  38.  
  39.         public override bool HasProperty(string propName, bool valueObject)
  40.         {
  41.             return ((valueObject && (Array.IndexOf<string>(s_valueProperties, propName) >= 0)) || ((!valueObject && (Array.IndexOf<string>(s_refProperties, propName) >= 0)) || base.HasProperty(propName, valueObject)));
  42.         }
  43.  
  44.         public override object InvokeConstructor(XmlNodeList xmlargs, ProxyContext proxyContext)
  45.         {
  46.             throw new NotImplementedException();
  47.         }
  48.  
  49.         public override object InvokeStaticMethod(string methodName, XmlNodeList xmlargs, ProxyContext proxyContext, out bool isVoid)
  50.         {
  51.             throw new ArgumentOutOfRangeException(methodName);
  52.         }
  53.  
  54.         protected override bool IsGetPropertyBlocked(string name, ProxyContext proxyContext)
  55.         {
  56.             return (proxyContext.IsGetPropertyBlocked(s_targetTypeId, name) || base.IsGetPropertyBlocked(name, proxyContext));
  57.         }
  58.  
  59.         protected override bool IsMethodBlocked(string name, ProxyContext proxyContext)
  60.         {
  61.             return (proxyContext.IsMethodBlocked(s_targetTypeId, name) || base.IsMethodBlocked(name, proxyContext));
  62.         }
  63.  
  64.         protected override bool IsSetPropertyBlocked(string name, ProxyContext proxyContext)
  65.         {
  66.             return (proxyContext.IsSetPropertyBlocked(s_targetTypeId, name) || base.IsSetPropertyBlocked(name, proxyContext));
  67.         }
  68.  
  69.         public override bool WriteOnePropertyValueAsJson(JsonWriter writer, object obj, ClientQueryProperty field, ProxyContext proxyContext)
  70.         {
  71.             bool flag = false;
  72.             SSCustomContext context = obj as SSCustomContext;
  73.             if (context == null)
  74.             {
  75.                 throw new ArgumentNullException("obj");
  76.             }
  77.             switch (field.Name)
  78.             {
  79.                 case "CustomRequestContext":
  80.                     if (field.ScalarProperty.HasValue && field.ScalarProperty.Value)
  81.                     {
  82.                         throw new InvalidClientQueryException();
  83.                     }
  84.                     base.CheckBlockedGetProperty("CustomRequestContext", proxyContext);
  85.                     flag = true;
  86.                     base.WriteQueryResult(writer, context.CustomClientObject, field.ObjectQuery, proxyContext);
  87.                     return flag;
  88.             }
  89.             return base.WriteOnePropertyValueAsJson(writer, obj, field, proxyContext);
  90.         }
  91.  
  92.         public override Type TargetType
  93.         {
  94.             get
  95.             {
  96.                 return typeof(SSCustomContext);
  97.             }
  98.         }
  99.  
  100.         public override Guid TargetTypeId
  101.         {
  102.             get
  103.             {
  104.                 return s_targetTypeId;
  105.             }
  106.         }
  107.  
  108.         public override string TargetTypeScriptClientFullName
  109.         {
  110.             get
  111.             {
  112.                 return "SP.CustomRequestContext";
  113.             }
  114.         }
  115.  
  116.     }
  117.  
  118. }

It is not enough to deploy the server side assembly to SharePoint, we should register our extension in the web.config of the SharePoint application as well. We could do that through a feature receiver and SPWebConfigModification class, but in this case it was easier to alter the configuration file manually. We should register our proxy assembly by adding it to the proxyAssemblies node as shown below:

  1. <microsoft.sharepoint.client>
  2.   <serverRuntime>
  3.     <hostTypes>
  4.       <add type="Microsoft.SharePoint.Client.SPClientServiceHost, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
  5.     </hostTypes>
  6.     <proxyAssemblies>
  7.       <add assembly="ClientExtensionPackage, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b33fcd4f1c7fc3ab" />
  8.     </proxyAssemblies>
  9.   </serverRuntime>
  10. </microsoft.sharepoint.client>

Let’s switch to the corresponding client side API.

The CustomRequestContext class is the “mirror” of the SSCustomContext (see the value of the Name parameter of the ClientCallableType attribute on the SSCustomContext  class and the common ServerTypeId value):

  1. namespace ClientExtension
  2. {
  3.     [ScriptType("SS.CustomRequestContext", ServerTypeId = "{DF694817-22BA-4952-A1E9-84C6E69709A8}")]
  4.     internal class CustomRequestContext : ClientObject
  5.     {
  6.         public CustomRequestContext(ClientRuntimeContext Context, ObjectPath ObjectPath)
  7.             : base(Context, ObjectPath)
  8.         {
  9.         }
  10.  
  11.         [Remote]
  12.         public static CustomRequestContext GetCurrent(ClientRuntimeContext Context)
  13.         {
  14.             object customRequestContext = null;
  15.             if (!Context.StaticObjects.TryGetValue("ClientExtension$Server$SSCustomContext$Current", out customRequestContext))
  16.             {
  17.                 customRequestContext = new CustomRequestContext(Context, new ObjectPathStaticProperty(Context, "{DF694817-22BA-4952-A1E9-84C6E69709A8}", "Current"));
  18.                 Context.StaticObjects["ClientExtension$Server$SSCustomContext$Current"] = customRequestContext;
  19.             }
  20.             return (CustomRequestContext)customRequestContext;
  21.         }
  22.  
  23.  
  24.         protected override bool InitOnePropertyFromJson(string peekedName, JsonReader reader)
  25.         {
  26.             bool flag = base.InitOnePropertyFromJson(peekedName, reader);
  27.             if (!flag)
  28.             {
  29.                 string str = peekedName;
  30.                 if (str != "CustomClientObject")
  31.                 {
  32.                     flag = true;
  33.                     reader.ReadName();
  34.                     this.CustomClientObject.FromJson(reader);
  35.                 }
  36.             }
  37.             return flag;
  38.         }
  39.  
  40.         [Remote]
  41.         public CustomClientObject CustomClientObject
  42.         {
  43.             get
  44.             {
  45.                 object obj;
  46.                 if (base.ObjectData.ClientObjectProperties.TryGetValue("CustomClientObject", out obj))
  47.                 {
  48.                     return (CustomClientObject)obj;
  49.                 }
  50.                 CustomClientObject customClientObject = new CustomClientObject(base.Context, new ObjectPathProperty(base.Context, base.Path, "CustomClientObject"));
  51.                 base.ObjectData.ClientObjectProperties["CustomClientObject"] = customClientObject;
  52.                 return customClientObject;
  53.             }
  54.         }
  55.  
  56.     }
  57. }

Note: The ScriptType attribute is not required in our case, as we don’t expose the functionality to JavaScript. I applied this attribute just to be consistent with the decoration of SharePoint classes.

The client side applications can access the custom client OM API through the CustomClientContext class. Note, how we get the current CustomRequestContext instance by calling CustomRequestContext.GetCurrent(this) in the CustomClientObject property.

  1. namespace ClientExtension
  2. {
  3.     public class CustomClientContext : ClientContext
  4.     {
  5.  
  6.         public CustomClientContext(string webFullUrl) : base(webFullUrl)
  7.         {
  8.         }
  9.  
  10.         public CustomClientContext(Uri webFullUrl)
  11.             : base((webFullUrl == null) ? null : webFullUrl.ToString())
  12.         {
  13.         }
  14.  
  15.         private CustomClientObject _customClientObject;
  16.  
  17.         public CustomClientObject CustomClientObject
  18.         {
  19.             get
  20.             {
  21.                 if (_customClientObject == null)
  22.                 {
  23.                     CustomRequestContext current = CustomRequestContext.GetCurrent(this);
  24.                     _customClientObject = current.CustomClientObject;
  25.                 }
  26.                 return _customClientObject;
  27.             }
  28.         }
  29.     }
  30.  
  31. }

The single GetMessage method of the CustomClientObject class does nothing more than adds ClientActionInvokeMethod instance created for the server side GetMessage method to the pending request of context and adds the query ID – result pair to the map that is used internally to track results in the response received from the server.

  1. namespace ClientExtension
  2. {
  3.     [ScriptType("SS.CustomClientObject", ServerTypeId = "{E44FC83D-F555-4DC5-885D-88C1057A5E72}")]
  4.     public class CustomClientObject : ClientObject
  5.     {
  6.         [EditorBrowsable(EditorBrowsableState.Never)]
  7.         public CustomClientObject(ClientRuntimeContext Context, ObjectPath ObjectPath)
  8.             : base(Context, ObjectPath)
  9.         {
  10.         }
  11.  
  12.  
  13.         [Remote]
  14.         public ClientResult<String> GetMessage(String name)
  15.         {
  16.             ClientAction query = new ClientActionInvokeMethod(this, "GetMessage", new Object[] { name });
  17.             base.Context.AddQuery(query);
  18.             ClientResult<String> result = new ClientResult<String>();
  19.             base.Context.AddQueryIdAndResultObject(query.Id, result);
  20.             return result;
  21.         }
  22.  
  23.     }
  24.  
  25. }

The console application simply creates a new CustomClientContext based on the URL of the SharePoint application, gets the CustomClientObject instance from the context, then calls its GetMessage method. The request is sent to the server when the ExecuteQuery method is called, and response can be read from the Value property of the ClientResult<String> instance we set when calling GetMessage.

  1. private void CallCustomOM()
  2. {
  3.     // replace URL with the address of your SharePoint application
  4.     CustomClientContext context = new CustomClientContext("http://yoursharepoint&quot;);
  5.     CustomClientObject cco = context.CustomClientObject;
  6.     ClientResult<String> result = cco.GetMessage("Joe");           
  7.     context.ExecuteQuery();
  8.     Console.WriteLine(result.Value);
  9. }

The output  produced by the application:

image

It is interesting to see what request our custom client object generates and what response is sent back from the server.

You should see a session opened to /_vti_bin/client.svc/ProcessQuery in Fiddler that contains the following request and response:

image

Conclusion

It’s useful to know that you can use the infrastructure provided by the SharePoint client object model to extend the default behavior. That is possible so easily because the main classes are defined neither as sealed nor internal.

All of the classes in this example are rather simple. Of course, in case of a real server side object you will end up with more complex classes. Despite of this, I hope this illustration helps you to start implementing your custom client OM extensions, either to complement the coverage of the out of the box client OM on existing SharePoint server side API or to create a client access bridge to your custom server side objects.

Advertisements

4 Comments »

  1. Can you show any guidance on how to extend SharePoint 2013 client object model? Or any idea, how to create a webservice under SharePoint, that can be called from javascript, but with the context of the current user?
    Thank you.

    Comment by Károly Hugyi — July 22, 2013 @ 13:27

  2. Hello, this solution worked for me in sharepooint 2010. I tested it in sharepoint 2013, but it seems some classes are to be changed – I guess ServerStub instead of _ServerProxy. I compiled it, however, while executing, it throws an exception, that method GetMessage does not exist. It creates custom context and custom client object, but while running ExecuteQuery – it does not go into custom client object – have you got any ideas how to fix this?

    Comment by jim — August 8, 2013 @ 12:51

    • Hi jim, we are also facing the same issue as you mentioned above. can you please help us how you resolved this issue?

      Comment by Rahul — June 9, 2015 @ 14:27


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: