Karine Bosch’s Blog

On SharePoint

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


In part one of this series I already described how you can integrate Silverlight in the data entry process by using a Silverlight application in the New and Edit form of a custom list definition. Appart from a more attractive layout, the sample doesn’t add much extra functionality to SharePoint. But I’m sure you can think about a number of other scenario’s where this technique can be used to have a more appealing and functional data entry form form. In this second part I will take the same technique a little bit further. 

The sample that will be explained here is about a job application form that needs to be filled out when you wants to apply for a job opportunity. In another sample I will explain how to expose all job opportunities in an attractive way in a web part on the home page of the portal. Visitors of the portal can scroll through the different job opportunities. When someone wants to apply for a job opening, he/she clicks on the job opening in the web part which redirects the candidate to the New form of the Job Application list. This form hosts a Silverlight application where the candidate can fill out some standard information like name and first name. The applicant can upload a picture using the Who Are You picture. A job opportunity requires a number of skills. An application can easily rate his/her skills using the rating control on the form.

It contains a number of interesting aspects like:

  • how do you integrate a Silverlight application in a custom NewForm or EditForm
  • how do you communicate with SharePoint from within Silverlight using HttpWebRequest
  • how can you use isolated storage
  • how to create a WCF service and how to call it from within Silverlight
  • how to deploy a Virtual Path provider using an HttpModule

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 download the sample here.

Step 1 – Create the site columns

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

  • Title
  • Last name
  • First name
  • Birthday
  • Skills
  • Application photo
  • City

A number of columns already exists as site columns, so you only need to define site columns for skills, photo of the applicant, and city. The new site columns are defined in the sitecolumns.xml file:

 <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <Field ID="{B22D2E1F-DBCC-480f-9E48-19C34061643B}"
           Name="Skills" DisplayName="Skills" Group="BESUG Site Columns" Type="Text" />
    <Field ID="{64A56830-FAF6-48e9-A49F-E81CEA39BD4A}"
           Name="ApplicantPhoto" DisplayName="Applicant Photo" Group="BESUG Site Columns" Type="Text" />
    <Field ID="{6FAD1254-B14A-46e9-818F-446DEF9AD756}"
           Name="ApplicantCity" DisplayName="City" Group="BESUG Site Columns" Type="Text" />
</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 application. The New form and the Edit form will be replaced by a custom form with name SLJobApplicationForm. You find following definition in the contenttype.xml file:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <ContentType ID="0x0100A09306C0961343dcA2CE649A77C6843F"
               Name="Job Application 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="{4a722dd4-d406-4356-93f9-2550b8f50dd0}" DisplayName="First Name" />
      <FieldRef ID="{475c2610-c157-4b91-9e2d-6855031b3538}" DisplayName="Last Name" />
      <FieldRef ID="{c4c7d925-bc1b-4f37-826d-ac49b4fb1bc1}" DisplayName="Birthday" />
      <FieldRef ID="{6FAD1254-B14A-46e9-818F-446DEF9AD756}" DisplayName="City" />
      <FieldRef ID="{B22D2E1F-DBCC-480f-9E48-19C34061643B}" DisplayName="Skils" />
      <FieldRef ID="{64A56830-FAF6-48e9-A49F-E81CEA39BD4A}" DisplayName="Applicant Photo" />
    </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>SLJobApplicationForm</Edit>
          <New>SLJobApplicationForm</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 Application 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="SLJobApplicationList"
                DisplayName="Job Application List"
                Description="Silverlight enabled list template for storing job applications."
                BaseType="0"
                Type="1001"
                RootWebOnly="false"
                OnQuickLaunch="TRUE"
                SecurityBits="11"
                Sequence="410"
                Category="Custom Lists"
                Image="/_layouts/images/Besug/besug.PNG" />
  <ListInstance TemplateType="1001"
             Id="SLJobApplications"
                Title="SL Job Applications"
                Url="Lists/SLJobApplicationList"
                OnQuickLaunch="True" />
  <ContentTypeBinding ContentTypeId="0x0100A09306C0961343dcA2CE649A77C6843F"
                      ListUrl="Lists/SLJobApplicationList"/>
</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 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="SLJobApplicationForm" 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:700;height:800">
        <object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
            width="700" height="700">
            <param name="source" value="/_layouts/SL Job Application/JobApplicationForm.xap" />
            <param name="initParams" value="uctlid=HiddenUrlField,lctlid=HiddenListField" />
            <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=3.0.40624.0" 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 initParameters parameter as a comma delimited string. The source parameter is set to the name of the Silverlight application, which will be deployed in 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 and styles defined in the Page.xaml and the App.xaml. The Expander controls are those that come with the Silverlight toolkit. It also contains a custom rating control that is used to measure the skills. The user interface has been designed by Kevin DeRudder.

Step 6 – Initialize the Silverlight Application

As already mentioned, the URL of the SharePoint site and the name of the Job Application 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 throught the HtmlPage.Document class.

When an existing job application is edited, the already existing data must be shown. Therefore the Silverlight application also need to retrieve the ID of the current job application. 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. In the demo, the applicant selected a job opportunity from a web part. The ID of this job opportunity is also passed in via the QueryString.

The JobListName variable stores the hard coded name of the list where job opportunities are entered, the PictureLib variable stores the hard coded name of the picture library where applicants can upload their picture.

        public static string JobListName = "SL Job Opportunities";      // can also be passed in from outside using the InitParams argument
        public static string PictureLib = "Applicants Picture Library"; // can also be passed in from outside using the InitParams argument
        public static string SourceUrl;
        public static string SiteUrl;
        public static string ListName;
        public static string JobID;
        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();
            }
            // retrieve the source URL from the querystring. It will be used to return back to the list.
            if (HtmlPage.Document.QueryString.ContainsKey("job"))
            {
                JobID = HtmlPage.Document.QueryString["job"].ToString();
            }
            // Load the main control here
            this.RootVisual = new Page();
  }

Step 7 – Retrieve information from the SharePoint site

When an existing job application needs to be displayed, the QueryString will contain an ID argument. In that case the existing job application 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=""FirstName"" />
                                <FieldRef Name=""FullName"" />
                                <FieldRef Name=""Birthday"" />
                                <FieldRef Name=""ApplicantCity"" />
                                <FieldRef Name=""Skills"" />
                                <FieldRef Name=""ApplicantPhoto"" />
                            </ViewFields>
                            </viewFields>
                            <queryOptions><QueryOptions xmlns=""""><IncludeMandatoryColumns>False</IncludeMandatoryColumns></QueryOptions></queryOptions>
                       </GetListItems>
                   </soap12:Body>
                </soap12:Envelope>";

 Each time the job application New or Edit form is loaded to create or update a job application, also information of the selected  job opportunity is retrieved from the SharePoint site.

        private const string job_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=""RequiredSkills"" />
                            </ViewFields>
                            </viewFields>
                            <queryOptions><QueryOptions xmlns=""""><IncludeMandatoryColumns>False</IncludeMandatoryColumns></QueryOptions></queryOptions>
                       </GetListItems>
                   </soap12:Body>
                </soap12:Envelope>";

Data is retrieved from the SharePoint site using HttpWebRequest calling the GetListItems method of the out of the box Lists.asmx web service. This technique has already been explained in the previous sample. HttpWebRequest and HttpWebResponse are executed asynchronously and on different threads. When the server comes back with a response, it is on a different thread. It can come back on the UI thread by using the SynchronizationContext.

        // Information of the images or movies 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.
        SynchronizationContext syncContext;
        string responsestring;
        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;
            request = (HttpWebRequest)WebRequest.Create(new Uri(App.SiteUrl + "/_vti_bin/Lists.asmx", UriKind.Absolute));
            request.Method = "POST";
            request.BeginGetRequestStream(new AsyncCallback(RequestCallback), request);
        }
        private string PrepareEnvelope()
        {
            string envelope = null;
            switch (requestType)
            {
                case RequestType.Job:
                    envelope = string.Format(job_envelope, App.JobListName, App.JobID);
                    break;
                case RequestType.Retrieve:
                    envelope = string.Format(retrieve_envelope, App.ListName, App.ItemID.ToString());
                    break;
                case RequestType.New:
                    envelope = string.Format(new_envelope, App.ListName,
                        application.FirstName, application.LastName, application.JobTitle,
                        ConvertToSpDate(application.BirthDate), application.City,
                        ConvertToString(application.Skills), application.ApplicantPhoto); //
                    break;
                case RequestType.Update:
                    envelope = string.Format(update_envelope, App.ListName, App.ItemID,
                        application.FirstName, application.LastName, application.JobTitle,
                        ConvertToSpDate(application.BirthDate), application.City,
                        ConvertToString(application.Skills), application.ApplicantPhoto); //
                    break;
            }
            return envelope;
        }
        // in the request a soap envelop is build based on the schema needed by the Lists.asmx.
        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)
            {
                // TODO: replace by correct exception handling
                responsestring = ex.Message;
            }
        }
        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);
        }
        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 ProcessResponse method takes care of populating the UI controls with the resulting XML. A first call to the SharePoint server retrieves the selected job opportunity. The incoming XML contains the data of the selected job opportunity and stores it in an object and displays the title in a TextBox. The required skills are listed so that a candidate can rate each skill separately. 

The second call to the SharePoint server retrieves a job application in case the candidate wants to modify an existing job application. The resulting XML contains the job application and its data is displayed in the different controls on the grid.

        private void ProcessResponse()
        {
            switch (requestType)
            {
                case RequestType.Job:
                    XDocument jobresult = XDocument.Parse(responsestring);
                    var jquery = from item in jobresult.Descendants(XName.Get("row", "#RowsetSchema"))
                                 select new JobData()
                                 {
                                     JobTitle = item.Attribute("ows_Title").Value,
                                     Skills = RetrieveRequiredSkills(item.Attribute("ows_RequiredSkills").Value)
                                 };
                    // Fill the form controls
                    JobData job = (JobData)jquery.First();
                    JobTitleTextBox.Text = job.JobTitle;
                    foreach (string skill in job.Skills)
                        AddSkillToForm(skill);
                    break;
                case RequestType.Retrieve:
                    application = new ApplicationData();
                    XDocument results = XDocument.Parse(responsestring);
                    var query = from item in results.Descendants(XName.Get("row", "#RowsetSchema"))
                                select new ApplicationData()
                                {
                                    FirstName = item.Attribute("ows_FirstName").Value,
                                    LastName = item.Attribute("ows_FullName").Value,
                                    JobTitle = item.Attribute("ows_Title").Value,
                                    BirthDate = ConvertDate(item.Attribute("ows_Birthday").Value),
                                    City = (item.Attribute("ows_ApplicantCity") != null ? item.Attribute("ows_ApplicantCity").Value : null),
                                    Skills = RetrieveSkills(item.Attribute("ows_Skills").Value),
                                    ApplicantPhoto = (item.Attribute("ows_ApplicantPhoto") != null ? item.Attribute("ows_ApplicantPhoto").Value : null)
                                };
                    application = (ApplicationData)query.First();
                    // Fill the form controls
                    LastNameTextBox.Text = application.LastName;
                    FirstNameTextBox.Text = application.FirstName;
                    JobTitleTextBox.Text = application.JobTitle;
                    if (!string.IsNullOrEmpty(application.City))
                        CityTextbox.Text = application.City;
                    BirthdatePicker.SelectedDate = application.BirthDate;
                    // Build the Skill control
                    foreach (SkillData skill in application.Skills)
                    {
                        AddSkillToForm(skill);
                    }
                    // fill out the picture control
                    if (application.ApplicantPhoto != null)
                    {
                        // fill the picture
                        Uri openUri = new Uri(application.ApplicantPhoto, UriKind.Absolute);
                        webClient.OpenReadAsync(openUri);
                    }
                    break;
                case RequestType.New:
                case RequestType.Update:
                    // see step 9 for the code
                    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 – Isolated Storage

The applicant can upload a picture from himself or herself by clicking on the Who Are You image. A dialog opens from where you can browse to the location of your picture. Once a picture selected and the OK button clicked, the image is not yet uploaded to the SharePoint picture library but stored in isolated storage. The picture will be uploaded to the Silverlight picture library when the Save button of the form is clicked.

        private void Image_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Multiselect = false;
            ofd.Filter = "jpeg|*.jpg|All files|*.*";
            bool? retval = ofd.ShowDialog();
            if (retval != null && retval == true)
                selectedPicture = ofd.File;
            else
                selectedPicture = null;
            using (IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication())
            {
                Stream filestream = selectedPicture.OpenRead();
                int filelength = (int)filestream.Length;
                byte[] data = new byte[filelength];
                filestream.Read(data, 0, filelength);
                filestream.Close();
                IsolatedStorageFileStream isoStream = iso.CreateFile(selectedPicture.Name);
                isoStream.Write(data, 0, filelength);
                isoStream.Close();               
            }
            using (IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if(iso.FileExists(selectedPicture.Name))
                {
                    using(IsolatedStorageFileStream isostream = iso.OpenFile(selectedPicture.Name, FileMode.Open))
                    {
                        BitmapImage bmpImg = new BitmapImage();
                        bmpImg.SetSource(isostream);
                        ApplicantImage.Source = bmpImg;
                    }               
                }           
            }
        }

Step 9 – The Save Process

When the candidate has filled the form, he/she can submit the application by clicking the Save button. This button is part of the Silverlight application, so the Save process needs to be duplicated in the Silverlight application. The Save process consists of two parts: the first call to the server saves the application as a list item in the SL Job Applications list. This call executes asynchronously and the application waits for this call to succeed before a second call to the server is made to upload the picture of the applicant to the Application Picture Library.

The code contains two SOAP envelopes: one for creating a new job application list item and one for updating an existing job application list item:

        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=""FirstName"">{1}</Field>
                                    <Field Name=""FullName"">{2}</Field>
                                    <Field Name=""Title"">{3}</Field>
                                    <Field Name=""Birthday"">{4}</Field>
                                    <Field Name=""ApplicantCity"">{5}</Field>
                                    <Field Name=""Skills"">{6}</Field>
                                    <Field Name=""ApplicantPhoto"">{7}</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=""FirstName"">{2}</Field>
                                    <Field Name=""FullName"">{3}</Field>
                                    <Field Name=""Title"">{4}</Field>
                                    <Field Name=""Birthday"">{5}</Field>
                                    <Field Name=""ApplicantCity"">{6}</Field>
                                    <Field Name=""Skills"">{7}</Field>
                                    <Field Name=""ApplicantPhoto"">{8}</Field>
                                  </Method>
                               </Batch>
                            </updates>
                        </UpdateListItems>
                    </soap12:Body>
                </soap12:Envelope>";

 When the Save button is clicked the filled out data is gathered in an object of type ApplicationData, which is a custom object defined in the Silverlight application. If a picture has been selected, the URL is constructed using the name of the picture library and the name of the picture.  Then the BeginRequest method is called to initiate the call to the server. This method prepares the correct SOAP envelope and calls the UpdateListItems method of the out of the box Lists.asmx web service:

        private void SaveButton_Clicked(object sender, RoutedEventArgs e)
        {
            if (CheckFields())
            {
                // update the application object
                if (App.ItemID == 0)
                {
                    requestType = RequestType.New;
                    application = new ApplicationData();
                }
                else
                {
                    requestType = RequestType.Update;
                }
                application.FirstName = FirstNameTextBox.Text;
                application.LastName = LastNameTextBox.Text;
                application.JobTitle = JobTitleTextBox.Text;
                application.BirthDate = BirthdatePicker.SelectedDate;
                application.City = CityTextbox.Text;
                application.Skills = BuildSkillCollection();
                if (selectedPicture != null)
                    application.ApplicantPhoto = App.PictureLibURL + "/" + selectedPicture.Name;
                BeginRequest();
            }
        }

When the job application is saved in the SharePoint list and the response returns to the Silvelright application, a second call to the server is made to store the picture of the candidate. This time a custom WCF service is called to upload the picture. If no picture was selected by the candiate, the form navigates back to the AllItems.aspx page:

        private void ProcessResponse()
        {
            switch (requestType)
            {
                case RequestType.Job:
                    // ... Code omitted for brievety...
                    break;
                case RequestType.Retrieve:
                    // ... Code omitted for brievety...
                    break;
                case RequestType.New:
                case RequestType.Update:
                    // this code is important here!
                    if (selectedPicture != null)
                    {
                        // this starts the upload of the selected picture after the application data is saved to SharePoint list
                        System.IO.FileStream fs = selectedPicture.OpenRead();
                        filecontents = new byte[fs.Length];
                        fs.Read(filecontents, 0, (int)fs.Length);
                        ImageServiceRef.ImageServiceClient client = new ApplicationForm.ImageServiceRef.ImageServiceClient();
                        client.UploadPictureCompleted += new EventHandler<ApplicationForm.ImageServiceRef.UploadPictureCompletedEventArgs>(client_UploadPictureCompleted);
                        client.UploadPictureAsync(App.PictureLib, selectedPicture.Name, selectedPicture.ToString(), filecontents);
                        fs.Close();
                        selectedPicture = null;
                    }
                    else
                    {
                        // this must redirect to the AllItems.aspx page
                        if (!string.IsNullOrEmpty(App.SourceUrl))
                            HtmlPage.Window.Navigate(new Uri(App.SourceUrl, UriKind.Absolute));
                    }

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

The upload of the picture also executes asynchronously. Therefore an event handler is attached to the UploadPictureCompleted event. When the upload of the image completes, this event will be triggered and the form will navigate to the AllItems.aspx page.

        void client_UploadPictureCompleted(object sender, ApplicationForm.ImageServiceRef.UploadPictureCompletedEventArgs e)
        {
            //MessageBox.Show(e.Result.ToString());
            // this must redirect to the AllItems.aspx page
            if (!string.IsNullOrEmpty(App.SourceUrl))
                HtmlPage.Window.Navigate(new Uri(App.SourceUrl, UriKind.Absolute));
        }

Step 10 – Develop a custom WCF service to upload the applicant picture

To upload a picture to a SharePoint picture library I tried to use the out of the box Image.asmx web service but I didn’t come to a successful save. That same night Kevin developed a custom WCF service to upload the picture so I didn’t investigate further. He encountered a problem with the size of the pictures being uploaded.

The contract for the WCF service contains only one method: the UploadPicture method.It accepts arguments like the URL to the SharePoint site, the name of the picture library to which the picture must be uploaded, the name of the picture and the stream of data.

namespace ImagingService
{
    // NOTE: If you change the interface name "IImageService" here, you must also update the reference to "IImageService" in App.config.
    [ServiceContract]
    public interface IImageService
    {
        [OperationContract]
        string UploadPicture(string siteUrl, string pictureLibrary,
        string filename, string fullFileName, byte[] filebytes);
    }
}

The implementation of the contract looks as follows:

namespace ImagingService
{
    // NOTE: If you change the class name "ImageService" here, you must also update the reference to "ImageService" in App.config.
    public class ImageService : IImageService
    {
        #region IImageService Members
        public string UploadPicture(string siteUrl, string pictureLibrary, string filename, string fullFileName, byte[] filebytes)
        {
            try
            {
                SPFolder piclib = null;
                SPSecurity.RunWithElevatedPrivileges(delegate()
                {
                    using (SPSite site = new SPSite(siteUrl))
                    {
                        using (SPWeb web = site.OpenWeb())
                        {
                            try
                            {
                                piclib = web.Lists[pictureLibrary].RootFolder;
                            }
                            catch                            
                            {
                                throw new Exception("Picture library not found.");
                            }                
                            if (piclib != null)
                                piclib.Files.Add(fullFileName, filebytes);
                        }
                    }
                });
                return "OK";

            }
            catch
            {
                return "oopz";
            }
        }
        #endregion
    }
}

Kevin encountered a problem with WCF services and the size of the pictures being uploaded. He solved the problem by adding transferMode=”Streamed” to the binding.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="StreamedBinding" transferMode="Streamed" />
            </basicHttpBinding>
        </bindings>
        <behaviors>
            <serviceBehaviors>
                <behavior name="ImagingService.ImageServiceBehavior">
                    <serviceMetadata httpGetEnabled="true" />
                    <serviceDebug includeExceptionDetailInFaults="false" />
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <services>
            <service behaviorConfiguration="ImagingService.ImageServiceBehavior"
                name="ImagingService.ImageService">
                <endpoint address="" binding="basicHttpBinding" bindingConfiguration="StreamedBinding"
                    contract="ImagingService.IImageService">
                </endpoint>
            </service>
        </services>
    </system.serviceModel>
</configuration>

This makes the WCF service not yet run in the context of SharePoint. If you want to achieve this, you have to make additional changes to the binding in this config file (and to the config file of the client) but unfortunately Silverlight cannot work with that type of binding. Additional information on how to make WCF services run in the SharePoint context can be found here: http://blah.winsmarts.com/2008-9-Getting_SPContextCurrent_in_a_SharePoint_2007_WCF_Service.aspx.

Step 11 – Deploy the custom WCF service

A WCF service can be deployed to the 12\ISAPI folder or sub folder of SharePoint. The app.config file has been renamed to web.config and moved to the 12\ISAPI\BESUG folder.

To be able to host the WCF service, you have to add a .svc file to the service project. This file is similar to the .asmx file that is used for web services. An Assembly directive points to the strongly named assembly and a ServiceHost directive points to the ImageService. The ImageService.svc file looks as follows:

<%@ Assembly Name="ImagingService, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b500b1fc8d627491"%>
<%@ ServiceHost Service="ImagingService.ImageService" %>

The solution manifest.xml contains directives on where to deploy the WCF service and its assembly:

<Solution
   SolutionId="{0F22171D-2328-49a9-A0E2-FD1F79E279A7}"
   xmlns="http://schemas.microsoft.com/sharepoint/">
  <RootFiles>
    <RootFile Location="ISAPI\BESUG\ImageService.svc"/>
    <RootFile Location="ISAPI\BESUG\web.config"/>
  </RootFiles>
  <Assemblies>
    <Assembly DeploymentTarget="GlobalAssemblyCache"   Location="ImagingService.dll" />
  </Assemblies>
</Solution>

The project structure looks as follows:

There is an install.bat file with the project that you can use to deploy the WCF service to SharePoint.

Step 12 – Deploy a Virtual Path Provider

After having dropped the assembly in the GAC and having deployed the ImageService.svc file to a sub directory of the ISAPI folder, you can try to browse to the WCF service but you will get an ugly virtualPath error.

This is because WCF services are using internally the character “~” in the path of the service and the out of the box SPVirtaulPathProvider is not able to handle these characters in the address of a resource. The SPVirtaulPathProvider is responsible for making work the content database paths and physical paths together.  You will need to create your own VirtualPathProvider and replace all the “~” characters. VirtualPathProviders work in a chained form so you can chain yours after the SPVirtualPathProvider.

You can develop the custom VirtualPathProvider in a separate class library. The VirtualPathProvider class must inherit from the VirtualPathProvider base class which is located in the System.Web.Hosting namespace in the System.Web.dll.

The methods to override are the CombineVirtualPaths and the FileExists method.

public class WCFVirtualPathProvider : VirtualPathProvider
{
    public override string CombineVirtualPaths(string basePath,
         string relativePath)
    {
         return Previous.CombineVirtualPaths(basePath, relativePath);
    }

    // all other methods omited, they simply call Previous...
    // like the above.
    public override bool FileExists(string virtualPath)
    {
         string fixedVirtualPath = virtualPath;
         if (virtualPath.StartsWith("~")
            && virtualPath.EndsWith(".svc"))
         {
             fixedVirtualPath = virtualPath.Remove(0, 1);
         }
         return Previous.FileExists(fixedVirtualPath);
    }
}

The custom VirtualPathProvider needs to be registered with SharePoint. This can be achieved by creating a custom IHttpModule. You can add an extra class to the class library. This class must inherit from the IHttpModule interface. This interface is located in the System.Web namespace of the System.Web.dll. Register the VirtualPathProvider in the Init method.

public class WCFVirtualPathProviderRegistrationModule: IHttpModule
{
    static bool wcfProviderInitialized = false;
    static object locker = new object();
    public void Init(HttpApplication context)
    {
       if (!wcfProviderInitialized)
       {
            lock (locker)
            {
                if (!wcfProviderInitialized)
                {
                    WCFVirtualPathProvider pathProvider = new WCFVirtualPathProvider();                       
                    HostingEnvironment.RegisterVirtualPathProvider(pathProvider);
                    wcfProviderInitialized = true;
                }
            }
        }
    }
    public void Dispose()
    {
    }
}

This assembly must be dropped in the Global Assembly Cache and in the web.config of your SharePoint web application an extra HttpModule element must be added to the HttpModules section of the web.config.

<add name="WCFVirtualPathProviderRegistrationModule" type="WCFLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b500b1fc8d627491"/>

But best practice is that you create a solution and features that deploy the VirtualPathProvider to the GAC and register the HttpModule in the web.config. Therefore you can create a separate project that defines the features and has a feature receiver class that registers the HttpModule using the SPWebConfigModification class.

public class FeatureReceiver : SPFeatureReceiver
{
    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
        SPWebApplication webApplication = (SPWebApplication)properties.Feature.Parent;
        webApplication.WebConfigModifications.Add(CreateHttpModuleModification());
        webApplication.WebService.ApplyWebConfigModifications();
        webApplication.WebService.Update();
    }
    public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
    {
        SPWebApplication webApplication = (SPWebApplication)properties.Feature.Parent;
        webApplication.WebConfigModifications.Remove(CreateHttpModuleModification());
        webApplication.WebService.ApplyWebConfigModifications();
        webApplication.WebService.Update();
    }
    public override void FeatureInstalled(SPFeatureReceiverProperties properties)
    {
        throw new NotImplementedException();
    }
    public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
    {
        throw new NotImplementedException();
    }
    public SPWebConfigModification CreateHttpModuleModification()
    {
        SPWebConfigModification modification;
        string ModName = "add[@name='WCFVirtualPathProviderRegistrationModule']";
        string ModXPath = "configuration/system.web/httpModules";
        modification = new SPWebConfigModification(ModName, ModXPath);
        modification.Owner = "BESUG";
        modification.Sequence = 0;
        modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
        modification.Value = string.Format(@"<add name=""{0}"" type=""{1}, {2}"" />",
              "WCFVirtualPathProviderRegistrationModule",
              "WCFLibrary.WCFVirtualPathProviderRegistrationModule",
              "WCFLibrary.WCFVirtualPathProviderRegistrationModule, WCFLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b500b1fc8d627491");
        return modification;
    }
}

The feature.xml looks as follows:

<Feature Id="{2D4DD622-B4F1-46ae-8E7F-B600582A60B0}"
     Title="WCF HttpModule Feature"
     Description="This feature registers the custom HttpModule for the VirtualPathProvider in the web.config."
     Version="1.0.0.0"
     Scope="WebApplication"
     Hidden="FALSE"
     ImageUrl="BESUG\Besug.png"
     ReceiverAssembly="WCFFeatureReceiver, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b500b1fc8d627491"
     ReceiverClass="WCF.FeatureReceiver"
     xmlns="http://schemas.microsoft.com/sharepoint/">
</Feature>

If you are going to use the WCF service regularly in combination with SharePoint, you can better deploy this feature as a Web Application feature.

The solution manifest.xml contains the following XML:

<Solution
  SolutionId="{65D53466-372C-4a23-8BEA-CD7EB9BE00D3}"
  xmlns="http://schemas.microsoft.com/sharepoint/">
  <FeatureManifests>
    <FeatureManifest Location="WCF HttpModule\feature.xml" />
  </FeatureManifests>
  <Assemblies>
    <Assembly DeploymentTarget="GlobalAssemblyCache"   Location="WCFLibrary.dll" />
    <Assembly DeploymentTarget="GlobalAssemblyCache"   Location="WCFFeatureReceiver.dll" />
  </Assemblies>
  <TemplateFiles>
    <TemplateFile Location="IMAGES\MSDN\SLMeetsSP.png"/>
  </TemplateFiles>
</Solution>

There is an install.bat file with the project that you can use to deploy the sample WCF Virtual Provider to SharePoint. Go to the Central Administration -> Application Management -> Manage Web Application Features to activate the feature.

Step 13 – Deploy and Test the custom list definition

The custom list definition is deployed using a feature. The sample project comes with an install.bat file to automate the building of the package and the solution deployment.

Return to the home page of your SharePoint site and select a job opportunity of your choice from the Job Opportunity web part from walkthrough 2. The New form of the SL Job Application list opens rendering the Silverlight application. Fill out the application form and don’t forget to rate your skills. 🙂 Try also to upload your picture.

Click the Save button to save your application.

All Job applications are saved in the SL Job Applications list:

And the pictures are saved in the Applicants Picture Library.

PS. When testing it is possible you have to enable anonymous access to the WCF service from within Internet Information Services (IIS).

If you arrived here, than I can only say “Thank you very much for your interest!”.

You can download the sample code here.

Advertisements

9 Comments »

  1. Excellent & valuable approach, I was in the process of performing exactly the same thing, it really came handy!

    Thank you,
    C. Marius

    Comment by C. Marius | February 22, 2010 | Reply

  2. Wonderful article! Please keep publishing, information value of your articles is very high.

    Thank you,
    Michal Brndiar

    Comment by Michal Brndiar | May 25, 2010 | Reply

  3. Hi,

    Excellent artical !

    I am working on creating Custom Column for sharepoint list, this column will show the attachment preview, when user click the image it should open in big size using SilverLight page.

    I am stuck at calling the silverlight application on image click so that it looks like java script popup with good GUI.

    Can you please help me how we can accomplish this?

    I tried to use the approch specified in above artical however it is now workin.

    Comment by Vishal | August 20, 2010 | Reply

  4. Vishal,
    Perhaps the Popup control from the Silverlight SDK can help you. You can make a separate user control that contains the popup control. When the user clicks the small image, the popup control can be shown.
    More info on this control here: http://blogs.msdn.com/b/silverlight_sdk/archive/2008/04/18/playing-with-the-popup-control.aspx
    Karine

    Comment by Karine Bosch | August 20, 2010 | Reply

    • Hi Karine,

      Thanks for reply, the setuation i am talking about is different.
      I have Custom Field in SP List which will render the image,
      Till here everything is SharEPoint.

      I want to call SilverLight application on image click.
      Which will look like popup on the Sharepoint site.
      I am just confused that where should I host the silverlight application.

      Comment by Vishal | August 23, 2010 | Reply

  5. Vishal,
    Silverlight application can also be easily rendered in custom fields. If you want, you can send me your code to karinebosch at hotmail dot com and I’ll check for you where you can best render the SL application.
    Karine

    Comment by Karine Bosch | August 23, 2010 | Reply

    • Hi Karine,

      Thanks for your help, finally I got the solution and now I am hosting the silverlight application in DIV which is hidden, i am making it visiable on button click. After some special effect it looks like popup window on SharePoint Page. however I have some other issue as below:
      1. I want to make this popup (DIV) model.
      2. I want to control rendring of custom field in All Item.aspx same like New Item.aspx, Edit.aspx.

      Please let me know your thoughts.
      Thanks,
      Vishal.

      Comment by Vishal | August 31, 2010 | Reply

  6. Hi Karine

    I was wondering if you could take a look at the current problem i have at this thread? Been stuck for ages. Much appreciated 🙂

    http://social.technet.microsoft.com/Forums/en-US/sharepointgeneral/thread/fad71baa-9bce-479a-8f8e-37cf8fcfd8e0/

    Comment by mike | July 15, 2011 | Reply

  7. Great post.

    Comment by Madonna | July 25, 2013 | Reply


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

%d bloggers like this: