Silverlight 3, .Net RIA Services, and common lookup data Redux


Back in March, I blogged about this topic here, and while that post provided some sample code as a first draft approach which could lead you to much of the solution, I did not provide a workable sample. So many folks have helped me with Silverlight 3 and the exciting potential it has for quickly developing Line-of-Business (LOB) applications, and this topic appears to be such a frequent question on the Silverlight forums, that I felt I owed it to the community to revisit this topic in a more complete fashion.

So, today, we will dive into a complete end-to-end solution that highlights the following technologies:

  • Silverlight 3
  • .Net RIA Services
  • Databinding of lookup data from separate entities with Foreign Key relationships

Update: This post is based on the Silverlight 3 Beta and the .Net RIA Services May 2009 CTP. Unfortunately, it breaks with the RTM version of Silverlight 3 and the July CTP of RIA Services in the areas of data access and the DataForm control. I promise to update this article as soon as possible, or write an entirely new one.

There will be no coverage of the new Silverlight 3 features supporting navigation, user authentication, out-of-browser (OOB) experience, pixel shaders, video codecs, etc. Instead, we will build a simple application focusing on the unique challenges associated with combo boxes in Silverlight data consuming applications. Nor will I be delving into how to implement this using a Model-View-ViewModel (MVVM) pattern. To keep things simple, we will use the Northwind database in an Entity Framework model (provided in the download). The UI will consist of a DataGrid for selecting Products and a linked DataForm with the selected Product Details. Because the Northwind Products table is related to the Categories and Suppliers tables via declarative foreign keys, we will provide a combo box for each that is bound to the equivalent foreign key on the Categories and Suppliers tables. The key difference is that we will display the human-readable name of the currently selected Category of Supplier in the dropdown, as well as a list of same when in edit mode and selecting a Category or Supplier. We won’t be binding to the actual child entities, but instead to IDictionary (key, value) implementations of them, which we will load immediately and cache on the client for performance.

I must extend a special thanks to Luke Longley of Microsoft and Chris Anderson (the original article from which I got the inspiration for the IDictionary refactoring was posted here), as well as numerous other folks on the Silverlight forums for their kind assistance in helping me understand what needed to be done to solve this UI challenge.

There is a complete sample project here for you to download and experiment with. Of course, you will need the Silverlight 3 Beta, .Net RIA Services 1.0 Beta, and Silverlight 3 Beta Tools for Visual Studio mentioned above. All of these are available here. You should have SQL Server Express 2005 installed at a minimum, but this project will also work with SQL Server 2005 or any version of SQL Server 2008 as well; you’ll just have a little re-configuration to do.

Getting to work

So, to begin, open Visual Studio 2008, and create a new Silverlight 3 Application (I called it SL3ComboBoxDemo). The Northwind database (modified by John Papa for the Entity Framework) is supplied in the Web project’s App_Data directory, so if you want to start with a fresh project instead of opening the sample, simply copy it there. In your Web project, add a new ADO.Net Entity Framework item and point it at your local NorthwindEF database. I named my model NorthwindEF_Model, and selected the Categories, Products and Suppliers tables. I named the connection string NorthwindEF_Entities and finished up.

Again in your Web project, add a new DomainService item. I named mine NorthwindEFService and checked all checkboxes to enable editing and metadata generation on the three entities in the model.

If you’re unfamiliar with the preceding two steps, simply refer to the first several chapters of the Microsoft .Net RIA Services Overview document. I then copied the code out of the automatically generated Test aspx file in the Web project and pasted it over top of the code in Default.aspx starting after the initial Page tag. I then deleted both test files (.aspx and .html).

Your solution (minus the items we’re going to add) should now look something like this:


Figure 1. SL3ComboBoxDemo Solution

Note that I have added the ErrorWindow files from the Silverlight Business Application template to our Silverlight 3 client project. We may need this, but hopefully not today <g>. Verify that SL3ComboBoxDemo.Web is your startup project and that Default.aspx is the set as the Start Page.

Open up the NorthwindEFService.metadata.cs file and make some additions in the ProductsMetadata as follows:

    public Categories Categories;
    public Suppliers Suppliers;

Now Build the solution. This generates the RIA data access code on the SL3ComboBoxDemo client.

The User Experience

Let’s add some UI and code-behind. Again, to keep things simple, we’re just going to add our UI and logic to the pages created by the project template: MainPage.xaml and MainPage.xaml.cs. In MainPage.xaml, add the following namespace references:


I set the width and height of the UserControl to 800 and 390 respectively, and this seemed to work for this application. Replace everything else from the outermost <Grid … declaration to the end of that Grid with this:

<Grid x:Name="LayoutRoot" Background="White" 
    HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <converter:CurrencyConverter x:Key="CurrencyConverter" />
    <converter:DictionaryConverter x:Key="DictionaryConverter" />
  <Border BorderBrush="#AA000000" BorderThickness="2" CornerRadius="5" 
          Margin="5,5,5,5" VerticalAlignment="Stretch" 
HorizontalAlignment="Stretch" > <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock Text="Silverlight 3 DataForm Combo Box Binding Demonstration" HorizontalAlignment="Center"/> <StackPanel> <ods:DomainDataSource x:Name="dds" LoadMethodName="LoadProducts" AutoLoad="True" LoadSize="42" LoadedData="dds_LoadedData" LoadingData="dds_LoadingData" SubmittedChanges="dds_SubmittedChanges" DataContext="BusinessManager.Context"> <ods:DomainDataSource.SortDescriptors> <data:SortDescriptor PropertyPath="ProductName" Direction="Ascending" /> </ods:DomainDataSource.SortDescriptors> <ods:DomainDataSource.FilterDescriptors> <data:FilterDescriptorCollection> <data:FilterDescriptor PropertyPath="ProductName" Operator="Contains"> <data:ControlParameter PropertyName="Text" RefreshEventName="TextChanged" ControlName="nameFilterBox"> </data:ControlParameter> </data:FilterDescriptor> </data:FilterDescriptorCollection> </ods:DomainDataSource.FilterDescriptors> </ods:DomainDataSource> <StackPanel Orientation="Horizontal" Margin="7,7,0,0"> <StackPanel VerticalAlignment="Top"> <StackPanel Orientation="Horizontal" Margin="0,0,0,5"
VerticalAlignment="Top"> <TextBlock Text=" Find: " VerticalAlignment="Center"/> <TextBox x:Name="nameFilterBox" Width="234" Height="24" /> </StackPanel> <datagrid:DataGrid x:Name="dataGrid1" Width="270" Height="266" HorizontalAlignment="Left" VerticalAlignment="Top" AutoGenerateColumns="False" IsReadOnly="True" SelectionChanged="dataGrid1_SelectionChanged" ItemsSource ="{Binding Data, ElementName=dds}"> <datagrid:DataGrid.Columns> <datagrid:DataGridTextColumn Header="Product Name" Binding="{Binding ProductName}" CanUserSort="True" /> </datagrid:DataGrid.Columns> </datagrid:DataGrid> <dataControls:DataPager x:Name="pager1" Width="270" PageSize="10" HorizontalAlignment="Left" Source="{Binding Data, ElementName=dds}"> </dataControls:DataPager> </StackPanel> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*" /> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Margin="3,0,0,0"> <dataControls:DataForm x:Name="dataForm1" Margin="5,0,5,0"
MinHeight="280" AutoEdit="False" AutoCommit="False" Width="480" VerticalAlignment="Top" CommandButtonsVisibility="All" Header="Product Details" CurrentItem="{Binding ElementName=dataGrid1, Path=SelectedItem}" DeletingItem="dataForm1_DeletingItem" ItemEditEnded="dataForm1_ItemEditEnded" AddingItem="dataForm1_AddingItem" AutoGenerateFields="False" CanUserAddItems="True"
CanUserDeleteItems="True"> <dataControls:DataForm.Fields> <dataControls:DataFormFieldGroup Orientation="Horizontal" > <dataControls:DataFormTextField FieldLabelPosition="Top" FieldLabelContent="Product Name " Binding="{Binding ProductName, Mode=TwoWay }" /> <dataControls:DataFormTextField FieldLabelPosition="Top" FieldLabelContent="Price " Binding="{Binding UnitPrice, Mode=TwoWay, Converter={StaticResource CurrencyConverter} }" /> </dataControls:DataFormFieldGroup> <dataControls:DataFormFieldGroup Orientation="Horizontal" > <dataControls:DataFormTextField FieldLabelPosition="Top" FieldLabelContent="Units In Stock " Binding="{Binding UnitsInStock, Mode=TwoWay }" /> <dataControls:DataFormTextField FieldLabelPosition="Top" FieldLabelContent="Units On Order " Binding="{Binding UnitsOnOrder, Mode=TwoWay }" /> <dataControls:DataFormTextField FieldLabelPosition="Top" FieldLabelContent="Reorder Level" Binding="{Binding ReorderLevel, Mode=TwoWay }" /> </dataControls:DataFormFieldGroup> <dataControls:DataFormSeparator/> <dataControls:DataFormFieldGroup Orientation="Horizontal"> <dataControls:DataFormComboBoxField x:Name="cboCategories" FieldLabelContent="Category:" DisplayMemberPath="Value" Binding="{Binding CategoryID, Mode=TwoWay, Converter={StaticResource DictionaryConverter}, ConverterParameter='ProductDictionaries.CategoriesLookup' }"/> <dataControls:DataFormComboBoxField x:Name="cboSuppliers" FieldLabelContent="Supplier:" DisplayMemberPath="Value" Binding="{Binding SupplierID, Mode=TwoWay, Converter={StaticResource DictionaryConverter}, ConverterParameter='ProductDictionaries.SuppliersLookup' }" /> </dataControls:DataFormFieldGroup> <dataControls:DataFormFieldGroup Orientation="Horizontal"> <dataControls:DataFormCheckBoxField FieldLabelContent="Discontinued?" FieldLabelPosition="Left" Binding="{Binding Discontinued, Mode=TwoWay}" /> <dataControls:DataFormDateField FieldLabelPosition="Left" FieldLabelContent="Discontinued Date:" Binding="{Binding DiscontinuedDate, Mode=TwoWay}" SelectedDateFormat="Short" /> </dataControls:DataFormFieldGroup> </dataControls:DataForm.Fields> </dataControls:DataForm> </StackPanel> <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,5,0,5"> <Button x:Name="saveButton" Width="100" Height="30" Margin="8,4,0,5" Content="Submit Changes" IsEnabled="False" Click="submitButton_Click" ToolTipService.ToolTip="Submit all changes to the server"/> </StackPanel> </Grid> </StackPanel> </StackPanel> </StackPanel> </Border> </Grid>

If you’ve gone through some of the RIA walk-throughs and samples (highly recommended), there’s not very much new here. So I’ll direct your attention to the binding of the DataFormComboBoxField, cboCategories:

<dataControls:DataFormComboBoxField x:Name="cboCategories" 
              FieldLabelContent="Category:" DisplayMemberPath="Value" 
              Binding="{Binding CategoryID, Mode=TwoWay, 
              Converter={StaticResource DictionaryConverter},
              ConverterParameter='ProductDictionaries.CategoriesLookup' }"/>

What we will be doing here is binding to the CategoryID in the Products entity, and using a Dictionary Converter to manage the relationship between what is in Products and what it points to in Categories. To do this, I created a Dictionary class for all lookup dictionaries associated with Products (the thought being that you might choose to partition different sets of lookup data into difference Dictionaries) as:

public static class DictionaryCache
    public static ProductDictionaries ProductDictionaries { get; set; }

The actual ProductDictionaries object looks like this:

public class ProductDictionaries
    public Dictionary<int, string> CategoriesLookup { get; set; }
    public Dictionary<int, string> SuppliersLookup { get; set; }

Now, let’s get our context loaded up. Because I typically have a lot of lookup tables, I decided to implement a singleton Context that the entire application can reference. That looks like this (BusinessManager.cs):

public static class BusinessManager 
    private static NorthwindEFContext context = new NorthwindEFContext();

    public static NorthwindEFContext Context
            return context;

Now in our code-behind, we can start to get our data loaded and bound. In MainPage.xaml.cs make sure you have the following using statements:

using System.Windows.Controls;
using System.Windows.Ria.Data;
using SL3ComboBoxDemo.Web;
using SL3ComboBoxDemo.Model;
using SL3ComboBoxDemo.Util;

Now, before the constructor, declare our data context and a bool useful for determining if we can skip some code execution once we’ve performed it once (due to the asynchronous nature of Silverlight data access):

private NorthwindEFContext Context = BusinessManager.Context;
private static bool formIsBound = false;

In the constructor, set up the following immediately after the InitializeComponent call:

dds.DomainContext = Context;
dataForm1.ItemEditEnded += 
new EventHandler<DataFormItemEditEndedEventArgs>(dataForm1_ItemEditEnded); pager1.PageIndexChanged +=
new EventHandler<EventArgs>(pager1_PageIndexChanged); this.Loaded += new RoutedEventHandler(MainPage_Loaded);

Down in MainPage_Loaded, we then wire up a Context.Loaded EventHandler and start loading data:

void MainPage_Loaded(object sender, RoutedEventArgs e)
    Context.Loaded += new EventHandler<LoadedDataEventArgs>(Context_Loaded);

The reason I used a LoadFromServer() call is in case I wanted to wire up Isolated Storage at some point later (in fact, I do this in a LOB application I am building). LoadFromServer() hits the Context up for only Categories and Suppliers, since the Products are set to AutoLoad in the markup:

private void LoadFromServer()

When Context_Loaded fires, we have to decide what we have and what to do with it. I decided that here is where I wanted to populate my ProductDictionaries if I had all of the data that I needed (since Context_Loaded is fired at least once for each Entity loaded). The way I ensure I have what I need to proceed is to check the counts of all the Entities I am loading.

void Context_Loaded(object sender, System.Windows.Ria.Data.LoadedDataEventArgs e)
    if (Context.Products.Count > 0
            && Context.Categories.Count > 0
            && Context.Suppliers.Count > 0)
        if (DictionaryCache.ProductDictionaries == null)
            DictionaryCache.ProductDictionaries = GetProductDictionaries();

GetProductDictionaries() does some LinqToSQL magic with our loaded entities on the client side to populate our ProductDictionaries object:

public ProductDictionaries GetProductDictionaries()
    var productDictionaries = new ProductDictionaries();

    var categoryQuery =
        from c in Context.Categories
        orderby c.CategoryName
        select c;
    productDictionaries.CategoriesLookup = 
        categoryQuery.ToDictionary(c => c.CategoryID,
                                   c => c.CategoryName);
    var supplierQuery =
        from s in Context.Suppliers
        orderby s.CompanyName
        select s;
    productDictionaries.SuppliersLookup = 
        supplierQuery.ToDictionary(s => s.SupplierID,
                                   s => s.CompanyName);
    return productDictionaries;

Now for the final piece of magic. Because the DataFormComboBoxField is not visible to us via normal x:Name identity (even though stubborn me put one on each <g> – more as a note to self), we’ll have to iterate through the controls in the DataForm and find the one bound to the ID we want to find, so that we can set the ItemsSource of each Combo Box correctly. That method is in the Util folder as DataFormBinding.GetFieldByBindingPath(). Back in the MainPage, we return to SetupComboBoxes (here’s where that bool declared in the class comes in handy):

void SetupComboBoxes()
    if (!formIsBound)
        if (Context.Categories.Count > 0)
            DataFormComboBoxField cboCategories =
                DataFormBinding.GetFieldByBindingPath(dataForm1, "CategoryID") 
                as DataFormComboBoxField;
            if (cboCategories.ItemsSource == null)
                cboCategories.ItemsSource = 
        if (Context.Suppliers.Count > 0)
            DataFormComboBoxField cboSuppliers =
                DataFormBinding.GetFieldByBindingPath(dataForm1, "SupplierID") 
                as DataFormComboBoxField;
            if (cboSuppliers.ItemsSource == null)
                cboSuppliers.ItemsSource = 
        if (Context.Products.Count > 0
            && Context.Categories.Count > 0
            && Context.Suppliers.Count > 0)
            formIsBound = true;
SetupDataForm(); } }
SetupDataForm(); }

SetupDataForm does the final setting of current item/index setting on the DataGrid and DataForm. I did not use any server activity method to indicate server activity, but you can easily wrap this Activity Control around the DataGrid, and have an indication when the data is being fetched. You can, of course, add any styling or theming that you want. Again, the focus on this post was the data! And here’s what we have built:


Figure 2. SL3ComboBoxDemo in action

The first time you load the application, it may take a short while to load the data, as your SQL Server Express User Instance is created, but any sessions thereafter will be fairly fast. I did not implement complete code to allow additions or deletions of Products in order to keep this article as simple as possible, but there is already some plumbing in place in the sample code. And so, I leave that to you as an exercise, dear reader. However, you can certainly edit the Products to your heart’s content.

Wrapping Up

So, there you have it – a quick tour, but now supported by a complete code solution, of how to bind lookup data to combo boxes on Silverlight 3 DataForms. If you look closely into the sample code, you will also find implementations of some other tips I learned on the Silverlight Forums. And finally, if you see any areas where I could improve my approach, please do not hesitate to let me know. I hope this helps you in your Silverlight development efforts.

Bob Baker

P.S. I went to Orlando CodeCamp on March 28, 2009. Did you?

posted @ Thursday, May 7, 2009 7:47 PM



Comments have been closed on this topic.