WEBSITE

Consuming a WCF Service with Silverlight

9/14/2010 4:24:12 PM

1. Problem

Your Silverlight application needs to communicate with a WCF service.

2. Solution

Add a reference to the WCF service to your Silverlight application, and use the generated proxy classes to invoke the service.

3. How It Works

From the context menu of the Silverlight project in Solution Explorer, select Add Service Reference. This brings up the dialog shown in Figure 1.

Figure 1. Visual Studio 2008 Add Service Reference dialog

You have the option of entering the URL of the service endpoint or, if the service project is part of your solution, of clicking Discover to list those services. After the service(s) are listed, select the appropriate service and click OK to add a reference to the service to your application, which generates a set of proxy classes. You can change the namespace in which the generated proxy lives by changing the default namespace specified by the dialog.

Additionally, you have the option to further customize the generated proxy by clicking the Advanced button. This brings up the dialog shown in Figure 2, where you can specify, among other options, the collection types to be used by the proxy to express data collections and dictionaries being exchanged with the service.

Figure 2. Visual Studio 2008 Service Reference Settings dialog

To display the generated proxy files, select the proxy node under the Service References node in your project tree, and then click the Show All Files button on the top toolbar on the Visual Studio Solution Explorer. The proxy node has the same name as the service for which you generated the proxy. You can find the generated proxy code in the Reference.cs file under the Reference.svcmap node in the project, as shown in Figure 3.

Figure 3. Visual Studio 2008 generated service proxy

3.1. Invoking a Service Operation

Assuming a service named ProductManager exists, Reference.cs contains a client proxy class for the service named ProductManagerClient. It also contains the data-contract types exposed by the service.

Silverlight uses an asynchronous invoke pattern—all web-service invocations are offloaded to a background thread from the local thread pool, and control is returned instantly to the executing Silverlight code. The proxy-generation mechanism implements this by exposing an xxxAsync() method and xxxCompleted event pair on the client proxy, where xxx is an operation on the service. To invoke the service operation from your Silverlight code, you execute the xxxAsync() method and handle the xxxCompleted event, in which you can extract the results returned by the service call from the event arguments. Note that although the service-invocation code executes on a background thread, the framework switches context to the main UI thread before invoking the completion event handler so that you do not have to worry about thread safety in your implementation of the handler.

Listing 1 shows such a pair from the generated proxy code for a service operation named GetProductHeaders.

Listing 1. Generated proxy code for a service operation
public event System.EventHandler<GetProductHeadersCompletedEventArgs>
GetProductHeadersCompleted;

public partial class GetProductHeadersCompletedEventArgs :
System.ComponentModel.AsyncCompletedEventArgs
{

private object[] results;

public GetProductHeadersCompletedEventArgs(object[] results,
Exception exception, bool cancelled, object userState) :
base(exception, cancelled, userState)
{
this.results = results;
}

public List<ProductHeader> Result
{
get
{
base.RaiseExceptionIfNecessary();
return ((System.Collections.Generic.List<ProductHeader>)(this.results[0]));
}
}
}

public void GetProductDetailAsync(ushort ProductId) {
this.GetProductDetailAsync(ProductId, null);
}


Note the custom event argument type named GetProductHeadersCompletedEventArgs in Listing 1. Silverlight creates one of these for every unique operation in your service. Each exposes a Result property as shown, which is strongly typed (in this case, to a List<ProductHeader>) to help you avoid any casting or conversion in retrieving the result of the service call.

3.2. Configuring a WCF Service for Silverlight

As indicated in the introduction to this chapter, Silverlight requires a SOAP/HTTP-based service to be WS-I Basic Profile 1.1 compliant. In WCF terms, that means using BasicHttpBinding on the service endpoint. Listing 2 shows a sample configuration section of a service that uses BasicHttpBinding.

Listing 2. WCF Service Configuration in Web.config
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="LargeMessage_basicHttpBinding"
maxReceivedMessageSize="1048576" />
</basicHttpBinding>
</bindings>
<serviceHostingEnvironment aspNetCompatibilityEnabled="True"/>
<services>

<service behaviorConfiguration="ServiceBehavior"
name="Recipe7_1.ProductsDataSoapService.ProductManager">
<endpoint address="" binding="basicHttpBinding"
bindingConfiguration="LargeMessage_basicHttpBinding"
contract=
"Recipe7_1.ProductsDataSoapService.IProductManager" />
<endpoint address="mex" binding="mexHttpBinding"
contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="ServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>

The additional endpoint utilizing mexHttpBinding is required to expose service-contract metadata, which is needed for the client proxy generation described earlier in this section. Note the setting of the maxReceivedMessageSize property on the binding to about 1 MB (defined in bytes). This increases it from its default value of about 65 KB, because you anticipate the messages in the code sample to be larger than that limit.

Also note that similar configuration settings are needed on the Silverlight client to initialize the proxy and consume the service. When you generate the proxy, this is automatically generated for you and stored in a file called ServiceReferences.ClientConfig. This file is packaged into the resulting .xap file for your Silverlight application, and the settings are automatically read in when you instantiate a service proxy.

4. The Code

The code sample for this recipe builds a simple master-detail style UI over product inventory data exposed by a WCF service. Listing 3 shows the service contract for the WCF service.

Listing 3. Service contract for the service in ServiceContract.cs
using System.Collections.Generic;
using System.ServiceModel;

namespace Recipe7_1.ProductsDataSoapService
{
[ServiceContract]
public interface IProductManager
{
[OperationContract]

List<ProductHeader> GetProductHeaders();
[OperationContract]
void UpdateProductHeaders(List<ProductHeader> Updates);

[OperationContract]
ProductDetail GetProductDetail(ushort ProductId);

[OperationContract]
void UpdateProductDetail(ProductDetail Update);
}
}

A service contract models the external interface that the service exposes to the world. You can represent the service contract in a common language runtime (CLR) programming language of your choice (C# in this case). The contract itself is an interface, with the operations defined as method signatures in the interface. The attribution of the interface with ServiceContractAttribute, and that of the operations with OperationContractAttribute, indicates to the WCF runtime that this interface is representative of a service contract. When you try to generate a proxy (or model it by hand) using Visual Studio, the Web Service Definition Language (WSDL) that is returned by the service and used to model the proxy also maps to this service contract.

The service contract in Listing 7-3 is implemented as an interface named IProductManager, allows retrieval of a collection of all ProductHeader objects through GetProductHeaders(), and accepts batched ProductHeader changes through UpdateProductHeaders(). It also lets you retrieve ProductDetail using GetProductDetail() for a specific product, in addition to allowing updates to ProductDetail information for a product using UpdateProductDetail() in a similar fashion.

Listing 4 shows the data contracts used in the service.

Listing 4. Data contracts for the service in DataContracts.cs
namespace Recipe7_1.ProductsDataSoapService
{
[DataContract]
public partial class ProductHeader
{
private ushort? productIdField;
private decimal? listPriceField;
private string nameField;
private string sellEndDateField;
private string sellStartDateField;

[DataMember]
public ushort? ProductId
{
get { return this.productIdField; }
set { this.productIdField = value; }
}
[DataMember]

public decimal? ListPrice
{
get { return this.listPriceField; }
set { this.listPriceField = value; }
}
[DataMember]
public string Name
{
get { return this.nameField; }
set { this.nameField = value; }
}
[DataMember]
public string SellEndDate
{
get { return this.sellEndDateField; }
set { this.sellEndDateField = value; }
}
[DataMember]
public string SellStartDate
{
get { return this.sellStartDateField; }
set { this.sellStartDateField = value; }
}
}

[DataContract]
public partial class ProductDetail
{

private ushort? productIdField;
private string classField;
private string colorField;
private byte? daysToManufactureField;
private string discontinuedDateField;
private string finishedGoodsFlagField;
private string makeFlagField;
private string productLineField;
private string productNumberField;
private ushort? reorderPointField;
private ushort? safetyStockLevelField;
private string sizeField;
private decimal? standardCostField;
private string styleField;
private string weightField;


[DataMember]
public ushort? ProductId
{
get { return this.productIdField; }
set { this.productIdField = value; }
}
[DataMember]
public string Class
{
get { return this.classField; }
set { this.classField = value; }
}
[DataMember]
public string Color
{
get { return this.colorField; }
set { this.colorField = value; }
}
[DataMember]
public byte? DaysToManufacture
{
get { return this.daysToManufactureField; }
set { this.daysToManufactureField = value; }
}
[DataMember]
public string DiscontinuedDate
{
get { return this.discontinuedDateField; }
set { this.discontinuedDateField = value; }
}
[DataMember]
public string FinishedGoodsFlag
{
get { return this.finishedGoodsFlagField; }
set { this.finishedGoodsFlagField = value; }
}
[DataMember]
public string MakeFlag
{
get { return this.makeFlagField; }
set { this.makeFlagField = value; }
}
[DataMember]
public string ProductLine
{

get { return this.productLineField; }
set { this.productLineField = value; }
}
[DataMember]
public string ProductNumber
{
get { return this.productNumberField; }
set { this.productNumberField = value; }
}
[DataMember]
public ushort? ReorderPoint
{
get { return this.reorderPointField; }
set { this.reorderPointField = value; }
}
[DataMember]
public ushort? SafetyStockLevel
{
get { return this.safetyStockLevelField; }
set { this.safetyStockLevelField = value; }
}
[DataMember]
public string Size
{
get { return this.sizeField; }
set { this.sizeField = value; }
}
[DataMember]
public decimal? StandardCost
{
get { return this.standardCostField; }
set { this.standardCostField = value; }
}
[DataMember]
public string Style
{
get { return this.styleField; }
set { this.styleField = value; }
}
[DataMember]
public string Weight
{
get { return this.weightField; }
set { this.weightField = value; }
}


}
}

Any custom CLR type that you define in your application and use in your service operations needs to be explicitly known to the WCF runtime. This is so that it can be serialized to/deserialized from the wire format (SOAP/JSON, and so on) to your application code format (CLR type). To provide this information to WCF, you must designate these types as data contracts. The DataContractAttribute is applied to the type, and each property member that you may want to expose is decorated with the DataMemberAttribute. Leaving a property undecorated does not serialize it, and neither is it included in the generated proxy code.

In this case, you define data contracts for the ProductHeader and the ProductDetail types that you use in the service contract. Note that WCF inherently knows how to serialize framework types such as primitive types and collections. Therefore, you do not need specific contracts for them.

Listing 5 shows the full implementation of the service in the ProductManager class, implementing the service contract IProductManager.

Listing 5. Service implementation in ProductManager.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel.Activation;
using System.Web;
using System.Xml.Linq;

namespace Recipe7_1.ProductsDataSoapService
{
[AspNetCompatibilityRequirements(
RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
public class ProductManager : IProductManager
{
public List<ProductHeader> GetProductHeaders()
{
//open the local XML data file for products
StreamReader stmrdrProductData = new StreamReader(
new FileStream(HttpContext.Current.Request.MapPath(
"App_Data/XML/Products.xml"), FileMode.Open));
//create a Linq To XML Xdocument and load the data
XDocument xDocProducts = XDocument.Load(stmrdrProductData);
//close the stream
stmrdrProductData.Close();
//transform the XML data to a collection of ProductHeader
//using a Linq To XML query
IEnumerable<ProductHeader> ProductData =
from elemProduct in xDocProducts.Root.Elements()
select new ProductHeader


{
Name = elemProduct.Attribute("Name") != null ?
elemProduct.Attribute("Name").Value : null,
ListPrice = elemProduct.Attribute("ListPrice") != null ?
new decimal?(Convert.ToDecimal(
elemProduct.Attribute("ListPrice").Value))
: null,
ProductId = elemProduct.Attribute("ProductId") != null ?
new ushort?(Convert.ToUInt16(
elemProduct.Attribute("ProductId").Value)) :
null,
SellEndDate = elemProduct.Attribute("SellEndDate") != null ?
elemProduct.Attribute("SellEndDate").Value : null,
SellStartDate = elemProduct.Attribute("SellStartDate") != null ?
elemProduct.Attribute("SellStartDate").Value : null
};
//return a List<ProductHeader>
return ProductData.ToList();
}

public void UpdateProductHeaders(List<ProductHeader> Updates)
{
//open the local data file and load into an XDocument
StreamReader stmrdrProductData = new StreamReader(
new FileStream(HttpContext.Current.Request.MapPath(
"App_Data/XML/Products.xml"), FileMode.Open));
XDocument xDocProducts = XDocument.Load(stmrdrProductData);
stmrdrProductData.Close();
//for each of the ProductHeader instances
foreach (ProductHeader Prod in Updates)
{
//find the corresponding XElement in the loaded XDocument
XElement elemTarget =
(from elemProduct in xDocProducts.Root.Elements()
where Convert.ToUInt16(elemProduct.Attribute("ProductId").Value)
== Prod.ProductId
select elemProduct).ToList()[0];
//and updates the attributes with the changes
if (elemTarget.Attribute("Name") != null)
elemTarget.Attribute("Name").SetValue(Prod.Name);
if (elemTarget.Attribute("ListPrice") != null
&& Prod.ListPrice.HasValue)
elemTarget.Attribute("ListPrice").SetValue(Prod.ListPrice);
if (elemTarget.Attribute("SellEndDate") != null)
elemTarget.Attribute("SellEndDate").SetValue(Prod.SellEndDate);


if (elemTarget.Attribute("SellStartDate") != null)
elemTarget.Attribute("SellStartDate").SetValue(Prod.SellStartDate);
}
//save the XDocument with the changes back to the data file
StreamWriter stmwrtrProductData = new StreamWriter(
new FileStream(HttpContext.Current.Request.MapPath(
"App_Data/XML/Products.xml"), FileMode.Truncate));
xDocProducts.Save(stmwrtrProductData);
stmwrtrProductData.Close();

}

public ProductDetail GetProductDetail(ushort ProductId)
{
StreamReader stmrdrProductData = new StreamReader(
new FileStream(
HttpContext.Current.Request.MapPath("App_Data/XML/Products.xml"),
FileMode.Open));

XDocument xDocProducts = XDocument.Load(stmrdrProductData);
stmrdrProductData.Close();

IEnumerable<ProductDetail> ProductData =
from elemProduct in xDocProducts.Root.Elements()
where elemProduct.Attribute("ProductId").Value == ProductId.ToString()
select new ProductDetail
{
Class = elemProduct.Attribute("Class") != null ?
elemProduct.Attribute("Class").Value : null,
Color = elemProduct.Attribute("Color") != null ?
elemProduct.Attribute("Color").Value : null,
DaysToManufacture = elemProduct.Attribute("DaysToManufacture") != null ?
new byte?(
Convert.ToByte(elemProduct.Attribute("DaysToManufacture").Value))
: null,
DiscontinuedDate = elemProduct.Attribute("DiscontinuedDate") != null ?
elemProduct.Attribute("DiscontinuedDate").Value : null,
FinishedGoodsFlag = elemProduct.Attribute("FinishedGoodsFlag") != null ?
elemProduct.Attribute("FinishedGoodsFlag").Value : null,

MakeFlag = elemProduct.Attribute("MakeFlag") != null ?
elemProduct.Attribute("MakeFlag").Value : null,
ProductId = elemProduct.Attribute("ProductId") != null ?
new ushort?(
Convert.ToUInt16(elemProduct.Attribute("ProductId").Value))


: null,
ProductLine = elemProduct.Attribute("ProductLine") != null ?
elemProduct.Attribute("ProductLine").Value : null,
ProductNumber = elemProduct.Attribute("ProductNumber") != null ?
elemProduct.Attribute("ProductNumber").Value : null,
ReorderPoint = elemProduct.Attribute("ReorderPoint") != null ?
new ushort?(
Convert.ToUInt16(elemProduct.Attribute("ReorderPoint").Value))
: null,
SafetyStockLevel = elemProduct.Attribute("SafetyStockLevel") != null ?
new ushort?(
Convert.ToUInt16(elemProduct.Attribute("SafetyStockLevel").Value))
: null,
StandardCost = elemProduct.Attribute("StandardCost") != null ?
new decimal?(Convert.ToDecimal(
elemProduct.Attribute("StandardCost").Value))
: null,
Style = elemProduct.Attribute("Style") != null ?
elemProduct.Attribute("Style").Value : null

};

return ProductData.ToList()[0];
}
public void UpdateProductDetail(ProductDetail Update)
{
StreamReader stmrdrProductData = new StreamReader(
new FileStream(
HttpContext.Current.Request.MapPath("App_Data/XML/Products.xml"),
FileMode.Open));
XDocument xDocProducts = XDocument.Load(stmrdrProductData);
stmrdrProductData.Close();

XElement elemTarget =
(from elemProduct in xDocProducts.Root.Elements()
where Convert.ToUInt16(elemProduct.Attribute("ProductId").Value)
== Update.ProductId
select elemProduct).ToList()[0];

if (elemTarget.Attribute("Class") != null)
elemTarget.Attribute("Class").SetValue(Update.Class);
if (elemTarget.Attribute("Color") != null)
elemTarget.Attribute("Color").SetValue(Update.Color);
if (elemTarget.Attribute("DaysToManufacture") != null
&& Update.DaysToManufacture.HasValue)


elemTarget.Attribute("DaysToManufacture").
SetValue(Update.DaysToManufacture);
if (elemTarget.Attribute("DiscontinuedDate") != null)
elemTarget.Attribute("DiscontinuedDate").
SetValue(Update.DiscontinuedDate);
if (elemTarget.Attribute("FinishedGoodsFlag") != null)
elemTarget.Attribute("FinishedGoodsFlag").
SetValue(Update.FinishedGoodsFlag);
if (elemTarget.Attribute("MakeFlag") != null)
elemTarget.Attribute("MakeFlag").
SetValue(Update.MakeFlag);
if (elemTarget.Attribute("ProductLine") != null)
elemTarget.Attribute("ProductLine").
SetValue(Update.ProductLine);
if (elemTarget.Attribute("ProductNumber") != null)
elemTarget.Attribute("ProductNumber").
SetValue(Update.ProductNumber);
if (elemTarget.Attribute("ReorderPoint") != null
&& Update.ReorderPoint.HasValue)
elemTarget.Attribute("ReorderPoint").
SetValue(Update.ReorderPoint);
if (elemTarget.Attribute("SafetyStockLevel") != null
&& Update.SafetyStockLevel.HasValue)
elemTarget.Attribute("SafetyStockLevel").
SetValue(Update.SafetyStockLevel);
if (elemTarget.Attribute("StandardCost") != null
&& Update.StandardCost.HasValue)
elemTarget.Attribute("StandardCost").
SetValue(Update.StandardCost);
if (elemTarget.Attribute("Style") != null)
elemTarget.Attribute("Style").
SetValue(Update.Style);

StreamWriter stmwrtrProductData =
new StreamWriter(
new FileStream(
HttpContext.Current.Request.MapPath("App_Data/XML/Products.xml"),
FileMode.Truncate));

xDocProducts.Save(stmwrtrProductData);
stmwrtrProductData.Close();
}
}
}


We discuss the operations for handling product headers briefly. The ones to handle product details are implemented in a similar fashion and should be easy to follow.

All the data for this service is stored in a local data file named Products.xml. In the GetProductHeaders() method, you open the file and read the XML data into an XDocument instance. A LINQ query is used to navigate the XDocument and transform the XML data into a collection of ProductHeader instances. In UpdateProductHeaders(), the XElement instance corresponding to each product is updated with the changes in the ProductHeader instance, and the changes are saved to the same data file.

Note the use of the AspNetCompatibilityRequirementsAttribute setting on the service class, indicating that support to be required. In order to get to the data files on the file system, you map the incoming HTTP request to a server path in the code. And the HttpContext type that makes the current request available to you is available only if ASP.NET support is enabled this way. This setting needs the corresponding configuration setting

<serviceHostingEnvironment aspNetCompatibilityEnabled="True"/>

already shown in Listing 2.

Figure 4 shows the Silverlight application's UI, and Listing 6 lists the XAML for the page.

Figure 4. The UI consuming products data from a WCF service

Listing 6. XAML for the page in MainPage.xaml
<UserControl x:Class="Recipe7_1.ProductsDataViewer.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:DataControls=
"clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
Width="800" Height="600">

<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="50*" />
<RowDefinition Height="5*" />
<RowDefinition Height="45*" />
</Grid.RowDefinitions>
<!-- Top Data Grid -->
<DataControls:DataGrid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
x:Name="ProductHeaderDataGrid"
Grid.Row="0"
SelectionChanged="ProductHeaderDataGrid_SelectionChanged"
CurrentCellChanged="ProductHeaderDataGrid_CurrentCellChanged"
BeginningEdit="ProductHeaderDataGrid_BeginningEdit">
<DataControls:DataGrid.Columns>
<DataControls:DataGridTextColumn
Header="Id"
Binding="{Binding ProductId}" />
<DataControls:DataGridTextColumn
Header="Name"
Binding="{Binding Name, Mode=TwoWay}" />
<DataControls:DataGridTextColumn
Header="Price"
Binding="{Binding ListPrice, Mode=TwoWay}" />
<DataControls:DataGridTextColumn
Header="Available From"
Binding="{Binding SellStartDate, Mode=TwoWay}" />
<DataControls:DataGridTextColumn
Header="Available Till"
Binding="{Binding SellEndDate, Mode=TwoWay}" />
</DataControls:DataGrid.Columns>
</DataControls:DataGrid>
<!-- Butons -->
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right"


VerticalAlignment="Center" Grid.Row ="1">
<Button x:Name="Btn_SendHeaderUpdates" Content="Update Product Headers"
Width="200" Click="Click_Btn_SendHeaderUpdates" Margin="0,0,20,0"/>
<Button x:Name="Btn_SendDetailUpdates" Content="Update Product Detail"
Width="200" Click="Click_Btn_SendDetailUpdate"/>
</StackPanel>
<Rectangle Stroke="Black" StrokeThickness="4" Grid.Row="2" />
<!-- Data entry form -->
<Grid Grid.Row="2" x:Name="ProductDetailsGrid" Margin="10,10,10,10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal"
Grid.Row="0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="2,0,0,0">
<TextBlock Text="Product Details for - "
FontWeight="Bold"
TextDecorations="Underline"/>
<TextBlock Text="{Binding ProductId}"
FontWeight="Bold"
TextDecorations="Underline"/>
</StackPanel>
<TextBlock Text="Color" Grid.Row="1" Grid.Column="0"
Margin="2,2,15,2" />
<TextBlock Text="Days To Manufacture" Grid.Row="2" Grid.Column="0"
Margin="2,2,15,2" />
<TextBlock Text="Discontinued On" Grid.Row="3" Grid.Column="0"
Margin="2,2,15,2" />
<TextBlock Text="Finished Goods" Grid.Row="4" Grid.Column="0"
Margin="2,2,15,2" />


<TextBlock Text="Make Flag" Grid.Row="5" Grid.Column="0"
Margin="2,2,15,2" />
<TextBlock Text="Product Line" Grid.Row="6" Grid.Column="0"
Margin="2,2,15,2" />
<TextBlock Text="Class" Grid.Row="7" Grid.Column="0"
Margin="2,2,15,2"/>
<TextBlock Text="Reorder Point" Grid.Row="1" Grid.Column="2"
Margin="2,2,15,2" />
<TextBlock Text="Safety Stock Level" Grid.Row="2" Grid.Column="2"
Margin="2,2,15,2" />
<TextBlock Text="Size" Grid.Row="3" Grid.Column="2"
Margin="2,2,15,2" />
<TextBlock Text="Weight" Grid.Row="4" Grid.Column="2"
Margin="2,2,15,2" />
<TextBlock Text="Standard Cost" Grid.Row="5" Grid.Column="2"
Margin="2,2,15,2" />
<TextBlock Text="Style" Grid.Row="6" Grid.Column="2"
Margin="2,2,15,2" />
<TextBlock Text="Number" Grid.Row="7" Grid.Column="2"
Margin="2,2,15,2" />
<TextBox Text="{Binding Color,Mode=TwoWay}"
Grid.Row="1" Grid.Column="1" Margin="2,2,25,2" />
<TextBox Text="{Binding DaysToManufacture,Mode=TwoWay}"
Grid.Row="2" Grid.Column="1" Margin="2,2,25,2" />
<TextBox Text="{Binding DiscontinuedDate,Mode=TwoWay}"
Grid.Row="3" Grid.Column="1" Margin="2,2,25,2" />
<TextBox Text="{Binding FinishedGoodsFlag,Mode=TwoWay}"
Grid.Row="4" Grid.Column="1" Margin="2,2,25,2" />
<TextBox Text="{Binding MakeFlag,Mode=TwoWay}"
Grid.Row="5" Grid.Column="1" Margin="2,2,25,2" />
<TextBox Text="{Binding ProductLine,Mode=TwoWay}"
Grid.Row="6" Grid.Column="1" Margin="2,2,25,2" />
<TextBox Text="{Binding Class,Mode=TwoWay}"
Grid.Row="7" Grid.Column="1" Margin="2,2,25,2"/>
<TextBox Text="{Binding ReorderPoint,Mode=TwoWay}"
Grid.Row="1" Grid.Column="3" Margin="2,2,25,2" />
<TextBox Text="{Binding SafetyStockLevel,Mode=TwoWay}"
Grid.Row="2" Grid.Column="3" Margin="2,2,25,2" />
<TextBox Text="{Binding Size,Mode=TwoWay}"
Grid.Row="3" Grid.Column="3" Margin="2,2,25,2" />
<TextBox Text="{Binding Weight,Mode=TwoWay}"
Grid.Row="4" Grid.Column="3" Margin="2,2,25,2" />
<TextBox Text="{Binding StandardCost,Mode=TwoWay}"
Grid.Row="5" Grid.Column="3" Margin="2,2,25,2" />
<TextBox Text="{Binding Style,Mode=TwoWay}"


Grid.Row="6" Grid.Column="3" Margin="2,2,25,2" />
<TextBox Text="{Binding ProductNumber,Mode=TwoWay}"
Grid.Row="7" Grid.Column="3" Margin="2,2,25,2" />
</Grid>
</Grid>
</UserControl>

The preceding XAML uses a DataGrid named ProductHeaderDataGrid to display the ProductHeader properties. For each selected ProductHeader, to display the related details in a master-detail fashion, you further bind the ProductDetail properties to controls in a Grid named ProductDetailsGrid, which uses TextBlocks for labels and appropriately bound TextBoxes for property values, to create a data-entry form for the bound ProductDetail.

You also include two Buttons inside a StackPanel to provide the user with a way to submit updates to ProductHeaders or a ProductDetail.

Listing 7 shows the codebehind for the MainPage.

Listing 7. Codebehind for MainPage in MainPage.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Recipe7_1.ProductsDataViewer.ProductsDataSoapService;

namespace Recipe7_1.ProductsDataViewer
{
public partial class MainPage : UserControl
{
ProductsDataSoapService.ProductManagerClient client = null;
bool InEdit = false;
public MainPage()
{
InitializeComponent();
//create a new instance of the proxy
client = new ProductsDataSoapService.ProductManagerClient();
//add a handler for the GetProductHeadersCompleted event
client.GetProductHeadersCompleted +=
new EventHandler<GetProductHeadersCompletedEventArgs>(
client_GetProductHeadersCompleted);
//add a handler for the UpdateProductHeadersCompleted event
client.UpdateProductHeadersCompleted +=
new EventHandler<System.ComponentModel.AsyncCompletedEventArgs>(
client_UpdateProductHeadersCompleted);
//add a handler for GetProductDetailCompleted
client.GetProductDetailCompleted +=

new EventHandler<GetProductDetailCompletedEventArgs>(
client_GetProductDetailCompleted);
//invoke the GetProductHeaders() service operation
client.GetProductHeadersAsync();
}

void ProductHeaderDataGrid_SelectionChanged(object sender, EventArgs e)
{
if (ProductHeaderDataGrid.SelectedItem != null)
//invoke the GetProductDetails() service operation,
//using the ProductId of the currently selected ProductHeader
client.GetProductDetailAsync(
(ProductHeaderDataGrid.SelectedItem as
ProductsDataSoapService.ProductHeader).ProductId.Value);
}

void client_GetProductDetailCompleted(object sender,
GetProductDetailCompletedEventArgs e)
{
//set the datacontext of the containing grid
ProductDetailsGrid.DataContext = e.Result;
}
void client_UpdateProductHeadersCompleted(object sender,
System.ComponentModel.AsyncCompletedEventArgs e)
{
client.GetProductHeadersAsync();
}

void client_GetProductHeadersCompleted(object sender,
GetProductHeadersCompletedEventArgs e)
{
//bind the data of form List<ProductHeader> to the ProductHeaderDataGrid
ProductHeaderDataGrid.ItemsSource = e.Result;
}

void ProductHeaderDataGrid_CurrentCellChanged(object sender,
EventArgs e)
{
//changing the dirty flag on a cell edit for the ProductHeader data grid
if (InEdit && (sender as DataGrid).SelectedItem != null)
{
((sender as DataGrid).SelectedItem as ProductHeader).Dirty = true;
InEdit = false;
}
}

private void ProductHeaderDataGrid_BeginningEdit(object sender,
DataGridBeginningEditEventArgs e)
{
InEdit = true;
}

void Click_Btn_SendHeaderUpdates(object Sender, RoutedEventArgs e)
{
//get all the header items
List<ProductHeader> AllItems =
ProductHeaderDataGrid.ItemsSource as List<ProductHeader>;
//use LINQ to filter out the ones with their dirty flag set to true
List<ProductHeader> UpdateList =
new List<ProductHeader>
(
from Prod in AllItems
where Prod.Dirty == true
select Prod
);
//send in the updates
client.UpdateProductHeadersAsync(UpdateList);
}

void Click_Btn_SendDetailUpdate(object Sender, RoutedEventArgs e)
{
//send the ProductDetail update
client.UpdateProductDetailAsync(ProductDetailsGrid.DataContext as
ProductsDataSoapService.ProductDetail);
}


}
}


To fetch and bind the initial ProductHeader data, in the constructor of the MainPage, you create an instance of the ProductService.ProductManagerClient type, which is the proxy class created by adding the service reference to the Silverlight project. You then invoke the GetProductHeaders() operation on the service. Handle the GetProductHeadersCompleted event, and, in it, bind the data to the DataGrid. The data is made available to you in the Results property of the GetProductHeadersCompletedEventArgs type.

Handle the row-selection change for the DataGrid in ProductHeaderDataGrid_SelectionChanged(), and fetch and bind the appropriate product details information similarly.

To reduce the amount of data sent in updates, you send only the data that has changed. As shown in Listing 8, you extend the partial class for the ProductHeader data contract to include a Dirty flag so that you can track only the ProductHeader instances that have changed.

Listing 8. Extension to ProductHeader type to include a dirty flag
namespace Recipe7_1.ProductsDataViewer.ProductsDataSoapService
{
public partial class ProductHeader
{
//dirty flag
public bool Dirty { get; set; }
}
}

Referring back to Listing 7, you see that to use the Dirty flag appropriately, you handle the BeginningEdit event on the ProductHeaderDataGrid. This event is raised whenever the user starts to edit a cell. In the handler, you set a flag named InEdit to indicate that an edit process has started. You also handle the CurrentCellChanged event, which is raised whenever the user navigates away from a cell to another one. In this handler, you see if the cell was in edit mode by checking the InEdit flag. If it was, you get the current ProductHeader data item from the SelectedItem property of the DataGrid and set its Dirty flag appropriately.

You handle the Click event of the button Btn_SendHeaderUpdates to submit the ProductHeader updates. Using a LINQ query on the currently bound collection of ProductHeaders, you filter out the changed data based on the Dirty flag, and you pass on the changed data set via UpdateProductHeadersAsync(). To update a ProductDetail, pass on the currently bound ProductDetail instance to UpdateProductDetailAsync().

Other  
 
Most View
The Tablet Wars (Part 2) - Kindle Fire HD
Custom: Installation Nation (Part 1)
Windows 8 : Using Remote Assistance to Resolve Problems
Remote Event Receivers in Sharepoint 2013 : Introducing Remote Event Receivers
Microsoft ASP.NET 4 : Repeated-Value Data Binding (part 1)
The Linux Build: Part For Penguins (Part 3)
Nikon 24-85MM F3.5-4.5G ED-IF VR With Amazing Optical Performance
The Best Entry Level Phones – November 2012 (Part 3) - Samsung Galaxy Ace 2
Programming Windows Services with Microsoft Visual Basic 2008 : Service Notification
Zotac GTX 660 2GB - Dominate The Middle Ground Of Gaming Performance
Top 10
Sharepoint 2013 : Farm Management - Disable a Timer Job,Start a Timer Job, Set the Schedule for a Timer Job
Sharepoint 2013 : Farm Management - Display Available Timer Jobs on the Farm, Get a Specific Timer Job, Enable a Timer Job
Sharepoint 2013 : Farm Management - Review Workflow Configuration Settings,Modify Workflow Configuration Settings
Sharepoint 2013 : Farm Management - Review SharePoint Designer Settings, Configure SharePoint Designer Settings
Sharepoint 2013 : Farm Management - Remove a Managed Path, Merge Log Files, End the Current Log File
SQL Server 2012 : Policy Based Management - Evaluating Policies
SQL Server 2012 : Defining Policies (part 3) - Creating Policies
SQL Server 2012 : Defining Policies (part 2) - Conditions
SQL Server 2012 : Defining Policies (part 1) - Management Facets
Microsoft Exchange Server 2010 : Configuring Anti-Spam and Message Filtering Options (part 4) - Preventing Internal Servers from Being Filtered