Sunday, November 25, 2012

Problem with context site in Sharepoint when call web service from javascript

If you need to call web service (here I will talk about old asmx web services) from javascript in ASP.Net or Sharepoint you may use auto-generated js proxy which is added to html output when you add ServiceReference to the ScriptManager instance for the current page:

   1: var scriptManager = ScriptManager.GetCurrent(this.Page);
   2: if (scriptManager == null)
   3: {
   4:     scriptManager = new ScriptManager();
   5:     this.Controls.Add(scriptManager);
   6: }
   7:  
   8: var referenceProxy = new ServiceReference();
   9: referenceProxy.Path = "/_layouts/test/test.asmx";
  10: referenceProxy.InlineScript = true;
  11:  
  12: scriptManager.Services.Add(referenceProxy);

Your web service’s class should be marked with [ScriptService] attribute, and all web methods which you want to use from javascript should be marked with [ScriptMethod] attribute:

   1: namespace MyNamespace
   2: {
   3:     [ScriptService]
   4:     [WebService(Namespace = "http://example.com/", Name = "Test")]
   5:     [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
   6:     [ToolboxItem(false)]
   7:     public class Test : WebService
   8:     {
   9:         [ScriptMethod]
  10:         [WebMethod]
  11:         public string Foo(string str)
  12:         {
  13:             // ...
  14:         }
  15:     }
  16: }

After that you can call web service from javascript like this:

   1: MyNamespace.Test.Foo("Hello, world!");

However in Sharepoint you may encounter with the problem when use this method. Check the line 9 in the code above which adds service reference to script manager.

   1: referenceProxy.Path = "/_layouts/test/test.asmx";

Path contains the url to the service. As you can see it is located in layouts folder, i.e. it can be called in context of any Sharepoint site. Also we specified that generated proxy should be added as inline script to the output html (referenceProxy.InlineScript = true). In this case we need to use relative url in the ServiceReference.Path property as it is said in the documentation:

If the InlineScript property is set to true, then you must use a relative path that points to the same Web application as the page that contains the ServiceReference instance.

In our example path is relative, so it should be Ok. However if this code runs not on the root site, but on some sub site (e.g. http://example.com/subsite) we will have problem: relative path which we specified will be combined with root url, i.e. web service will be executed in context of root site, but not in context of the sub site. I.e. full url will be the following: http://example.com/_layouts/test/test.asmx. It may cause different problems, e.g. if locale of subsite differs from locale of the root site your users may see incorrectly localized content.

In order to fix it first of all we need to comment the line which sets InlineScript to true. It will allow us to use absolute url in the Path property:

   1: string referenceProxyUrl = SPUrlUtility.CombineUrl(SPContext.Current.Web.Url,
   2:     "/_layouts/test/test.asmx");
   3:  
   4: var referenceProxy = new ServiceReference();
   5: referenceProxy.Path = referenceProxyUrl;
   6: //referenceProxy.InlineScript = true;
   7:  
   8: scriptManager.Services.Add(referenceProxy);

These changes will have the following effect. Proxy will be added as external js file via <script> tag. Src attribute will contain absolute url:

   1: <script src="http://example.com/subsite/_layouts/test/test.asmx/jsdebug" type="text/javascript"></script>

(it added “/jsdebug” to the absolute url to the asmx). As you can see js proxy is loaded from context of the correct sub site now. So problem is solved? Unfortunately no. When you will check SPContext.Current.Web.Url property in the debugger of the web method, you will see that code still runs in context of the root site (it will contain http://example.com url, instead of http://example.com/subsite). And this is regardless of the site in which content js proxy was loaded.

In order to fix it we need to perform one extra step. Open the url which is specified in the src script. You will see the code of the proxy. It contain line which we are interesting in:

   1: MyNamespace.Test.set_path("/_layouts/test/test.asmx");

set_path is internal method generated on the client side only, i.e. there is no such method in our web service. As you can see it still contains relative url. This is exactly what we need. Before to use web service proxy in the javascript, we need to override relative url by absolute. We can get absolute url on server side as shown above using standard SPUrlUtility.CombineUrl() method and then pass it to javascript. After that call set_path with passed value:

   1: MyNamespace.Test.set_path(absoluteUrlFromServerSide);
   2: MyNamespace.Test.Foo("Hello, world!");

And it fixes the problem finally. Now the code of asmx web service will be executed in the context of the sub site. Hope that it will help you. E.g. you will encounter with this problem if will use framework for loading web parts asynchronously which I wrote above in the following post: Create asynchronous web parts for Sharepoint.

PS. However the described solution doesn’t fix problem with different locales which I used as example :) Some time ago I wrote how Sharepoint sets locale of the current thread using language of requested SPWeb: see this post. But because of some reason it didn’t happen in this case. So I fixed problem with locale manually:

   1: namespace MyNamespace
   2: {
   3:     [ScriptService]
   4:     [WebService(Namespace = "http://example.com/", Name = "Test")]
   5:     [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
   6:     [ToolboxItem(false)]
   7:     public class Test : WebService
   8:     {
   9:         [ScriptMethod]
  10:         [WebMethod]
  11:         public string Foo(string str)
  12:         {
  13:             this.ensureCorrectLocale();
  14:             // ...
  15:         }
  16:  
  17:         private void ensureCorrectLocale()
  18:         {
  19:             if (Thread.CurrentThread.CurrentUICulture.LCID !=
  20: SPContext.Current.Web.UICulture.LCID)
  21:             {
  22:                 Thread.CurrentThread.CurrentUICulture =
  23: SPContext.Current.Web.UICulture;
  24:             }
  25:             if (Thread.CurrentThread.CurrentCulture.LCID !=
  26: SPContext.Current.Web.Locale.LCID)
  27:             {
  28:                 Thread.CurrentThread.CurrentCulture =
  29: SPContext.Current.Web.Locale;
  30:             }
  31:         }
  32:     }
  33: }

After that current thread’s locale became correct.

No comments:

Post a Comment