How to: Bind Data Service Data to Controls (Silverlight Client)
With WCF Data Services, you can bind Silverlight controls, such as a ListBox or ComboBox, to an instance of DataServiceCollection<T>. This class handles the events raised by the controls to keep the DataServiceContext synchronized with changes that are made to data in the controls. A DataServiceCollection<T> is defined based on a DataServiceQuery<TElement>. This query, when it is executed, returns an Open Data Protocol (OData) feed that provides the objects for the collection.
The procedures in this topic show how to perform the following tasks:
(Optional) enable paging functionality in the Northwind data service.
Create a new Silverlight application.
Generate client data service classes that support automatic data binding.
Query the data service.
Bind the results to controls in the application.
Use a CollectionViewSource instance to simplify the binding of master/detail Order and Order_Detail objects. To do this, set the Source property of the CollectionViewSource to the master DataServiceCollection<T>. The data binding of controls is defined in the XAML such that changing the selected Order changes the displayed Order_Details when the collection is loaded. For more information, see How to: Bind to Hierarchical Data and Create a Master/Details View.
Note
The CollectionViewSource class is not supported in Silverlight 3.
This example shows how to bind an OData feed when paging is enabled in the data service. Paging of a data feed is supported by version 2.0 of the OData Protocol, and later versions.
The Northwind sample data service accessed by the application is created when you complete the WCF Data Services quickstart. You can also use the public Northwind sample data service that is published on the OData Web site; this sample data service is read-only and attempting to save changes returns an error.
To enable paging in the Northwind sample data service
In Solution Explorer under your ASP.NET project, double-click Northwind.svc.
This opens the code page for the Northwind sample data service.
In the code for the data service, append the following code to the InitializeService method:
' Set the data service version to V2 to support paging. config.DataServiceBehavior.MaxProtocolVersion = _ System.Data.Services.Common.DataServiceProtocolVersion.V2 ' Set paging limits for the Customers and Orders entity sets. ' These limits are set very low to demonstrate paging with the ' Northwind database. Paging should be configured to optimize ' data service performance. config.SetEntitySetPageSize("Orders", 2) config.SetEntitySetPageSize("Order_Details", 2)
// Set the data service version to V2 to support paging. config.DataServiceBehavior.MaxProtocolVersion = System.Data.Services.Common.DataServiceProtocolVersion.V2; // Set paging limits for the Customers and Orders entity sets. // These limits are set very low to demonstrate paging with the // Northwind database. Paging should be configured to optimize // data service performance. config.SetEntitySetPageSize("Orders", 2); config.SetEntitySetPageSize("Order_Details", 2);
This enables paging for the Orders and Order_Details entity sets.
To create the application for Silverlight project
In Solution Explorer, right-click the Solution, point to Add, and then select New Project.
In the Add New Project dialog box, select Silverlight from the Categories pane, and then select the Silverlight Application template. Name the project DataBindingSample.
In the Add Silverlight Application dialog box, select Host the Silverlight application in a new or existing Web site in the solution. Select Add a test page that references the application and Make it the start page.
Click OK.
This creates the application for Silverlight.
To add a data service reference to the project
Right-click the DataBindingSample project, click Add Service Reference, and then click Discover.
This displays the Northwind data service that you created in the first task.
In the Namespace text box, type Northwind, and then click OK.
This adds a new code file to the project, which contains the data classes that are used to access and interact with data service resources as objects. The data classes are created in the namespace DataBindingSample.Northwind.
To define the client application user-interface
In Solution Explorer under DataBindingSample, right-click References and click Add Reference.
This displays the Add Reference dialog box.
Select System.Windows.Controls.Data and click OK.
In Solution Explorer, double-click the MainPage.xaml file. This opens XAML markup for the Page class that is the user interface for the Silverlight application.
Replace the existing XAML markup with the following markup that defines the application user-interface:
<!-- NOTE: By convention, the sdk prefix indicates a URI-based XAML namespace declaration for Silverlight SDK client libraries. This namespace declaration is valid for Silverlight 4 only. In Silverlight 3, you must use individual XAML namespace declarations for each CLR assembly and namespace combination outside the scope of the default Silverlight XAML namespace. For more information, see the help topic "Prefixes and Mappings for Silverlight Libraries". --> <UserControl x:Class="MainPage" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:sdk="https://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" Loaded="MainPage_Loaded" mc:Ignorable="d" xmlns:d="https://schemas.microsoft.com/expression/blend/2008" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:my="clr-namespace:DataBindingSample.Northwind" d:DesignHeight="371" d:DesignWidth="0"> <UserControl.Resources> <CollectionViewSource x:Key="OrdersViewSource" d:DesignSource="{d:DesignInstance my:Order, CreateList=True}" /> <CollectionViewSource x:Key="OrdersOrder_DetailsViewSource" Source="{Binding Path=Order_Details, Source={StaticResource OrdersViewSource}}" /> </UserControl.Resources> <StackPanel Orientation="Vertical" Margin="10" Height="Auto" Name="LayoutRoot" Width="450"> <StackPanel Orientation="Horizontal"> <sdk:Label Content="Customer ID:"/> <TextBox Name="customerId" Text="ALFKI" Margin="10" Width="100"/> <Button Name="getCustomerOrders" Content="Get Orders" Height="25" Width="80" Click="getCustomerOrders_Click" /> </StackPanel> <StackPanel Name="ordersStackPanel" VerticalAlignment="Top" Orientation="Vertical" DataContext="{StaticResource OrdersViewSource}"> <Grid HorizontalAlignment="Left" Name="ordersGrid" VerticalAlignment="Top"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <sdk:Label Content="Order:" Grid.Column="0" Grid.Row="0" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" /> <ComboBox DisplayMemberPath="OrderID" Grid.Column="1" Grid.Row="0" Height="23" HorizontalAlignment="Left" ItemsSource="{Binding}" Margin="3" Name="OrderIDComboBox" VerticalAlignment="Center" Width="120" SelectionChanged="ordersList_SelectionChanged"> <ComboBox.ItemsPanel> <ItemsPanelTemplate> <VirtualizingStackPanel /> </ItemsPanelTemplate> </ComboBox.ItemsPanel> </ComboBox> <sdk:Label Content="Freight:" Grid.Column="2" Grid.Row="0" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" /> <TextBox Grid.Column="3" Grid.Row="0" Height="23" HorizontalAlignment="Left" Margin="3" Name="FreightTextBox" Text="{Binding Path=Freight, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" /> <sdk:Label Content="Required Date:" Grid.Column="0" Grid.Row="1" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" /> <sdk:DatePicker Grid.Column="1" Grid.Row="1" Height="23" HorizontalAlignment="Left" Margin="3" Name="RequiredDateDatePicker" SelectedDate="{Binding Path=RequiredDate, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" /> <sdk:Label Content="Order Date:" Grid.Column="2" Grid.Row="1" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" /> <TextBox Grid.Column="3" Grid.Row="1" Height="23" HorizontalAlignment="Left" Width="120" Margin="3" Name="OrderDateTextBlock" Text="{Binding Path=OrderDate}" VerticalAlignment="Center" /> </Grid> </StackPanel> <StackPanel DataContext="{StaticResource OrdersOrder_DetailsViewSource}" Orientation="Vertical"> <sdk:Label Content="Order items:" Margin="10"/> <sdk:DataGrid AutoGenerateColumns="False" Height="170" ItemsSource="{Binding}" Name="Order_DetailsDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" Width="400"> <sdk:DataGrid.Columns> <sdk:DataGridTextColumn x:Name="ProductIDColumn" Binding="{Binding Path=ProductID}" Header="Product" Width="SizeToHeader" /> <sdk:DataGridTextColumn x:Name="QuantityColumn" Binding="{Binding Path=Quantity}" Header="Quantity" Width="SizeToHeader" /> <sdk:DataGridTextColumn x:Name="DiscountColumn" Binding="{Binding Path=Discount}" Header="Discount" Width="SizeToHeader" /> <sdk:DataGridTextColumn x:Name="UnitPriceColumn" Binding="{Binding Path=UnitPrice}" Header="Unit Price" Width="SizeToHeader" /> </sdk:DataGrid.Columns> </sdk:DataGrid> </StackPanel> <Button Name="saveChangesButton" Content="Save Changes" HorizontalAlignment="Right" Click="saveChangesButton_Click" Width="100" Height="25" Margin="10"/> </StackPanel> </UserControl>
Note
For a C# application, you must include the namespace in the Class attribute of the UserControl. The default namespace is not required for a Visual Basic application.
To add the code that binds data service data to controls in the Silverlight application
In Solution Explorer under DataBindingSample, open the code page for the MainPage.xaml file, and add the following using statement (Imports in Visual Basic).
using System.Windows.Data; using System.Data.Services.Client; using DataBindingSample.Northwind;
Add the following declarations to the MainPage class:
Dim context As NorthwindEntities Dim trackedOrders As DataServiceCollection(Of Order) Dim selectedOrder As Order Dim ordersViewSource As CollectionViewSource
NorthwindEntities context; DataServiceCollection<Order> trackedOrders; Order selectedOrder; CollectionViewSource ordersViewSource;
Add the following MainPage_Loaded method to the MainPage class:
Private Sub MainPage_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs) ' Initialize the data service context. context = _ New NorthwindEntities(New Uri("https://localhost:54321/Northwind.svc")) ' Initialize the binding and view source collections. trackedOrders = New DataServiceCollection(Of Order)() ordersViewSource = CType(Me.Resources("OrdersViewSource"), CollectionViewSource) ' Define a handler for the LoadCompleted event of the collection. AddHandler trackedOrders.LoadCompleted, _ AddressOf trackedOrders_LoadCompleted End Sub
private void MainPage_Loaded(object sender, RoutedEventArgs e) { // Initialize the data service context. context = new NorthwindEntities(new Uri("Northwind.svc", UriKind.Relative)); // Initialize the binding and view source collections. trackedOrders = new DataServiceCollection<Order>(); ordersViewSource = (CollectionViewSource)this.Resources["OrdersViewSource"]; // Define a handler for the LoadCompleted event of the binding collection. trackedOrders.LoadCompleted += new EventHandler<LoadCompletedEventArgs>(trackedOrders_LoadCompleted); }
When the page is loaded, this code initializes the binding collections and content, and registers the method to handle the LoadCompleted event raised by the binding collection.
Insert the following code into the MainPage class:
Private Sub getCustomerOrders_Click(ByVal sender As Object, _ ByVal e As RoutedEventArgs) ' Define a query that returns orders for a give customer. Dim query = From orderByCustomer In context.Orders _ Where orderByCustomer.Customer.CustomerID = _ Me.customerId.Text _ Select orderByCustomer ' Asynchronously load the result of the query. trackedOrders.LoadAsync(query) ' Disable the button until the loading is complete. getCustomerOrders.IsEnabled = False End Sub Private Sub trackedOrders_LoadCompleted(ByVal sender As Object, _ ByVal e As LoadCompletedEventArgs) If e.Error Is Nothing Then ' Load all pages of Orders before binding. If trackedOrders.Continuation IsNot Nothing Then trackedOrders.LoadNextPartialSetAsync() Else ' Bind the root StackPanel element to the collection ' related object binding paths are defined in the XAML. ordersViewSource.Source = trackedOrders ' Re-enable the button since the loading is complete. getCustomerOrders.IsEnabled = True End If Else MessageBox.Show(String.Format("An error has occured: {0}", e.Error.Message)) getCustomerOrders.IsEnabled = True End If End Sub
private void getCustomerOrders_Click(object sender, RoutedEventArgs e) { // Reset the grids. ordersViewSource.Source = null; // Define a query that returns orders for a give customer. var query = from orderByCustomer in context.Orders where orderByCustomer.Customer.CustomerID == this.customerId.Text select orderByCustomer; // Asynchronously load the result of the query. trackedOrders.LoadAsync(query); // Disable the button until the loading is complete. getCustomerOrders.IsEnabled = false; } private void trackedOrders_LoadCompleted(object sender, LoadCompletedEventArgs e) { if (e.Error == null) { // Load all pages of Orders before binding. if (trackedOrders.Continuation != null) { trackedOrders.LoadNextPartialSetAsync(); } else { // Bind the root StackPanel element to the collection; // related object binding paths are defined in the XAML. ordersViewSource.Source = trackedOrders; // Re-enable the button since the loading is complete. getCustomerOrders.IsEnabled = true; } } else { MessageBox.Show(string.Format("An error has occured: {0}",e.Error.Message)); getCustomerOrders.IsEnabled = true; } }
When the getCustomerOrders button is clicked, the following operations are performed:
The LoadAsync method is called on the binding collection to execute the supplied query that returns orders, filtered by the supplied customer ID.
The LoadNextPartialSetAsync method is called to load subsequent results pages as long as the Continuation property returns a value.
The collection of loaded Order objects is bound to the Source property of the CollectionViewSource, which is the master binding object for all controls in the page.
Insert the following code into the MainPage class:
Private Sub ordersList_SelectionChanged(ByVal sender As Object, _ ByVal e As SelectionChangedEventArgs) ' Get the selected Order in the DataGrid. Dim ordersList As ComboBox = CType(sender, ComboBox) selectedOrder = CType(ordersList.SelectedItem, Order) If Not selectedOrder Is Nothing Then ' Asynchronously load related items, if they are not already loaded. If selectedOrder.Order_Details.Count = 0 Then ' Register the method to handle the LoadCompleted event. AddHandler selectedOrder.Order_Details.LoadCompleted, _ AddressOf Order_Details_LoadCompleted Try ' Load the related items. selectedOrder.Order_Details.LoadAsync() Catch ex As InvalidOperationException MessageBox.Show(String.Format("An error has occured: {0}", _ ex.Message)) End Try End If End If End Sub Private Sub Order_Details_LoadCompleted(ByVal sender As Object, _ ByVal e As LoadCompletedEventArgs) Dim trackedItems As DataServiceCollection(Of Order_Detail) = _ CType(sender, DataServiceCollection(Of Order_Detail)) ' Load any remaining pages of Order_Details. If Not trackedItems.Continuation Is Nothing Then Try trackedItems.LoadNextPartialSetAsync() Catch ex As InvalidOperationException MessageBox.Show(String.Format("An error has occured: {0}", _ ex.Message)) End Try End If End Sub
private void ordersList_SelectionChanged(object sender, SelectionChangedEventArgs e) { // Get the selected Order in the DataGrid. ComboBox ordersList = sender as ComboBox; selectedOrder = ((Order)(ordersList.SelectedItem)); if (selectedOrder != null) { // Asynchronously load related items, if they are not already loaded. if (selectedOrder.Order_Details.Count == 0) { // Register the method to handle the LoadCompleted event. selectedOrder.Order_Details.LoadCompleted += new EventHandler<LoadCompletedEventArgs>( Order_Details_LoadCompleted); try { // Load the related items. selectedOrder.Order_Details.LoadAsync(); } catch (InvalidOperationException ex) { MessageBox.Show(string.Format("An error has occured: {0}", ex.Message)); } } } } void Order_Details_LoadCompleted(object sender, LoadCompletedEventArgs e) { DataServiceCollection<Order_Detail> trackedItems = sender as DataServiceCollection<Order_Detail>; // Load any remaining pages of Order_Details. if (trackedItems.Continuation != null) { try { trackedItems.LoadNextPartialSetAsync(); } catch (InvalidOperationException ex) { MessageBox.Show(string.Format("An error has occured: {0}", ex.Message)); } } }
The ordersList_SelectionChanged method handles the SelectionChanged event. When the user selects an order in the ComboBox, the following operations are performed:
The Order_Details_LoadCompleted method is registered to handle the LoadCompleted event raised by the DataServiceCollection<T> that represents the related items for the selected order.
The LoadAsync method is called to asynchronously load the Order_Details objects related to the Order selected in the ComboBox.
The LoadNextPartialSetAsync method is called to load subsequent results pages as long as the Continuation property returns a value.
The loaded items are propagated to the DataGrid by the child CollectionViewSource.
Insert the following code that saves changes into the MainPage class:
' We need to persist the result of an operation ' to be able to invoke the dispatcher. Private currentResult As IAsyncResult Private Sub saveChangesButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs) ' Define the delegate to callback into the process Dim callback As AsyncCallback = AddressOf OnChangesSaved Try ' Start the saving changes operation. This needs to be a ' batch operation in case we are added a new object with ' a new relationship. context.BeginSaveChanges(SaveChangesOptions.Batch, _ callback, context) Catch ex As Exception MessageBox.Show(String.Format( _ "The changes could not be saved to the data service.\n" _ & "The following error occurred: {0}", ex.Message)) End Try End Sub Private Sub OnChangesSaved(ByVal result As IAsyncResult) ' Persist the result for the delegate. currentResult = result ' Use the Dispatcher to ensure that the ' asynchronous call returns in the correct thread. Dispatcher.BeginInvoke(AddressOf ChangesSavedByDispatcher) End Sub Private Sub ChangesSavedByDispatcher() Dim errorOccured As Boolean = False context = CType(currentResult.AsyncState, NorthwindEntities) Try ' Complete the save changes operation and display the response. Dim response As DataServiceResponse = _ context.EndSaveChanges(currentResult) For Each changeResponse As ChangeOperationResponse In response If changeResponse.Error IsNot Nothing Then errorOccured = True Next If Not errorOccured Then MessageBox.Show("The changes have been saved to the data service.") Else MessageBox.Show("An error occured. One or more changes could not be saved.") End If Catch ex As Exception ' Display the error from the response. MessageBox.Show(String.Format("The following error occured: {0}", ex.Message)) End Try End Sub
private void saveChangesButton_Click(object sender, RoutedEventArgs e) { try { // Start the saving changes operation. This needs to be a // batch operation in case we are added a new object with // a new relationship. context.BeginSaveChanges(SaveChangesOptions.Batch, OnChangesSaved, context); } catch (Exception ex) { MessageBox.Show(string.Format("The changes could not be saved to the data service.\n" + "The following error occurred: {0}", ex.Message)); } } private void OnChangesSaved(IAsyncResult result) { bool errorOccured = false; // Use the Dispatcher to ensure that the // asynchronous call returns in the correct thread. Dispatcher.BeginInvoke(() => { context = result.AsyncState as NorthwindEntities; try { // Complete the save changes operation and display the response. DataServiceResponse response = context.EndSaveChanges(result); foreach (ChangeOperationResponse changeResponse in response) { if (changeResponse.Error != null) errorOccured = true; } if (!errorOccured) { MessageBox.Show("The changes have been saved to the data service."); } else { MessageBox.Show("An error occured. One or more changes could not be saved."); } } catch (Exception ex) { // Display the error from the response. MessageBox.Show(string.Format("The following error occured: {0}", ex.Message)); } } ); }
This code asynchronously sends changes that were made in the data bound controls back to the data service.
See Also
Other Resources
WCF Data Services QuickStart for Silverlight
WCF Data Services (Silverlight)
WCF Data Services Tasks for Silverlight