With Silverlight you can really light up your SharePoint user interfaces. It can be used in different SharePoint domains like web parts, application pages, navigation, content management, custom fields, editor parts, etc.
In this tutorial I’m going to explain how you can host a Silverlight 2 beta 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.
When the Web Part loads the complete list of AdventureWorks products will be loaded. In the Topic box you can enter a part of a product number and click the Search button. Based on that search string a restricted list of products will be returned. You can select a product from the list to view its details.
You can also modify its list price if you want and update the AdventureWorks products list. This will again use the HttpWebRequest technique to call the UpdateListItems method of the Lists.asmx web service.
The Silverlight application will be deployed as an embedded resource of the SharePoint Web Part.
You can download the source code of this tutorial from here. The zip contains the sources for the SharePoint Web Part and the Silverlight application, together with two SharePoint list templates for the AdventureWorks Products and the AdventureWorks Product Pictures.
I created the sample Web Part using the Visual Studio 2008 extenstions for WSS 3.0. It creates the necessary infrastructure for the deployment afterwards.
Before you can start coding you need to add a reference to the System.Web.Silverlight.dll (version 2.0.5.0) and to the System.Web.Extensions.dll (version 3.5.0.0).
Open the AdventureWorksProducts.cs file in Code View and declare a class level variable for the silverlight control that will host the silverlight application:
System.Web.UI.SilverlightControls.Silverlight silverlightControl = null;
Override the OnLoad method in which you need to check whether a script manager already exists on the page. If not, you have to add one at the first position of the Controls collection:
protected override void OnLoad(EventArgs e) { base.OnLoad(e); // Script manager instance may appear only once on a page ScriptManager scriptManager = ScriptManager.GetCurrent(this.Page); if (scriptManager == null) { scriptManager = new ScriptManager(); this.Controls.AddAt(0, scriptManager); } }
Within the CreateChildControls method that is already prepared for you, you have to instantiate the Silverlight control and set its properties. One of these properties is the Source property. The setting depends on where you are going to deploy your silverlight application. In this sample I will deploy the Silverlight application as an embedded resource of my SharePoint Web Part so the Source property has to point to that location. I will come back to this property once the Silverlight application is created. Don’t forget to specify a Width and Height, otherwise the Silverlight application will not be visible. Another property that is worth mentioning is the InitParameters property. I use this property to pass the URL to the SharePoint site and the name from the list that needs to be queried. The retrieval of the data, and thus the communication with SharePoint, will be done from within the Silverlight application. Notice that this property is a string. You can pass data in it by respecting the syntax name1=value1,name2=value2,… Within the Silverlight application this data will be unpacked in a dictionary. But more on this in next section.
Don’t forget to add the Silverlight control to the Controls collection of the Web Part.
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.30523"; silverlightControl.Width = new Unit(750); silverlightControl.Height = new Unit(600); silverlightControl.Source = null; // parameters passed are the the URL of the SharePoint site and the // name of the list that contains the AdventureWorks products silverlightControl.InitParameters = "siteurl=" + SPContext.Current.Web.Url + ",listname=AdventureWorks Products"; this.Controls.Add(silverlightControl); }
Now override the RenderContents method to render the Silverlight control:
protected override void RenderContents(HtmlTextWriter writer) { if (silverlightControl != null) silverlightControl.RenderControl(writer); }
That’s all for the Web Part code.
The development of the Silverlight application is a bit harder, certainly if you are a normal ASP.NET or SharePoint developer. Add a new project to the solution using the Silverlight Application template. This template comes with a standard Page.xaml and an App.xaml. The App.xaml file inherits from the System.Windows.Application class which represents the Silverlight application while the Page represents a Silverlight control.
Before designing the Silverlight controls you will have to retrieve the parameters that have been passed by the SharePoint Web Part. Open the App.xaml.cs file and locate the Application_Startup method. The second incoming argument is of type StartUpEventArgs and contains in its InitParams property the data you passed in via the Web Part. At this side of the Silverlight application it is a dictionary of key-value pairs.
To be able to use this data from within the Silverlight application you have to pass the data to constructor of the Page control.
private void Application_Startup(object sender, StartupEventArgs e) { string siteUrl = null; string listName = null; if (e.InitParams != null && e.InitParams.Count == 2) { siteUrl = e.InitParams["siteurl"]; listName = e.InitParams["listname"]; } // Load the main control this.RootVisual = new Page(siteUrl, listName); }
Open the Page.xaml.cs code behind file and modify the constructor:
public Page(string siteUrl, string listName) { }
The Silverlight application also contains a class that defines the Product object. The different text boxes in the Silverlight controls will be bound to the properties of this type of objects. The class inherits from INotifyPropertyChanged, to enable the object to be notified when the data changes. This interface requires only one thing: that the class has an event of type PropertyChangedEventHandler, named PropertyChanged. This event is fired when any property that is tied to a UI control is changed. In this case the Product object will be notified when the user tries to update the product price.
public class Product : INotifyPropertyChanged { public int Id { get; set; } public int ProductId { get; set; } public string ProductNumber { get; set; } public string ProductName { get; set; } public string Description { get; set; } private double listPrice; public double ListPrice { get { return listPrice; } set { listPrice = value; OnPropertyChanged("ListPrice"); } } public string Color { get; set; } public string Size { get; set; } public string Weight { get; set; } public string ThumbNail { get; set; } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string name) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(name)); } #endregion }
Now it is time to design the Silverlight controls.
The Silverlight application consists of 3 controls: the main control that displays a search box with a button and a ListBox for displaying the list of AdventureWorks products. The second control displays the details of the product selected from the list. In the details control you will also be able to update the product price and save your changes to the SharePoint site. The third control is a rotator control that is only shown when data is retrieved from the SharePoint web service with the purpose to show the user that the application is busy.
The XAML for the main Silverlight control is not much:
<UserControl x:Class="SL.XAML.AdventureWorksProducts.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:SL.XAML.AdventureWorksProducts"
Width="750" Height="600">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Style="{StaticResource Header}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Border Style="{StaticResource TitleBorder}">
<TextBlock Text="AdventureWorks Product Search"
Style="{StaticResource TitleText}" />
</Border>
<TextBox x:Name="SearchTextBox" Text="Topic..."
FontFamily="Trebuchet MS" Grid.Column="1"
FontSize="12" Padding="1,3,1,1"/>
<Button x:Name="SearchButton"
Content="Search"
Click="SearchButton_Click"
Style="{StaticResource SearchButton}" />
</Grid>
<ListBox x:Name="ProductsList" Style="{StaticResource ProductsList}"
SelectionChanged="ProductsList_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<!-- Product Thumbnail -->
<Image Source="{Binding ThumbNail}"
Style="{StaticResource ThumbNailPreview}" />
<!-- Product Number-->
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding ProductNumber}"
Margin="5"
Style="{StaticResource TitleBlock}"
FontWeight="Bold" />
<TextBlock Text="{Binding ProductName}"
Margin="5"
Style="{StaticResource TitleBlock}" />
</StackPanel>
<!-- List Price-->
<TextBlock Text="{Binding ListPrice, Mode=TwoWay}"
Margin="5"
Style="{StaticResource PriceBlock}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock x:Name="MessageTextBlock" Grid.RowSpan="2"
Visibility="Collapsed" Text="No items found" />
<my:ProductDetails x:Name="DetailsView" Grid.RowSpan="2"
Visibility="Collapsed" />
<my:Rotator x:Name="LoadRotator" Grid.RowSpan="2"
Visibility="Collapsed" />
</Grid>
</UserControl>
The main grid contains two rows: one for the search text box and button, and one for the products list box.
The first row of the main grid contains a second grid with a border and 3 columns: one for the Title, one for the search text and one for the search button. It applies a style that is defined as resource in the App.xaml file defining the margin:
<Style x:Key="Header" TargetType="Grid">
<Setter Property="Margin" Value="7" />
<Setter Property="Grid.Row" Value="0"/>
</Style>
Also the border, the title and the button apply a style that is defined as resource in the App.xaml. The button has an event handler attached to it, which will be explained later in the article.
The second row of the main grid contains the list box for the AdventureWorks products. The list box contains a number of stack panels for the layout of the product data. Besides styles the individual text boxes also bind to properties of the product object. For the list price text block, the binding mode is set to TwoWay.
At the end of the Page control you see the XAML for the ProductDetails control. Initially this control is hidden. It is only when the user selects a product that this controls is populated with the selected product and that the controls is made visible.
The rotator control will only be shown when data is retrieved from the SharePoint web service.
The ProductDetails control contains a main grid that contains a rectangle and border element defining the background. The controls displaying the details of the selected product are drawn within grids and stack panels for a good positioning on the control. Most of the controls apply styles defined in the App.xaml. The text blocks displaying the data are bound to a property of the product object. The XAML for the control looks as follows:
<UserControl x:Class="SL.XAML.AdventureWorksProducts.ProductDetails"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="600" Height="250">
<Grid>
<Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Opacity="0.765" Fill="#FF8A8A8A" />
<Border CornerRadius="20" Background="#FF5C7590" Width="600" Height="250">
<StackPanel Margin="5, 7, 0, 5">
<!-- Top Right Close Button -->
<Button Content="Close" Style="{StaticResource CloseButton}" Click="Button_Click"/>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="150"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Left: Large Picture -->
<Image Source="{Binding ThumbNail}" Style="{StaticResource DetailsThumbNailPreview}"/>
<!-- Center: Product Details -->
<StackPanel Orientation="Vertical" Grid.Column="1" >
<TextBlock Text="Product number:" Style="{StaticResource DetailBlock}" FontWeight="Bold" Height="30"/>
<TextBlock Text="Product name:" Style="{StaticResource DetailBlock}" />
<TextBlock Text="Color:" Style="{StaticResource DetailBlock}" />
<TextBlock Text="Size:" Style="{StaticResource DetailBlock}" />
<TextBlock Text="Weight:" Style="{StaticResource DetailBlock}" />
<TextBlock Text="List price in $:" Style="{StaticResource DetailBlock}" Height="30" />
<TextBlock Text="Description:" Style="{StaticResource DetailBlock}" />
</StackPanel>
<StackPanel Orientation="Vertical" Grid.Column="2" >
<TextBlock Text="{Binding ProductNumber}" Style="{StaticResource DetailBlock}" FontWeight="Bold" Height="30"/>
<TextBlock Text="{Binding ProductName}" Style="{StaticResource DetailBlock}" />
<TextBlock Text="{Binding Color}" Style="{StaticResource DetailBlock}" />
<TextBlock Text="{Binding Size}" Style="{StaticResource DetailBlock}" />
<TextBlock Text="{Binding Weight}" Style="{StaticResource DetailBlock}" />
<TextBlock x:Name="ListPriceTextBlock" Text="{Binding ListPrice, Mode=TwoWay }" Style="{StaticResource DetailBlock}" Height="30" />
<TextBox x:Name="ListPriceTextBox" Text="" Width="200" Visibility="Collapsed" HorizontalAlignment="Left" />
</StackPanel>
<TextBlock Text="{Binding Description}" Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="1"
Style="{StaticResource DetailBlock}" FontStyle="Italic" />
</Grid>
<Button x:Name="EditButton" Width="80" Content="Edit Price" HorizontalAlignment="Right" Margin="20" Click="EditButton_Click"/>
</StackPanel>
</Border>
</Grid>
</UserControl>
Now to the code behind of both Silverlight controls. When the Silverlight application loads, all products of the AdventureWorks Products list are retrieved using the HttpWebRequest for calling the GetListItems method of the Lists.asmx web service.
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri(siteUrl + "/_vti_bin/Lists.asmx", UriKind.Absolute)); request.Method = "POST"; request.ContentType = "application/soap+xml; charset=utf-8"; request.Headers["ClientType"] = "Silverlight"; request.BeginGetRequestStream(new AsyncCallback(RequestCallback), request);
The call to the Request is asynchronous an thus needs a callback method. In that callback method the soap envelope is build. If you don’t know the correct envelope of a web service method you have to browse to that web service and check the method of your choice. You will get a page with the necessary information of the envelope.
The envelope contains the necessary XML to execute the GetListItems method on the AdventureWorks Products list passed in via the Web Part. If the user entered a string in the Search text box a CAML query is build to retrieve only the products containing that string. The body of the request is retrieved by calling the EndGetRequestStream method of the HttpWebRequest object. Then the envelope is written to the body and the request is send back to the server.
Also the Response is asynchronous so you need to define another callback method for HttpWebRequest when coming back from the server.
The envelope is constructed as follows:
private void RequestCallback(IAsyncResult asyncResult) { try { string envelope = @"see image"; string query = string.Empty; if (!string.IsNullOrEmpty(searchstring) && searchstring != "Topic...") { query = "{0}"; query = string.Format(query, searchstring); } envelope = string.Format(envelope, listName, query); HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState; body = request.EndGetRequestStream(asyncResult); System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding(); byte[] formBytes = encoding.GetBytes(envelope); body.Write(formBytes, 0, formBytes.Length); body.Close(); request.BeginGetResponse(new AsyncCallback(ResponseCallback), request); } catch (WebException ex) { string s = ex.Message; } }
Both the Request and the Response are started asynchronously on another thread. When the response comes back from the server, it is not on the worker thread. So the data is stored in a class level variable and the ProcessResponse method on the worker thread is called.
private void ResponseCallback(IAsyncResult asyncResult) { LoadRotator.Visibility = Visibility.Collapsed; HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState; HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult); if (response.StatusCode == HttpStatusCode.OK) { products = new ObservableCollection(); Stream content = response.GetResponseStream(); using (StreamReader reader = new StreamReader(content)) { responsestring = reader.ReadToEnd(); } try { this.Dispatcher.BeginInvoke(ProcessResponse); } catch { } } else { MessageTextBlock.Visibility = Visibility.Visible; } }
The ProcessResponse method parses the XML into a collection of Product objects using LINQ to XML. For this purpose a reference to the System.Xml.Linq assembly is added. All products are added to the Products collection and then assigned to the ItemSource property of the products list box in order to enable the data binding.
private void ProcessResponse() { LoadRotator.Visibility = Visibility.Collapsed; products = new ObservableCollection(); XDocument results = XDocument.Parse(responsestring); var query = from item in results.Descendants(XName.Get("row", "#RowsetSchema")) select new Product() { Id = System.Convert.ToInt32(item.Attribute("ows_ID").Value, CultureInfo.InvariantCulture), ProductNumber = item.Attribute("ows_Title").Value + "_", ProductName = item.Attribute("ows_ProductName").Value, ListPrice = System.Convert.ToDouble(item.Attribute("ows_ListPrice").Value, CultureInfo.InvariantCulture), Description = item.Attribute("ows_Description") != null ? item.Attribute("ows_Description").Value : string.Empty, Color = item.Attribute("ows_Color") != null ? item.Attribute("ows_Color").Value : string.Empty, Size = item.Attribute("ows_Size") != null ? item.Attribute("ows_Size").Value : string.Empty, Weight = item.Attribute("ows_Weight") != null ? item.Attribute("ows_Weight").Value : string.Empty, ThumbNail = GetThumbnailUrl(item.Attribute("ows_Thumbnail")) }; foreach (Product p in query.ToList()) { products.Add(p); } ProductsList.ItemsSource = products; }
When the user selects a product in the list, a box containing the detailed information is shown to the user. This is triggered by the SelectionChanged event of the list box. The selected product is retrieved from the list box and assigned to the ProductDetails control, which is then made visible.
private void ProductsList_SelectionChanged(object sender, SelectionChangedEventArgs e) { Product product = (Product)ProductsList.SelectedItem; if (product != null) { DetailsView.DataContext = product; DetailsView.SiteUrl = siteUrl; DetailsView.ListName = listName; DetailsView.Visibility = Visibility.Visible; } }
This brings us to the code behind of the ProductDetails control. The control is populated by the main control. This control contains also an Edit Price button. When the user clicks this button, the list price text block is changed into a text box in which the user can enter the new price. When the user then clicks the Update button, the UpdateListItems method of the Lists.asmx web service is called to save the new list price. The update is also performed by using HttpWebRequest, which runs asynchronously on different threads.
The envelope for the UpdateListItems method looks like follows:
private void UpdateListPrice() { HttpWebRequest request = (HttpWebRequest)WebRequest.Create( new Uri(siteUrl + "/_vti_bin/Lists.asmx", UriKind.Absolute)); request.Method = "POST"; request.ContentType = "application/soap+xml; charset=utf-8"; request.BeginGetRequestStream(new AsyncCallback(RequestCallback), request); } private void RequestCallback(IAsyncResult asyncResult) { string envelope = @"see image"; string query = string.Empty; query = @"{0}{1}"; query = string.Format(query, selectedProduct.Id, newListPriceString); envelope = string.Format(envelope, listName, query); HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState; body = request.EndGetRequestStream(asyncResult); System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding(); byte[] formBytes = encoding.GetBytes(envelope); body.Write(formBytes, 0, formBytes.Length); body.Close(); request.BeginGetResponse(new AsyncCallback(ResponseCallback), request); } private void ResponseCallback(IAsyncResult asyncResult) { HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState; HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult); Stream content = response.GetResponseStream(); if (response.StatusCode == HttpStatusCode.OK) { try { this.Dispatcher.BeginInvoke(ProcessResponse); } catch { } } } private void ProcessResponse() { selectedProduct.ListPrice = newListPrice; }
Because of the TwoWay binding mode set on the ListPriceTextBlock on the list box, the price change is propagated to the item in the list box during the execution of the ProcessResponse method.
Build the Silverlight application and copy the resulting .xap file to your SharePoint project. I copied it to a Resources sub directory.
Now that it comes to the deployment of the SharePoint web part and its Silverlight application, you have to make some modifications to the Web Part files that contain the metadata for the Web Part.
Set the properties of the embedded silverlight resource. Include the Resources folder and the .xap file to the Web Part project. In the file properties set the Build action to Embedded Resource.
Build the SharePoint project and open the DLL with a tool like Reflector in order to copy the right namespace of the embedded resource which is the silverlight application. Return to the Web Part project and open the Web Part class. Before the namespace declaration you have to add the metadata attribute that enables an embedded resource in an assembly:
[assembly: WebResource("SL.AdventureWorksProducts.Resources.SL.XAML.AdventureWorksProducts.xap", "application/x-silverlight-app")] namespace SL.AdventureWorksProducts { … Web Part code … }
In the CreateChildControls method set the source property of the silverlight control to the location of the silverlight application. You can retrieve this location by using the GetWebResourceUrl method:
silverlightControl.Source = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), "SL.AdventureWorksProducts.Resources.SL.XAML.AdventureWorksProducts.xap");
If needed you can modify the .webpart file and the .xml file to set certain Web Part properties.
When working with the Visual Studio 2008 extensions for WSS you can set the Start browser with URL property of the web part project to the URL of your SharePoint site. This will deploy the Web Part to this URL.
When deployed you can add the Web Part to a web part page. Don’t forget to configure your SharePoint server for enabling Silverlight! If you don’t know how to do that, you can always check the document - which is part of the Silverlight BluePrint for SharePoint - included with this article.
If you want to learn more about Silverlight integration in SharePoint you can download the Silverlight BluePrint for Sharepoint at http://codeplex.com/SL4SP.
Ten years ago I started my career as Visual Basic developer but as soon as .NET came out I started developing in VB.NET and C#. In 2002 I started working for a Belgian company U2U, specialized in .NET and SharePoint training. I did some consultancy work on SharePoint 2003 but once SharePoint 2007 released I started working full time on WSS and MOSS.
Contact the author | Other Posts by Karine Bosch (3) | Author's Website