Run the Demo Here ==> http://developer.entityspaces.net/ES2008/Demos/Silverlight/PartTwo Download the Source Here ==> http://www.developer.entityspaces.net/downloads/EntitySpacesSilverlightDemo2.zip
In Part One, I described the process of getting Silverlight to interact with a WCF web service, and I showed how I bound EntitySpaces based data objects (WCF client proxies) to Silverlight controls, specifically a DataGrid control. While there were a lot of pages of review in Part One, it was mostly point-and-click “configuration coding”, as MyGeneration, EntitySpaces, and Visual Studio did most of the work for me.
Figure 1 - EntitySpaces RIA running in Safari 3.1 (on Windows) with just about 20 lines of hand-written client-server code and XAML markup.
But showing data in a grid isn’t very exciting. It’s time to look at some interaction with data and give EntitySpaces a little more credit than just a data wrapper.
Let’s assume that the user wants to filter the products list by category. I want to keep this demonstration as simple as possible, while proving out technical implementations of basic scenarios, so I’m not going to make any significant effort just yet to make this look pretty.
Here’s the XAML:
<UserControl x:Class="EntitySpacesSilverlightDemo_Silverlight.Page" xmlns:Controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid x:Name="LayoutRoot" Background="Gray"> <Grid.RowDefinitions> <RowDefinition Height="75" /> <RowDefinition /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Text="EntitySpaces on Silverlight ~ Northwind Demo" VerticalAlignment="Center" HorizontalAlignment="Center" /> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="200" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="Please Wait" Visibility="Collapsed" x:Name="WaitText" /> <ListBox Width="300" Grid.Column="0" x:Name="ViewSelector" SelectionChanged="ViewSelector_SelectionChanged"> <ListBoxItem Content="Products: All"></ListBoxItem> </ListBox> <Controls:DataGrid AutoGenerateColumns="True" x:Name="ESDataGrid" Grid.Column="1" /> </Grid> </Grid></UserControl>
For starters and for the sake of continuing the discussion of ES over WCF, I’m going to make this a client-server filter—I’ll pass in a CategoryID as a parameter in my WCF request for products.
Here’s the complete client-side code-behind for Page.xaml.cs, additions in bold+brown:
using System;using System.Collections.Generic;using System.Linq;using System.Windows;using System.Windows.Controls;using System.Windows.Documents;using System.Windows.Input;using System.Windows.Media;using System.Windows.Media.Animation;using System.Windows.Shapes;using System.Net;using EntitySpacesSilverlightDemo_Silverlight.Northwind; namespace EntitySpacesSilverlightDemo_Silverlight{ public partial class Page : UserControl { NorthwindClient.NorthwindClient ESNorthwind; public Page() { InitializeComponent(); ESNorthwind = new NorthwindClient.NorthwindClient(); ESNorthwind.GetCategoriesCompleted += ApplyCategoriesToList; ESNorthwind.GetProductsCompleted += new EventHandler<NorthwindClient.GetProductsCompletedEventArgs>(NorthwindClient_GetProductsCompleted); ESNorthwind.GetProductsByCategoryCompleted += new EventHandler<GetProductsByCategoryCompletedEventArgs>(Northwind_GetProductsByCategoryCompleted); GetCategories(); LoadAllProducts(); } private void GetCategories() { ESNorthwind.GetCategoriesAsync(); } private void ApplyCategoriesToList(object sender, GetCategoriesCompletedEventArgs e) { if (e.Error == null) { Categories[] cc = e.Result.Collection; for (int i = cc.Length-1; i >=0; i--) { Categories category = cc[i]; ListBoxItem lbi = new ListBoxItem(); lbi.Tag = "CategoryID: " + category.CategoryID; lbi.Content = "Products: " + category.CategoryName; ViewSelector.Items.Insert(1, lbi); } } } private void LoadAllProducts() { WaitText.Visibility = Visibility.Visible; ESNorthwind.GetProductsAsync(); } void LoadProductsByCategory(int categoryId) { WaitText.Visibility = Visibility.Visible; ESNorthwind.GetProductsByCategoryAsync(categoryId); } void Northwind_GetProductsByCategoryCompleted(object sender, GetProductsByCategoryCompletedEventArgs e) { WaitText.Visibility = Visibility.Collapsed; ProductsCollection pc = e.Result; ESDataGrid.ItemsSource = pc.Collection; } void NorthwindClient_GetProductsCompleted(object sender, Northwind.GetProductsCompletedEventArgs e) { WaitText.Visibility = Visibility.Collapsed; ProductsCollection pc = e.Result; ESDataGrid.ItemsSource = pc.Collection; ESDataGrid.Columns[ESDataGrid.Columns.Count - 1].Visibility = Visibility.Collapsed; // hide "esRowState" } private void ViewSelector_SelectionChanged(object sender, SelectionChangedEventArgs e) { ListBoxItem lbi = ViewSelector.SelectedItem as ListBoxItem; if (lbi != null) { if (lbi.Content.ToString() == "Products: All") { LoadAllProducts(); return; } string tag = lbi.Tag as string; if (tag != null && tag.StartsWith("CategoryID: ")) { tag = tag.Substring(12); // length of "CategoryID:" LoadProductsByCategory(int.Parse(tag)); } } } }}
using System;using System.Collections.Generic;using System.Linq;using System.Windows;using System.Windows.Controls;using System.Windows.Documents;using System.Windows.Input;using System.Windows.Media;using System.Windows.Media.Animation;using System.Windows.Shapes;using System.Net;using EntitySpacesSilverlightDemo_Silverlight.Northwind;
namespace EntitySpacesSilverlightDemo_Silverlight{ public partial class Page : UserControl { NorthwindClient.NorthwindClient ESNorthwind;
public Page() { InitializeComponent(); ESNorthwind = new NorthwindClient.NorthwindClient();
ESNorthwind.GetCategoriesCompleted += ApplyCategoriesToList; ESNorthwind.GetProductsCompleted += new EventHandler<NorthwindClient.GetProductsCompletedEventArgs>(NorthwindClient_GetProductsCompleted); ESNorthwind.GetProductsByCategoryCompleted += new EventHandler<GetProductsByCategoryCompletedEventArgs>(Northwind_GetProductsByCategoryCompleted);
GetCategories();
LoadAllProducts(); }
private void GetCategories() { ESNorthwind.GetCategoriesAsync(); }
private void ApplyCategoriesToList(object sender, GetCategoriesCompletedEventArgs e) { if (e.Error == null) { Categories[] cc = e.Result.Collection; for (int i = cc.Length-1; i >=0; i--) { Categories category = cc[i]; ListBoxItem lbi = new ListBoxItem(); lbi.Tag = "CategoryID: " + category.CategoryID; lbi.Content = "Products: " + category.CategoryName; ViewSelector.Items.Insert(1, lbi); } } }
private void LoadAllProducts() { WaitText.Visibility = Visibility.Visible; ESNorthwind.GetProductsAsync(); }
void LoadProductsByCategory(int categoryId) { WaitText.Visibility = Visibility.Visible; ESNorthwind.GetProductsByCategoryAsync(categoryId); }
void Northwind_GetProductsByCategoryCompleted(object sender, GetProductsByCategoryCompletedEventArgs e) { WaitText.Visibility = Visibility.Collapsed; ProductsCollection pc = e.Result; ESDataGrid.ItemsSource = pc.Collection; }
void NorthwindClient_GetProductsCompleted(object sender, Northwind.GetProductsCompletedEventArgs e) { WaitText.Visibility = Visibility.Collapsed; ProductsCollection pc = e.Result; ESDataGrid.ItemsSource = pc.Collection; ESDataGrid.Columns[ESDataGrid.Columns.Count - 1].Visibility = Visibility.Collapsed; // hide "esRowState" }
private void ViewSelector_SelectionChanged(object sender, SelectionChangedEventArgs e) { ListBoxItem lbi = ViewSelector.SelectedItem as ListBoxItem; if (lbi != null) { if (lbi.Content.ToString() == "Products: All") { LoadAllProducts(); return; } string tag = lbi.Tag as string; if (tag != null && tag.StartsWith("CategoryID: ")) { tag = tag.Substring(12); // length of "CategoryID:" LoadProductsByCategory(int.Parse(tag)); } } } }}
On the server, we will perform a basic EntitySpaces query operation.
INorthwind service interface:[ServiceContract]public interface INorthwind{ [OperationContract] ProductsCollectionProxyStub GetProducts(); [OperationContract] ProductsCollectionProxyStub GetProductsByCategory(int categoryId); [OperationContract] CategoriesCollectionProxyStub GetCategories();}Northwind service implementation:public class Northwind : INorthwind{ public ProductsCollectionProxyStub GetProducts() { ProductsCollection pc = new ProductsCollection(); pc.LoadAll(); ProductsCollectionProxyStub pcs = new ProductsCollectionProxyStub(pc); return pcs; } public ProductsCollectionProxyStub GetProductsByCategory(int categoryId) { ProductsCollection pc = new ProductsCollection(); pc.Query.Where(pc.Query.CategoryID.Equal(categoryId)); pc.Load(pc.Query); ProductsCollectionProxyStub pcs = new ProductsCollectionProxyStub(pc); return pcs; } public CategoriesCollectionProxyStub GetCategories() { CategoriesCollection cc = new CategoriesCollection(); cc.LoadAll(); // strip out the OLE bitmaps for (int i = 0; i < cc.Count; i++) { cc[i].Picture = null; } CategoriesCollectionProxyStub ccs = new CategoriesCollectionProxyStub(cc); return ccs; }}
INorthwind service interface:[ServiceContract]public interface INorthwind{ [OperationContract] ProductsCollectionProxyStub GetProducts();
[OperationContract] ProductsCollectionProxyStub GetProductsByCategory(int categoryId); [OperationContract] CategoriesCollectionProxyStub GetCategories();}Northwind service implementation:public class Northwind : INorthwind{ public ProductsCollectionProxyStub GetProducts() { ProductsCollection pc = new ProductsCollection(); pc.LoadAll(); ProductsCollectionProxyStub pcs = new ProductsCollectionProxyStub(pc); return pcs; }
public ProductsCollectionProxyStub GetProductsByCategory(int categoryId) { ProductsCollection pc = new ProductsCollection(); pc.Query.Where(pc.Query.CategoryID.Equal(categoryId)); pc.Load(pc.Query); ProductsCollectionProxyStub pcs = new ProductsCollectionProxyStub(pc); return pcs; }
public CategoriesCollectionProxyStub GetCategories() { CategoriesCollection cc = new CategoriesCollection(); cc.LoadAll(); // strip out the OLE bitmaps for (int i = 0; i < cc.Count; i++) { cc[i].Picture = null; }
CategoriesCollectionProxyStub ccs = new CategoriesCollectionProxyStub(cc); return ccs; }}
Practically speaking, calling out to the server every time you want to filter the products is bad design unless the Products list contains a lot (hundreds of thousands) of records, in which case we should also drop the “Products: All” unfiltered option from the ListBox so that the user cannot make the mistake of loading all products which might take forever.
If the unfiltered source data is relatively small, as Northwind’s Products table actually is, then it would make more sense to load all products from the server and then filter against the category locally. This eliminates the HTTP-based callback to the server and makes the user’s experience much faster and more responsive.
LINQ-to-Objects is supported in Silverlight, and this includes LINQ-to-EntitySpaces-client-proxies. We can change the NorthwindClient_GetProductsCompleted and ViewSelector_SelectionChanged methods to use LINQ filtering rather than server-side filtering. In so doing, we can remove much of the code we just added and achieve the same results but executing much faster.
Products[] NorthwindProducts = null;void NorthwindClient_GetProductsCompleted(object sender, Northwind.GetProductsCompletedEventArgs e){ WaitText.Visibility = Visibility.Collapsed; NorthwindProducts = e.Result.Collection; ESDataGrid.ItemsSource = NorthwindProducts; ESDataGrid.Columns[ESDataGrid.Columns.Count - 1].Visibility = Visibility.Collapsed; // hide "esRowState"} private void ViewSelector_SelectionChanged(object sender, SelectionChangedEventArgs e){ ListBoxItem lbi = ViewSelector.SelectedItem as ListBoxItem; if (lbi != null) { if (lbi.Content.ToString() == "Products: All") { ESDataGrid.ItemsSource = NorthwindProducts; ESDataGrid.Columns[ESDataGrid.Columns.Count - 1].Visibility = Visibility.Collapsed; // hide "esRowState" return; } string tag = lbi.Tag as string; if (tag != null && tag.StartsWith("CategoryID: ")) { int catID = int.Parse(tag.Substring(12)); ESDataGrid.ItemsSource = from p in NorthwindProducts where p.CategoryID == catID select p; ESDataGrid.Columns[ESDataGrid.Columns.Count - 1].Visibility = Visibility.Collapsed; // hide "esRowState" } }}
Products[] NorthwindProducts = null;void NorthwindClient_GetProductsCompleted(object sender, Northwind.GetProductsCompletedEventArgs e){ WaitText.Visibility = Visibility.Collapsed; NorthwindProducts = e.Result.Collection; ESDataGrid.ItemsSource = NorthwindProducts; ESDataGrid.Columns[ESDataGrid.Columns.Count - 1].Visibility = Visibility.Collapsed; // hide "esRowState"}
private void ViewSelector_SelectionChanged(object sender, SelectionChangedEventArgs e){ ListBoxItem lbi = ViewSelector.SelectedItem as ListBoxItem; if (lbi != null) { if (lbi.Content.ToString() == "Products: All") { ESDataGrid.ItemsSource = NorthwindProducts; ESDataGrid.Columns[ESDataGrid.Columns.Count - 1].Visibility = Visibility.Collapsed; // hide "esRowState" return; } string tag = lbi.Tag as string; if (tag != null && tag.StartsWith("CategoryID: ")) { int catID = int.Parse(tag.Substring(12)); ESDataGrid.ItemsSource = from p in NorthwindProducts where p.CategoryID == catID select p; ESDataGrid.Columns[ESDataGrid.Columns.Count - 1].Visibility = Visibility.Collapsed; // hide "esRowState" } }}
We can now eliminate one of the INorthwind interfaces we just added, ..
.. and we can eliminate some of the just-added client/server handling code in Silverlight:
Let’s assume that the user is an administrator who is maintaining this list of products and needs to be able to make changes to product data on the fly. The bad news is that in the interest of focusing on EntitySpaces and less on Silverlight UI, I intended to refrain from creating a pop-up editor for editing a record. The good news is that the Silverlight DataGrid supports two-way databinding and inline edits, and in such case one would need no layout changes at all. I only need to add an event handler for DataGrid edits and pass the revised record up to the server via WCF.
We’ll bind to the CommittingCellEdit and CommittingRowEdit events. In CommittingCellEdit we’ll update the property that was edited, and in CommittingRowEdit we’ll pass the Product out to the server.
Page.xaml (modify):
<Controls:DataGrid AutoGenerateColumns="True" x:Name="ESDataGrid" Grid.Column="1" CommittingCellEdit="ESDataGrid_CommittingCellEdit"CommittingRowEdit="ESDataGrid_CommittingRowEdit" />
Page.xaml.cs (add):
private void ESDataGrid_CommittingCellEdit(object sender, DataGridCellCancelEventArgs e){ Products p = (Products)e.Row.DataContext; string f = e.Column.Header.ToString(); string v = ((TextBox)e.Element).Text; System.Reflection.PropertyInfo pi = p.GetType().GetProperty(f); if (pi.PropertyType == typeof(string)) { pi.SetValue(p, v, null); } else if (pi.PropertyType == typeof(int) || pi.PropertyType == typeof (int?)) { pi.SetValue(p, int.Parse(v), null); } else e.Cancel = true; if (!e.Cancel) { p.esRowState = "Modified"; }} private void ESDataGrid_CommittingRowEdit(object sender, DataGridRowCancelEventArgs e){ Products p = (Products)e.Row.DataContext; if (p.esRowState == "Modified") { ESNorthwind.UpdateProductAsync(p); }}
private void ESDataGrid_CommittingCellEdit(object sender, DataGridCellCancelEventArgs e){ Products p = (Products)e.Row.DataContext; string f = e.Column.Header.ToString(); string v = ((TextBox)e.Element).Text; System.Reflection.PropertyInfo pi = p.GetType().GetProperty(f); if (pi.PropertyType == typeof(string)) { pi.SetValue(p, v, null); } else if (pi.PropertyType == typeof(int) || pi.PropertyType == typeof (int?)) { pi.SetValue(p, int.Parse(v), null); } else e.Cancel = true; if (!e.Cancel) { p.esRowState = "Modified"; }}
private void ESDataGrid_CommittingRowEdit(object sender, DataGridRowCancelEventArgs e){ Products p = (Products)e.Row.DataContext; if (p.esRowState == "Modified") { ESNorthwind.UpdateProductAsync(p); }}
This won’t compile until we revise our service in the Web project and then update the reference to it in the Silverlight project.
INorthwind.cs (add):
[OperationContract]void UpdateProduct(ProductsProxyStub product);
AppCode/Northwind.cs (or Northwind.svc.cs, add):
public void UpdateProduct(ProductsProxyStub product){ Products prod = (Products)product.GetEntity(); prod.Save(); // wow, that was easy..}
Silverlight project:
Now the Silverlight code should compile, and if you set breakpoints both on the server’s UpdateProduct() method as well as on the new event handler in Silverlight, you should be able to watch the data model as it gets updated and sent back to the server.
Recommitting back to the server was so easy (although the reflection bits in the CommitingCellEdit event handler needs some clean-up) it’s actually a little dangerous. We can’t go live with this change on a public demo without having a nightly automated process to restore the Northwind data or else it will become unusable.
But this tutorial should have still sufficed in getting you on your feet with fetching, filtering, and updating data between Silverlight and EntitySpaces. There is a lot of design-related knowledge to be had in Silverlight, and real-world business objects to be realized in EntitySpaces, but this was a basic data discussion, so the rest is up to you.
From mobile devices to large scale enterprise solutions in need of serious transaction support, EntitySpaces can meet your needs. Whether you’re writing an ASP.NET application with medium trust requirements, a Mono application, or a Windows.Forms application, the EntitySpaces architecture is there for you. EntitySpaces is provider independent, which means that you can run the same binary code against any of the supported databases. EntitySpaces is available in both C# and VB.NET. EntitySpaces uses no reflection, no XML files, and sports a tiny foot print of less than 200k. Pound for pound, EntitySpaces is one tough, dependable .NET architecture. The EntitySpaces Team-- EntitySpaces LLCPersistence Layer and Business Objects for Microsoft .NEThttp://www.entityspaces.net
From mobile devices to large scale enterprise solutions in need of serious transaction support, EntitySpaces can meet your needs. Whether you’re writing an ASP.NET application with medium trust requirements, a Mono application, or a Windows.Forms application, the EntitySpaces architecture is there for you. EntitySpaces is provider independent, which means that you can run the same binary code against any of the supported databases. EntitySpaces is available in both C# and VB.NET. EntitySpaces uses no reflection, no XML files, and sports a tiny foot print of less than 200k. Pound for pound, EntitySpaces is one tough, dependable .NET architecture.
The EntitySpaces Team--
EntitySpaces LLCPersistence Layer and Business Objects for Microsoft .NEThttp://www.entityspaces.net
Page rendered at Sunday, March 14, 2010 3:57:24 PM (Eastern Standard Time, UTC-05:00)
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.