Karine Bosch’s Blog

On SharePoint

Walkthrough 1 – Integrating Silverlight 3 in a Custom List Template (part 1)


The sample that will be explained here is about job opportunities. Human resources can create a job opportunity for each vacant job position in the company and publish it on the company portal. A job opportunity consists of a job title, a job description and required skills.

The list for storing the job opportunities will be defined using CAML and will inherit from the out of the box Generic List. In other sections of my blog I already explained extensively how you can create your own site columns, content types and list definitions. In this section I will explain how you can use Silverlight to make the New and/or Edit forms of a list or document library more attractive and more intuitive for end users. Note that the complete user interface is replaced by a Silverlight application. This means that the Save behavior needs to be duplicated in the Silverlight application. You can also replace only one column by a Silverlight application and use the standard OK and Cancel buttons of the New and Edit form. But this will be demonstrated in a later sample.

I built this sample together with my colleague Kevin DeRudder from U2U somewhat a year ago for a presentation for the Silverlight User Group in Belgium.

You can read the development details in the following 9 steps. You can download the sample here.

Step 1 – Create the site columns

The metadata of the list that will store the job opportunities, consists of the following columns:

  • Job Title
  • Job Description
  • We Offer
  • Required Skills

A site column for Title already exists and will be used to store the job Title. The new site columns are defined in the sitecolumns.xml file:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
 <Field ID="{68C470A1-61AA-43be-949E-034E52EDC60F}"
        Name="JobDescription" DisplayName="Job Description"
        Group="BESUG Site Columns"
        Type="Note"
        UnlimitedLengthInDocumentLibrary="TRUE"
        NumLines="10"
        RichText="TRUE"
        Required="FALSE"/>
 <Field ID="{1DF4A0C6-5E6C-4ddd-BF28-04D382A4D256}"
        Name="WeOffer" DisplayName="We Offer"
        Group="BESUG Site Columns" Type="Note"
        UnlimitedLengthInDocumentLibrary="TRUE"
        NumLines="10"
        RichText="TRUE"
        Required="FALSE"/>
 <Field ID="{4AC02FF8-AC7C-4fc4-AC6D-467A42ED2832}"
        Name="RequiredSkills" DisplayName="Required Skills"
        Group="BESUG Site Columns"
        Type="MultiChoice"
        Format="RadioButtons"
        FillInChoice="TRUE"
        Required="FALSE">
        <Default>C#</Default>
        <CHOICES>
           <CHOICE>C#</CHOICE>
           <CHOICE>VB.NET</CHOICE>
           <CHOICE>ASP.NET</CHOICE>
        </CHOICES>
 </Field>

</Elements>

Step 2 – Create the content type

The content type inherits from the Item content type and adds the necessary fields. It also specifies that the standard form will be used to display an existing job opportunity. The New form and the Edit form will be replaced by a custom form with name SLJobOpportunityForm. You find following definition in the contenttype.xml file:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
 <ContentType ID="0x0100DF420F1E24BF43CEA66C54C70EBC1DD4"
     Name="Job Opportunity Content Type"
     Description="Job Application Content Type"
     Group="BESUG Content Types"
     Version="1">
  <FieldRefs>
   <FieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}"
      Name="Title"
      Required="TRUE"
      DisplayName="Job Title" />

   <FieldRef ID="{68C470A1-61AA-43be-949E-034E52EDC60F}" DisplayName="Job Description"/>
   <FieldRef ID="{1DF4A0C6-5E6C-4ddd-BF28-04D382A4D256}" DisplayName="We Offer"/>
   <FieldRef ID="{4AC02FF8-AC7C-4fc4-AC6D-467A42ED2832}" DisplayName="Required Skils"/>
  </FieldRefs>
  <XmlDocuments>
   <XmlDocument NamespaceURI="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
    <FormTemplates xmlns="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
     <Display>ListForm</Display>
     <Edit>SLJobOpportunityForm</Edit>
     <New>SLJobOpportunityForm</New>
    </FormTemplates>
   </XmlDocument>
  </XmlDocuments>
 </ContentType>
</Elements>

Step 3 – Create the custom list definition

The custom list is based on a custom list definition. The schema.xml has been copied from the Generic List and modified to the needs of this list:

  • the Job Opportunity content type has been added to the <ContentTypes> element
  • the necessary site columns have been added to the <Fields> element
  • the necessary site columns have been added to the <ViewFields> element of the different views

A custom list template and list instance are defined as follows in the listtemplate.xml file:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

  <ListTemplate Name="SLJobList"
      DisplayName="Job Opportunity List"
      Description="Silverlight enabled list template for storing job opportunities."
      BaseType="0"
      Type="1002"
      RootWebOnly="false"
      OnQuickLaunch="TRUE"
      SecurityBits="11"
      Sequence="410"
      Category="Custom Lists"
      Image="/_layouts/images/Besug/besug.PNG" /> 

  <ListInstance TemplateType="1002"
      Id="SLJobOpportunities"
      Title="SL Job Opportunities"
      Url="Lists/SLJobList"
      OnQuickLaunch="True">
  </ListInstance>

  <ContentTypeBinding ContentTypeId="0x0100DF420F1E24BF43CEA66C54C70EBC1DD4" ListUrl="Lists/SLJobList"/>
</Elements>

Step 4 – Create the custom control template to host the Silverlight application

When you want your custom list to use a custom new and/or edit form, you need to create a control template that you deploy to the 12\TEMPLATE\CONTROLTEMPLATES folder. A control template is always defined in a .ascx control. I will also deploy the Silverlight application to that location. The control template with name SLJobOpportunityForm is defined in the SLJobApplication.ascx control and looks as follows:

<%@ Control Language="C#" %>
<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="SharePoint" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" namespace="Microsoft.SharePoint.WebControls"%>
<%@ Register TagPrefix="wssuc" TagName="ToolBar" src="~/_controltemplates/ToolBar.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ToolBarButton" src="~/_controltemplates/ToolBarButton.ascx" %>
<SharePoint:RenderingTemplate ID="CompositeField1" runat="server">
    <Template>
    <TR>
        <TD width="30%" valign="top">
            <SharePoint:FieldLabel ID="FieldLabel1" runat="server"/>
        </TD>
        <TD width="70%" valign="top">
            <SharePoint:FormField ID="FormField1" runat="server"/>
        </TD>
    </TR>
    </Template>
</SharePoint:RenderingTemplate>
<SharePoint:RenderingTemplate ID="SLJobOpportunityForm" runat="server" >
    <Template>
    <script language="c#" runat="server">
        public string WebUrl
        {
            get { return Microsoft.SharePoint.SPContext.Current.Web.Url; }
        }
        public string ListName
        {
            get { return Microsoft.SharePoint.SPContext.Current.List.Title; }
        }
    </script>
    <input type="hidden" ID="HiddenUrlField" value="<%= WebUrl %>" />
    <input type="hidden" ID="HiddenListField" value="<%= ListName %>" />           
    <div id="silverlightControlHost" style="width:650;height:800">
        <object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
            width="650" height="800">
            <param name="source" value="/_layouts/SL Job Opportunity/SLJobOpportunityForm.xap" />
            <param name="initParams" value="uctlid=HiddenUrlField,lctlid=HiddenListField" />
            <a href="http://go.microsoft.com/fwlink/?LinkID=124807" style="text-decoration: none">
                <img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight"
                    style="border-style: none" />
            </a>
        </object>
    </div>
    </Template>   
</SharePoint:RenderingTemplate>

The <object> tag is used to host the Silverlight 3 application in the control template. The control template has also 2 hidden controls: one for the SharePoint site URL and one for the name of the list where the job applications will be created. Both hidden fields are filled when the control is loaded and the id of the hidden fields is passed with the initParams parameter as a comma delimited string. The source parameter is set to the location and the name of the Silverlight application. As you can see the Silverlight application is deployed to a sub folder of the 12\TEMPLATE\LAYOUTS folder.

Step 5 – Build the Silverlight user interface

I will not go in too much detail on the XAML of the Silverlight application. It consists of a number of controls defined in the Page.xaml

To achieve the coloring of the buttons a special theme is used. The Silverlight toolkit contains – besides a large number of controls – a number of themes that you can use in your Silverlight applications.  In this sample the BureauBlue theme is applied. You can download the toolkit here.

<UserControl
    x:Class="CreateNewJobForm.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"   
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
    xmlns:bureauBlue="clr-namespace:System.Windows.Controls.Theming;assembly=System.Windows.Controls.Theming.BureauBlue"
    xmlns:Blacklight_Controls="clr-namespace:Blacklight.Controls;assembly=Blacklight.Controls"
    Width="640" Height="520" >
    <bureauBlue:BureauBlueTheme Background="Transparent">
        <Grid x:Name="LayoutRoot" Background="Transparent">
        <!-- here comes the rest of the controls -->
        </Grid>
    </bureauBlue:BureauBlueTheme>
</UserControl>

Step 6 – The initialization of the Silverlight Application

As already mentioned, the URL of the SharePoint site and the name of the Job Opportunity list are stored in hidden fields when the New/Edit form loads. The IDs of the hidden fields are passed to the Silverlight application in the initParameters argument of the Silverlight control. When the Silverligth application is initiated, the parameters become available to the Silverlight application as a dictionary. Based on the IDs in the dictionary the hidden fields can be retrieved because Silverlight can access other controls on the same page through the HtmlPage.Document class.

When an existing job opportunity is edited, the already existing data must be shown. Therefore the Silverlight application also needs to retrieve the ID of the current job opportunity. This ID is passed by SharePoint in the QueryString. The QueryString constructed by SharePoint also contains a source parameters. This contains the URL of the page to which must be navigated when updates to the form are saved. In general this will be the AllItems.aspx page. All data is stored in class level static variables that can be consulted from within the Silvelright controls of the application.

        public static string SourceUrl;
        public static string SiteUrl;
        public static string ListName;
        public static int ItemID = 0;  // I initialize this variable to -1 indicating that a new list item will be created.

        private void OnStartup(object sender, StartupEventArgs e)
        {
            string urlFieldId = null;
            string listFieldId = null;
            if (e.InitParams != null)
            {
                if (e.InitParams.ContainsKey("uctlid"))
                    urlFieldId = e.InitParams["uctlid"];
                if (e.InitParams.ContainsKey("lctlid"))
                    listFieldId = e.InitParams["lctlid"];
            }
            // retrieve the web URL and the list name where the applications are stored
            foreach (HtmlElement el in HtmlPage.Document.GetElementsByTagName("input"))
            {
                if (urlFieldId != null && el.Id.Contains(urlFieldId))
                {
                    SiteUrl = (string)HtmlPage.Document.GetElementById(el.Id).GetProperty("Value");
                }
                else if (listFieldId != null && el.Id.Contains(listFieldId))
                {
                    ListName = (string)HtmlPage.Document.GetElementById(el.Id).GetProperty("Value");
                }
            }
            // also retrieve the item ID from the querystring
            if (HtmlPage.Document.QueryString.ContainsKey("ID"))
            {
                int.TryParse(HtmlPage.Document.QueryString["ID"].ToString(), out ItemID);
            }
            // retrieve the source URL from the querystring. It will be used to return back to the list.
            if (HtmlPage.Document.QueryString.ContainsKey("Source"))
            {
                SourceUrl = HtmlPage.Document.QueryString["Source"].ToString();
            }
            // Load the main control here
            this.RootVisual = new Page();
      }

Step 7 – Retrieve information from the SharePoint site

When an existing job opportunity needs to be displayed, the QueryString will contain an ID argument. In that case the existing job opportunity needs to be retrieved from the SharePoint site. For most of the calls to SharePoint your can use the standard SharePoint web services. The retrieve a list item you can use the GetListItems method of the Lists.asmx. The SOAP envelop for this method looks as follows:

        private const string retrieve_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=""""><Where><Eq><FieldRef Name=""ID"" /><Value Type=""Number"">{1}</Value></Eq></Where></Query></query>
                            <viewFields>
                            <ViewFields xmlns="""">
                                <FieldRef Name=""Title"" />
                                <FieldRef Name=""JobDescription"" />
                                <FieldRef Name=""WeOffer"" />
                                <FieldRef Name=""RequiredSkills"" />
                            </ViewFields>
                            </viewFields>
                            <queryOptions><QueryOptions xmlns=""""><IncludeMandatoryColumns>False</IncludeMandatoryColumns></QueryOptions></queryOptions>
                       </GetListItems>
                   </soap12:Body>
                </soap12:Envelope>";

 If you’re not quite sure how this SOAP envelop looks like you can open an Internet Explorer and navigate to the Lists.asmx web service. There you can explore the SOAP envelope for each method on the web service.

The SOAP envelope will be completed with the query and the  name of the list on which the query will be executed. Web services can be called from within Silverlight using the HttpWebRequest and HttpWebResponse classes. Both HttpWebRequest and HttpWebResponse work asynchronously and on different threads. There are different techniques to come back on the UI thread after the response has been processed by the server and returned to the client: the Dispatcher and the SynchronizationContext.
This sample uses the SynchronizationContext which is a class that helps you synchronise different threads. Before you start a request to the server, you have to store a reference to the current SynchronizationContext while on the UI thread:

        private void BeginRequest()
        {
            // Grab SynchronizationContext while on UI Thread  
            syncContext = SynchronizationContext.Current;
            // Information of the products that should be downloaded is obtained by calling the SharePoint Lists.asmx web service
            // using HttpWebRequest. The call to the Request as to the Response are asynchronous.
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri(App.SiteUrl + "/_vti_bin/Lists.asmx", UriKind.Absolute));
            request.Method = "POST";
            request.BeginGetRequestStream(new AsyncCallback(RequestCallback), request);
        }

Then the request is created and sent to the server. The Create method is a static method on the base WebRequest class and expects a URI with the location of the internet resource. In this case you need to specify the location of the SharePoint Lists.asmx web service. You typically access this web service by using the URL to the SharePoint web (not the site collection), completed with /_vti_bin/, which is the location where the SharePoint web services are deployed, and the name of the web service. Specify also that the URI contains an absolute URL.
Call the BeginGetRequestStream method to start the request to the server. Specify a callback method that must be called when the server finishes processing the request. In this case this is the RequestCallback method.
In this method the SOAP envelope is completed with the name of the list and the query to retrieve the list item.

        // in the request a soap envelop is build based on the schema needed by the Lists.asmx. The GetListItems method is called on
        // the picture library selected by the user (in the web part).
        private void RequestCallback(IAsyncResult asyncResult)
        {
            try
            {
                string envelope = PrepareEnvelope();
                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;
            }
        }

 When the server comes back with a response, it comes back in the callback method but on a different thread. This method calls the EndGetResponse method to extract the response. It also calls the Post method on the SynchronizationContext variable to come back on the UI thread.

        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);
        }

The ExtractResponse method checks the response. If an error occurred an error message will be displayed. If the status code is OK the response will be processed in the ProcessResponse method.

        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();
        }

The response coming back from a SharePoint web service method will always be XML. The ProcessResponse method will use LINQ to extract the information needed, like the job title, job description, offer and skills.

        private void ProcessResponse()
        {
            switch (requestType)
            {
                case RequestType.Retrieve:
                    job = new JobData();
                    XDocument results = XDocument.Parse(responsestring);
                    var query = from item in results.Descendants(XName.Get("row", "#RowsetSchema"))
                                select new JobData()
                                {
                                    JobTitle = item.Attribute("ows_Title").Value,
                                    Description = item.Attribute("ows_JobDescription").Value,
                                    WeOffer = item.Attribute("ows_WeOffer").Value,
                                    Skills = RetrieveSkills(item.Attribute("ows_RequiredSkills").Value)
                                };
                    job = (JobData)query.First();
                    // Fill the form controls
                    JobTitleTextBox.Text = job.JobTitle;
                    JobDescriptionTextBox.Text = job.Description;
                    WeOfferTextBox.Text = job.WeOffer;
                    PopulateSkillsPanel(job.Skills);
                    break;
                default:
                    // this must redirect to the AllItems.aspx page
                    if (!string.IsNullOrEmpty(App.SourceUrl))
                        HtmlPage.Window.Navigate(new Uri(App.SourceUrl, UriKind.Absolute));
                    break;
            }
        }

Step 8 – The Save process

When the Human Resource manager has filled out the form, the job opportunity needs to be stored to the SL Job Opportunity list. To save list items to a SharePoint list you can use the UpdateListItems method of the Lists.asmx web service. There are two SOAP envelopes for this method, one for creating a new job opportunity and one for updating an existing job opportunity:

       private const string new_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>
                        <UpdateListItems xmlns=""http://schemas.microsoft.com/sharepoint/soap/"">
                            <listName>{0}</listName>
                            <updates>
                               <Batch PreCalc=""TRUE"" OnError=""Continue"">
                                  <Method ID=""1"" Cmd=""New"">
                                    <Field Name=""Title"">{1}</Field>
                                    <Field Name=""JobDescription"">{2}</Field>
                                    <Field Name=""WeOffer"">{3}</Field>
                                    <Field Name=""RequiredSkills"">{4}</Field>
                                  </Method>
                               </Batch>
                            </updates>
                        </UpdateListItems>
                    </soap12:Body>
                </soap12:Envelope>";
       private const string update_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>
                        <UpdateListItems xmlns=""http://schemas.microsoft.com/sharepoint/soap/"">
                            <listName>{0}</listName>
                            <updates>
                               <Batch PreCalc=""TRUE"" OnError=""Continue"">
                                  <Method ID=""1"" Cmd=""Update"">
                                    <Field Name=""ID"">{1}</Field>
                                    <Field Name=""Title"">{2}</Field>
                                    <Field Name=""JobDescription"">{3}</Field>
                                    <Field Name=""WeOffer"">{4}</Field>
                                    <Field Name=""RequiredSkills"">{5}</Field>
                                  </Method>
                               </Batch>
                            </updates>
                        </UpdateListItems>
                    </soap12:Body>
                </soap12:Envelope>";

The same technique is used using the HttpWebRequest and HttpWebResponse methods. When the server comes back from saving the job opportunity, it comes back in the ProcessResponse method. This method redirects the user to the AllItems.aspx page of the SL Job Opportunity list.

Step 9 – Deploy and Test

The structure of the Visual Studio project looks as follows:

When the whole thing is deployed to SharePoint, you can activate the Silverlight Job Opportunity feature. This is a site collection feature. Navigate to the SL Job Opportunity list and click th New button. The New form opens displaying the Silverlight application.

Fill out some data and click the Save button. The Silverlight application will creat a new list item in the SharePoint list and navigate to the AllItems.aspx page.

You can download the sample here.

Enjoy!

 

2 Comments »

  1. Hi Karine,

    It was nice article. But in ur site menu page right side Having allignment problem in all browsers. Please try to fix that .

    Bcos i found difficulty in choosing of SharepointListPropertyFeatureMenu.

    Dont Take it as negative. ur blog is good. If u improve means it ll be too good

    Comment by Sarath | March 12, 2014 | Reply

    • Thanks for the feedback, Sarath. I won’t take it as negative 🙂

      Comment by Karine Bosch | March 22, 2014 | Reply


Leave a comment