Second Life of a Hungarian SharePoint Geek

May 28, 2014

How to Make a SharePoint Web Site / Page Temporarily Editable if the Site is Configured to Disable Editing

Filed under: Fiddler, Project Server, SharePoint — Tags: , , — Peter Holpar @ 22:16

Nowadays I’m working quite a lot with Project Server 2013. One of my tasks is to create a customized project web site template. A quite good description of the overall process can be read in this post.

In my case the customization should include not only custom lists or navigation items, but more advanced design features as well. For example, the site should have a breadcrumb, that we can enable using SharePoint Designer (SPD) via editing the master page as described here. So let’s start SPD, and try to edit the master page of a project website! You will receive a warning:

Page Editing is Disabled
This web site has been configured to disallow page editing with SharePoint Designer.
Contact your web site administrator for more information.

image

It seems to be a by design feature, as stated here:

“An administrator or designer can accidentally break the whole functionality of the site by incorrectly modifying pages at the root web of a Project Site.”

Yes, it is of course possible to accidentally break the whole functionality of the site if one incorrectly modifies pages, but IMHO it should be the responsibility of the administrator to decide if such modifications should be disabled or not.

Some of the workarounds I found on the web (like this one) suggest altering the out-of-the-box site templates by removing the DisableWebDesignFeatures attribute (see the related Project element in the site schema), or setting the vti_disablewebdesignfeatures2 in the Properties collection of the SharePoint web (like suggested here). However, I have not found the vti_disablewebdesignfeatures2 property at all for the project web site (the value of the allowmasterpageediting property is 1, meaning pages should be theoretically editable), and did not want to alter any of the default templates. Is there a better way to make the pages / site editable?

From this post I’ve learned that this behavior is caused directly by a server response when opening a web site with SPD. The HTTP response (sent by _vti_bin/_vti_aut/author.dll, see the related entries in the FrontPage Server Extensions here) includes

<li>vti_disablewebdesignfeatures2

and next the list of disabled features, like

<li>VX|wdfopensite

that will disable opening the site in SPD, or in our case:

<li>VX|wdfeditpages

that will disable “only” page editing.

Having this information I came up quickly with my own solution to the problem, namely using Fiddler to alter the response sent by the server on the fly. In the CustomRules.js we should add the following code block to the OnBeforeResponse method:

if (oSession.HostnameIs("yourserver.com") && oSession.oResponse.headers.ExistsAndContains("Content-Type","application/x-vermeer-rpc")) {
      oSession.utilDecodeResponse();
      oSession.utilReplaceInResponse(‘<li>VX|wdfeditpages’, ‘<li>VX|’);
}

This code fakes the response, simulating a site that does not disable any kind of editing.

Note: You should replace yourserver.com with the host name of your SharePoint / Project Server site, and wdfeditpages with option(s) returned by the server.

Note 2: I suggest you to restart SPD (if it was running and you’ve already tried to open the problematic site earlier in the session) after you start capturing with Fiddler, as SPD seems to cache the former server responses. You might be requested to authenticate yourself again when opening the site in SPD.

November 7, 2013

Recurring authentication prompt when editing task list in datasheet view

Filed under: Fiddler, Security, SP 2010, Web service — Tags: , , , — Peter Holpar @ 23:35

The other day we received a complaint from a user, stating he cannot edit a specific list in the Datasheet view, although he had no such problem with other lists. Whenever he would have liked to edit a task list in SharePoint using the Datasheet view he was prompted for his credentials repeatedly, even though he had write permissions on the list and was able edit the same items using the standard web forms. The problem occurred not immediately when he switched to the Datasheet view, but only when he was to insert data copied from an Excel sheet or was to edit the data in the view otherwise. When he clicked Cancel in the authentication dialog, IE became unresponsive and must have been restarted. Other users had no such problem.

My first intention was that it may be caused by using wrong (e.g. 64-bit) version of Internet Explorer or some issue with the local Office installation. This theory was proved to be wrong after the issue was reproduced by the same user on a workstation where the other users were able to edit the list.

As a next try, I captured the network traffic by Fiddler for both the problematic user and for another user, who had no issue with the editing. Analyzing the results I found that in the background the Datasheet view calls the Webs and Lists SharePoint web services. The only difference I found between the traces was that in the case of the problematic user there was two 401 – Unauthorized HTTP response when calling the Lists WS. The first 401 response was simply to force authentication of the client application, and could be found in the normal case as well. The other request that resulted in the second 401 response contained the following body:

<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:xsd="http://www.w3.org/2001/XMLSchema&quot; xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body&gt; <GetListItems xmlns="http://schemas.microsoft.com/sharepoint/soap/"&gt; <listName>UserInfo</listName><query><Query><Where><Membership Type="SPGroup" ID="3"><FieldRef Name="ID" /></Membership></Where><OrderBy><FieldRef Name="Department"/></OrderBy></Query></query><viewFields><ViewFields><FieldRef Name="ID"/><FieldRef Name="Department"/></ViewFields></viewFields><rowLimit>10000</rowLimit><queryOptions><QueryOptions><ViewAttributes Scope="RecursiveAll"/></QueryOptions></queryOptions></GetListItems> </soap:Body></soap:Envelope>

From this request it is obvious that a list called UserInfo is queried, however there was no list with that name on the site. One might think that it is about the hidden user information list of SharePoint, but it is not the case. In fact it is a virtual list that refers to the members of a group in the case of a Person or Group field. At that point it was already obvious, that the Assigned To field of the task list was configured to enable selection of users from a specific group (in this case group with ID = 3), and the problematic user was neither member of the group nor a site collection administrator, so he had no permission to query the membership of the group.

image

The solution for the problem was to enable non-group members to see the members of the group as displayed below:

image

November 5, 2013

Redirecting a redirection using Fiddler

Filed under: Fiddler — Tags: — Peter Holpar @ 22:51

Recently I’m working on a claims based authentication solution for a SharePoint 2010 application that includes a custom STS that authenticates the users through an external IP by a two-factor authentication (user name and password + TAN sent via SMS). The external IP accepts a redirection URL as a query string parameter, the browser is redirected to this address after the successful authentication. The problem is that the allowed redirection URLs are configured at the IP web application, the list of URLs is maintained by the external company. The URL of our intranet site (no SharePoint / IIS) is on the list of the allowed values, the URL of our SP 2010 development environment is not. We have requested the configuration change of the IP, but in the meantime I wanted to find a way to use the IP as is from the developer server.

My first approach was to use Fiddler to redirect the request sent to the original host (e.g. our intranet) to the developer server. To achieve that, I altered the CustomRules.js of Fiddler like this:

static function OnBeforeRequest(oSession: Session) {
  var siteFrom = "intranet.company.com/folder1/login.php"
  var siteTo = "devenv.company.com/folder2/login.aspx"
  oSession.url = oSession.url.Replace(siteFrom, siteTo);
  …
}

It means however, that the original URL is not changed in the browser, only the HTTP request is routed to the other server in the background. So all relative URLs (images, scripts, css files) in the login page of our dev. server are referring to (not existing) resources on the intranet site.

A better approach is to alter the redirection response (HTTP status code 302) sent by the IP to include the URL of the dev. server instead of the URL of the intranet. It can be fulfilled by removing the original Location header and appending a new Location header corresponding to the developer site URL, as illustrated by the code below:

static function OnBeforeResponse(oSession: Session) {
  var siteFrom = "intranet.company.com/folder1/login.php"
  var siteTo = "devenv.company.com/folder2/login.aspx"
  if (oSession.responseCode == 302) {
    var sLocation = oSession.oResponse["Location"];
    var sNewLocation = sLocation.replace(siteFrom, siteTo);
    if (sLocation != sNewLocation) {
      // optionally append further query string parameters
      sNewLocation += "&ReturnUrl=%2fWingtipSTS%2fdefault.aspx%3fwa%3dwsignin1.0%26wtrealm%3dhttp%253a%252f%252fyoursite%253a2500%252f_trust%252fdefault.aspx%26wctx%3dhttp%253a%252f%252fyoursite%253a2500%252f_layouts%252fAuthenticate.aspx%253fSource%253d%25252F%26wreply%3dhttp%253a%252f%252fyoursite%253a2500%252f_trust%252fdefault.aspx&wa=wsignin1.0&wtrealm=http%3a%2f%2fyoursite%3a2500%2f_trust%2fdefault.aspx&wctx=http%3a%2f%2fyoursite%3a2500%2f_layouts%2fAuthenticate.aspx%3fSource%3d%252F&wreply=http%3a%2f%2fyoursite%3a2500%2f_trust%2fdefault.aspx";
      oSession.oResponse.headers.Remove("Location");
      oSession.oResponse.headers.Add("Location", sNewLocation);
    }
  }
  …
}
 
This approach ensures that the browser displays the correct URL and relative URLs on the page are pointing to the existing resources.

August 22, 2013

Strange error Saving changes of the Content Editor Web Part

Filed under: CEWP, Fiddler — Tags: , — Peter Holpar @ 23:01

A few days ago I just worked with the Content Editor Web Part (CEWB) when I received the following alerts after editing the HTML source of the web part:

"Cannot retrieve properties at this time” and “Cannot save your changes"

I found several references for this error on the web, but none of the proposed solutions (like AAM, trusted sites, etc.) solved my issue. After a short research I found the culprit: it was Fiddler, on of my favorite tools that intercepted the communication between SharePoint and my browser. Strange, that it did not helped when I turned off capturing of network traffic, only after shutting down the application disappeared the symptoms, and I was able to save the changes.

July 6, 2013

Issues with the SharePoint-Outlook connection

Filed under: Bugs, Fiddler, Outlook, SP 2010 — Tags: , , , — Peter Holpar @ 11:20

Issue 1

Recently we received a complain from a user, that synchronizing SharePoint 2010 documents to Outlook 2010 does not work. The error message in Outlook was:

Task ‘SharePoint’ reported error (0x80070005) : ‘You do not have permission to view this SharePoint List HTTP 302’

The conditions were:

SharePoint application with default (HTTPS) and intranet (HTTP) zones, identical host header for the zones. The IIS site for the intranet zone was stopped and redirected to the default zone (HTTP –> HTTPS).

The user was to synchronize two folders of a large document library (accessed using HTTPS) to Outlook 2010. Adding (any) first folder of the library to Outlook was OK, it was synchronized down to Outlook without any problems. However adding (any) second folder of the library gave us the same error. Synchronizing the items in the first folder was working the same time.

Using Fiddler I found, that instead of HTTPS Outlook sends the requests for synchronizations (the GetList method of the Lists web service is called, see details of the communications in a former post) to SharePoint using HTTP. IIS responded with the correct redirection status, HTTP 302, but it was simply ignored by Outlook. It seems to me as a double-bug: first, Outlook should not try to connect using HTTP when we created the connection from a HTTPS site, and second, it should follow the redirection response sent back by IIS.

I found a similar problem in this TechNet forum thread, and questions / conclusions like:

„Does Outlook 2007 not understand http 302 redirections?”

„I suspect Outlook doesn’t handle the redirection right.”

The “solution” in this case was to synchronize the full (quite large) document library, instead of the selected subfolders.

Another workaround might be to use Fiddler to redirect the HTTP request to HTTPS for the specific web site (by extending the default OnBeforeRequest function in CustomRules.js), however, a typical user would not like to run Fiddler in the background just to be able synchronize files from SharePoint.

    static function OnBeforeRequest(oSession: Session) {
       if (oSession.HostnameIs("yoursite.com") && oSession.oRequest.headers.UriScheme == "http") {
         oSession.oRequest.headers.UriScheme = "https";
       }
    … // original code of OnBeforeRequest function

 

Issue 2

We have two (fast) identical SharePoint 2010 systems: a test and a productive environment. Content is the same, FQDN of the servers are different. When I connect a SharePoint document library from prod. to Outlook 2010, it’s synchronized down to the client as expected. However, when I try to connect Outlook to the same library in test, the first link in Outlook simply replaced by the new one, the URL of the connection is overridden by the URL of the library from the test system. Seems to be a bug as well.

March 8, 2013

Accessing Office 365 REST services using LINQPad

LINQPad is a great tool, even for a SharePoint developer when working with the RESTful web services. However, it does not provide an authentication mechanism against Office 365, a major issue when there is no on-premise SharePoint or Project Server at your hand to develop and test your queries (as suggested by Andrew Lavinsky in this post), as illustrated by the figures below.

We add a new connection of type WCF Data Service 5.1 (OData 3) to LINQPad:

image

Specify the URI of the ListData.svc at our  O365 tenant, and Default (XML) as Formatter.

image

Then we receive the following error (you can try to specify username and password in the previous step, but it makes no difference):

Error: The remote server returned an error: (403) Forbidden.

image

I’ve found a workaround for this issue on the web, but for me it was so complex at the first sight (even though I later understood how it should work), so I decided to find another way, using my other favorite tool Fiddler.

Our “solution”: we will “cache” the authentication cookies from an Internet Explorer session, then inject the same cookies to the LINQPad sessions.

Start Fiddler, choose Roles / Customize Rules…, and edit the CustomRules.js file (don’t forget to create a backup!).

Before the OnBeforeRequest function add this code:

static var authCookies = "";
static var o365Site = "yourO365Site.sharepoint.com"; // modify this value!

At the beginning of the OnBeforeRequest function add this code:

if (oSession.HostnameIs(o365Site)) {
  var cookie = oSession.oRequest["Cookie"];
  if ((cookie == "") && (authCookies != "")) {
    //oSession.oRequest["Accept"] = "text/html, application/xhtml+xml, */*";
    oSession.oRequest["Cookie"] = authCookies;
  }
}

At the beginning of the OnBeforeResponse function add this code:

if (oSession.HostnameIs(o365Site)) {
  var cookie = oSession.oRequest["Cookie"];
  if (cookie != "") {
    authCookies = cookie;
  }
}

Done! Save the changes of CustomRules.js. Then (having Fiddler running and capturing network traffic!) start IE, navigate to your O365 site, and authenticate yourself when requested. Cookies are cached in Fiddler at this point.

Note: In my development environment I always enable Fiddler to decrypt HTTPS traffic. I have not tested this solution with decryption disabled, and have doubts, if it should work. If you test it, please, leave us a comment with the results.

image

In the next step (the same Fiddler instance is still running and capturing network traffic!), try to reconnect LINQPad to the same O365 site. Cookies are replayed by Fiddler, authentication in LINQPad should work this time.

image

To test the functionality, I submitted a simple query:

image

So far the good news. After “solving” the authentication issue, let’s see a further problem, and that is bound to the (missing) $metadata support of Microsoft’s OData implementation in SP 2013.

As you might have noticed, in the example above I used the “old-style”, SP 2010 compatible version of the REST API (_vti_bin/ListData.svc), and not the “new-school” format, including _api (like _api/web/), and that is no just accidentally.

Since LINQPad needs the $metadata to build up the object structure, it simply does not work without that:

In LINQPad:

Error: The remote server returned an error: (404) Not Found.

image

In Fiddler (HTTP status 404):

Cannot find resource for the request $metadata.

One of the workarounds may be (again with Fiddler) to use an existing beta installation of SP 2013, capture the response for the $metadata request to a file, then in the development environment send it as a response for the $metadata request from LINQPad automatically, but it is rather hacky, even for me. In my opinion it simply does not worth monkeying so much with that, we should rather learn and use the syntax of the OData requests.

Developers (including myself) who need to work against Project Online are luckier. Although the OData service of PS seems to be available only through the new _api interface, the $metadata support is still there in this case.

image

And a sample query:

image

Have fun using LINQPad against your O365 site and Project Online!

UPDATE: The same trick can be applied, when we would like to add a service reference to our Visual Studio project, referring to an O365 / Project Online site (just like described here for an on-premise PS). Although VS 2012 displays an authentication dialog for my – in this case German – O365 site (it is not the case with VS 2010),

image

it seems to have no effect (at least, not always?):

image

Note, that based on the error details it is likely not just a simple authentication issue, as VS would like to append /_vti_bin/ListData.svc to the service URL.

In this case you can use Fiddler again to replay the cookies and authenticate on behalf of VS:

image

UPDATE 2 (3/22/2013): I was wrong when I wrote that developers working against Project Online were much luckier. I’ve just realized, that although $metadata is really available for the ProjectData service, it is not supported for the ProjectServer service, that you should use to access entities like enterprise resources, calendars or custom fields (as illustrated by the next screenshot, requesting https://yourProjects.sharepoint.com/sites/pwa/_api/ProjectServer).

image

In LINQPad:

Error: The remote server returned an error: (404) Not Found.

image

In Fiddler (HTTP status 404):

Cannot find resource for the request $metadata.

July 15, 2012

Emulating slow server response times using Fiddler

Filed under: Fiddler, Testing — Tags: , — Peter Holpar @ 20:22

Recently I’m working on a Silverlight business application that communicates with the server side using RIA web services. On the customer’s test system we experienced an unpleasant behavior in the asynchronous operations due to the slow response time of some background systems. In our development system we were not able to reproduce these issues.

Fortunately, Fiddler can help to emulate the slow server response times as well as slow network speeds.

Setting slow network speed is easy using the UI. One should check the Simulate Modem speeds option in Rules / Performance.

image 

If you need to set custom speeds, you should modify the upload / download speeds (delay ms per KB) through the request-trickle-delay and response-trickle-delay session parameters (see these samples for details) in the CustomRules.js (Rules / Customize Rules…).

Our case was a bit complicated, as the amount of the network traffic was really not proportional with the response times. Small response packages was just as slow as huge ones. So I had to use a custom, fix delay in the OnBeforeResponse function.

I have achieved this delay using a custom wait function:

static function wait(msecs)
{
var start = new Date().getTime();
var cur = start;
while(cur – start < msecs)
{
   cur = new Date().getTime();
}
}

Then I can use this call in OnBeforeResponse:

wait(5000);

However, using this simple function call would make every response slow, not only the ones received from our test web server. For example, a web search, or checking your web mail on the same system would be slow as well – a quite inconvenient side effect.

The solution to the problem was an extra condition:

if (oSession.HostnameIs("localhost:64399"))
{
  wait(5000);
}

where localhost:64399 is the name and the port number of our test web system.

December 30, 2011

Synchronizing SharePoint tasks with Outlook including categories

Filed under: Fiddler, Outlook, SP 2010 — Tags: , , — Peter Holpar @ 00:30

Recently one of our customers requested a SharePoint task list that is synchronized with Outlook. That is usually quite easy, but in this case we should have provided a way to tag the tasks and group them based on this multi-value property on the Outlook side, similarly to the default Categories field feature.

As you may know, only a limited set of task properties are synchronized between SharePoint and Outlook, but details –  like what exactly these properties are and how the synchronization process works – are not very well documented.

To investigate the behavior I’ve created a test list called OLTasks based on the Tasks list template in SharePoint.

image

Then connected the list to Outlook.

image

I’ve started Fiddler and found that the Lists web service is used for synchronization. First, the GetList method is called.

image

Next, a GetListItemChangesSinceToken request is sent. This method is suggested by Microsoft for item synchronization. As one can read on MSDN:

“The most efficient way to synchronize third-party clients with Microsoft SharePoint Foundation 2010 is to download only those items that have changed since the last synchronization occurred. You can do this in SharePoint Foundation 2010 by calling the GetListItemChangesSinceToken Web method.”

The first request contains no token, and the response includes the entire list schema (not shown below). Since our task list contains no item, the ItemCount is zero.

image

Note, that the request above contains the Categories property, however, our Task content type has no such field.

image

I’ve created a new task item in the list to see how it is downloaded to Outlook.

image

image

Again, the GetListItemChangesSinceToken method was called, in this case there was a token in the request (changeToken is not shown below due to lack of space) and the value of ItemCount was 1 in response.

image 

After synchronization, the item appeared in Outlook:

image

image

I’ve modified the task description, and found that on the next synchronization the UpdateListItems method was called to upload changes to SharePoint.

image

Before and after calling the UpdateListItems method the GetListItemChangesSinceToken method was called to detect possible conflicts and synchronize back changes from server.

To provide the Categories field for our tasks, I’ve added the existing site column with the same name to the list.

image

image 

Set a test value for the field at the existing task,

image

and created a new one with other test values. Note, that I’ve specified two values in this case, separated by a comma.

image

As expected the values are synchronized down to Outlook, as shown in this view, grouped by the Categories field.

image

When opening Task 2, we found that our category values are not in the Master Category List.

image

We can resolve it – and add some color codes as well – by clicking New… on the dialog box.

image

After this configuration, Outlook handles our categories as known ones. Next, I’ve set a new category for the task in Outlook.

image

The Outlook view reflects the changes.

image

The UpdateListItems method uploads the changes to SharePoint.

image

And the updated values are displayed in our SharePoint list as well.

image

I hope this quick guide helps you to better understand the default synchronization process, and to utilize similar techniques in your applications.

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.

June 2, 2011

(Re-)enabling Flash content on SharePoint pages

Filed under: Fiddler, Flash, PowerShell, SP 2010 — Tags: , , , — Peter Holpar @ 22:22

Recently I was to migrate a MOSS 2007 solution to SP 2010. Everything was OK but I found that the Flash content – .swf files were stored in the SharePoint content DB – was displayed as static content, not like an animation, only the first frame was shown.

My first assumption was that there might be an issue with the Content-Type HTTP header sent by the server, but checking the traffic by Fiddler proved the header is therewith the correct value (see red marking below):

image

However, there were a few interesting headers there, like the X-Download-Options: noopen (see the yellow marking above).

After a quick search on the web I found this post that helped me to solve the issue:

Open PDF File in Browser from SharePoint 2010

Based on the article I run this PowerShell script on the server:

$webApp = Get-SPWebApplication http://yourserver
$webApp.AllowedInlineDownloadedMimeTypes.Add("application/x-shockwave-flash" )
$webApp.Update()

… and run an IISRESET.

Due to the change the X-Download-Options header disappeared and the Flash animations worked as designed.

Older Posts »

Blog at WordPress.com.