Silverlight Data Binding Sample upgraded to Silverlight 2 Final Release

Recently I wrote an article for an online SharePoint magazine. It’s about hosting a Silverlight 2 application in a a SharePoint web part. You can also read the article on the U2U web site. I upgraded the sample to Silverlight 2 final release. You can download the upgraded code and the list templates here.

It is a tutorial in which I explain how you can host a Silverlight 2 application from within a SharePoint Web Part. The Web Part will pass the URL of the SharePoint site together with the name of the list for which the Silverlight application will show the data. The retrieval of the data will be done by the Silverlight application using the HttpWebRequest technique for calling the SharePoint web services. As the SharePoint web services return a chunk of XML the XML will be handled by using LINQ for XML. The data will be bound to the Silverlight controls.

There are a few changes to take into account when working with Silverlight 2 final release.

The only thing to change in the SharePoint web part is in the CreateChildControl method: you have to set the MinimumVersion property of the Silverlight control to 2.0.30923.0.

protected override void CreateChildControls()
{
    base.CreateChildControls();

    // instantiation of the silverlight control
    silverlightControl =
      new System.Web.UI.SilverlightControls.Silverlight();
    silverlightControl.ID = "SLAdventureWorks";
    silverlightControl.MinimumVersion = "2.0.30923.0";
    silverlightControl.Width = new Unit(750);
    silverlightControl.Height = new Unit(600);
    silverlightControl.Source =
       this.Page.ClientScript.GetWebResourceUrl(this.GetType(),
        "SL.AdventureWorksProducts.Resources.SL.XAML.AdventureWorksProducts.xap");

    // parameters passed are the URL of the SharePoint site and
    //the name of the list containing the AdventureWorks Products
    silverlightControl.InitParameters = "siteurl=" +
       SPContext.Current.Web.Url +
       ",listname=AdventureWorks Products";

    this.Controls.Add(silverlightControl);
}

 

The major change lies in the silverlight application, specifically in the functioning of the HttpWebRequest object.

1. The HttpWebRequest object works completely asynchronous and on different threads as the main thread. This means that the Silverlight application continues processing while the request to the Web Service is executed. Before you start a request to a Web Service, in this case a SharePoint web service, you have to grab the context of the UI thread.

syncContext = SynchronizationContext.Current;

The syncContext variable is a class-level variable:

SynchronizationContext syncContext;

 

2. Create the HttpWebRequest object passing it the URL of the SharePoint web service and specify a callback method on which the HttpWebRequest can come back after processing the request. The request will come back on a background thread.

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
        new Uri(siteUrl + "/_vti_bin/Lists.asmx", UriKind.Absolute));
request.Method = "POST";
request.BeginGetRequestStream(new AsyncCallback(RequestCallback),
        request);

 

3. The RequestCallback method contains the code for the soap envelope. The body is retrieved from the request stream by calling the EndGetRequestStream method of the HttpWebRequest and completed with the soap envelope. Then a callback method is defined for the response to come back on. Also the response will come back on a different background thread. The request stream must be closed before the BeginGetResponse method of the HttpWebRequest object is called.

private void RequestCallback(IAsyncResult asyncResult)
{
   try
   {
      string envelope = @"<?xml version=""1.0"" encoding=""utf-8""?>
        <soap12:Envelope xmlns:xsi=""
http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap12=""http://www.w3.org/2003/05/soap-envelope"">
        <soap12:Body>
        <GetListItems xmlns=""
http://schemas.microsoft.com/sharepoint/soap/"">
              <listName>{0}</listName>
              <query><Query xmlns="""">{1}<OrderBy><FieldRef Name=""Title"" /></OrderBy></Query></query>
              <viewFields><ViewFields xmlns="""">
                    <FieldRef Name=""ID"" />
                    <FieldRef Name=""Title"" />
                    <FieldRef Name=""ProductName"" />
                    <FieldRef Name=""ListPrice"" />
                    <FieldRef Name=""Thumbnail"" />
                    <FieldRef Name=""Color"" />
                    <FieldRef Name=""Weight"" />
                    <FieldRef Name=""Size"" />
                    <FieldRef Name=""Description"" />
                 </ViewFields>
              </viewFields>
             <queryOptions><QueryOptions xmlns=""""><IncludeMandatoryColumns>False</IncludeMandatoryColumns></QueryOptions></queryOptions>
        </GetListItems>
        </soap12:Body>
      </soap12:Envelope>";

     string query = string.Empty;
     if (!string.IsNullOrEmpty(searchstring) && searchstring != "Topic...")
     {
        query = "<Where><Contains><FieldRef Name=\"Title\" />" +
           ";<Value Type=\"Text\">{0}</Value></Contains></Where>";
        query = string.Format(query, searchstring);
     }
     envelope = string.Format(envelope, listName, query);
     HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState;
     request.ContentType = "application/soap+xml; charset=utf-8";
     request.Headers["ClientType"] = "Silverlight";

     Stream requestStream = request.EndGetRequestStream(asyncResult);
     StreamWriter body = new StreamWriter(requestStream);
     body.Write(envelope);
     body.Close();

     request.BeginGetResponse(new AsyncCallback(ResponseCallback), request);
  }
  catch (WebException ex)
  {
     responsestring = ex.Message;
  }

        }

 

3. When the response comes back on a background thread, the response is retrieved. If an error occurred along the way, it is captured in a class-level string variable. The UI thread is invoked by calling the Post method on the context variable, passing the address of the ExtractResponse method to it.

private void ResponseCallback(IAsyncResult asyncResult)
{
     HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState;
     WebResponse response = null;

     try
     {
         response = request.EndGetResponse(asyncResult);
     }
     catch (WebException we)
     {
         responsestring = we.Status.ToString();
     }
     catch (System.Security.SecurityException se)
     {
         responsestring = se.Message;
         if (responsestring == "")
            responsestring = se.InnerException.Message;
     }
     syncContext.Post(ExtractResponse, response);             
}

 

4. The ExtractResponse method executes the GetResponseStream method on the response and retrieves the resulting xml from the stream.

private void ExtractResponse(object state)
{
    HttpWebResponse response = state as HttpWebResponse;

    if (response != null && response.StatusCode == HttpStatusCode.OK)
    {
       using (StreamReader reader = new StreamReader(response.GetResponseStream()))
       {
           responsestring = reader.ReadToEnd();
           ProcessResponse();
       }
    }
    else
       ProcessMessage();
}

 

5. The rest of the code remains the same. The ProcessResponse method parses the resulting XML with LINQ to XML into objects and the object collection is bound to the ListBox.

The update of the list price in the product detail box works in a similar way, except that it executes the UpdateListItems method of the Lists.asmx web service.

If you are configuring the web.config of your SharePoint web applications the “lazy” way please verify the HttpHandlers section. It should look like the following:

  <httpHandlers>
      <remove verb="GET,HEAD,POST" path="*" />
      <add verb="GET,HEAD,POST" path="*" type="Microsoft.SharePoint.ApplicationRuntime.SPHttpHandler, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      <add verb="OPTIONS,PROPFIND,PUT,LOCK,UNLOCK,MOVE,COPY,GETLIB,PROPPATCH,MKCOL,DELETE,(GETSOURCE),(HEADSOURCE),(POSTSOURCE)" path="*" type="Microsoft.SharePoint.ApplicationRuntime.SPHttpHandler, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      <add verb="*" path="Reserved.ReportViewerWebControl.axd" type="Microsoft.Reporting.WebForms.HttpHandler, Microsoft.ReportViewer.WebForms, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
      <add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
      <add verb="*" path="*_AppService.axd" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
      <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="false" />
      <remove verb="*" path="*.asmx" />
      <add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      <add verb="*" path="*_AppService.axd" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" validate="false" />
  </httpHandlers>

 

Otherwise SharePoint web services and the Silverlight HttpWebRequest object will NOT work.