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.
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.
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.
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.
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().