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

One of the first things I learned in developing User Interfaces (UI) over the last 30 years was to eliminate as much user typing as possible. One way to do that is to have common elements available as selections from lists (combo boxes, radio buttons, etc.). Many of these lists should also be maintainable on the user system, so as needs change, the items can be updated.

I have been working with the new Silverlight 3 Community Technology Preview (CTP) bits since late last year, and have struggled with implementing the link between a parent database table and it’s related  items stored either in one or more lookup tables, or in in-memory collections and lists.

Here’s a simple example. Imagine there is a database table containing Clients or customers of a professional services firm. Each of the Partners ‘owns’ one or more clients, in that the Partner probably brought that Client into the firm, and manages the relationship with that Client. So, in a line-of-business (LOB) application dealing with these Clients, one would like to have a combo box containing a list of the Partners, and connecting them to each Client by their unique database table identifier, but displaying the Partner human-readable name to the user. This was unbelievably easy in Microsoft Access, and relatively easy in Visual Basic 6 and .Net Windows Forms. It is not as easy in Windows Presentation Foundation (WPF) or it’s sibling Silverlight frameworks. One of the real benefits of Silverlight is the rich UI that can be developed to present data, but the deployment story is fantastic.

I spent some time on the Microsoft Silverlight.net Forums, and decided to pose a question about this UI pattern.After several days, a few folks piped in with their suggestions, but one approach in particular looked promising. I have to give a lot of credit to Luke Longley here for much of this work, but our back and forth dialog effectively solved the problem, and within a few days, it was the number one active thread on the private forum dedicated to what was then called the Alexandria framework. Luke doesn’t currently have a blog, so we agreed that I would post this when the Non-Disclosure Agreement (NDA) we both had with Microsoft was lifted. With the release to the web of Silverlight 3 Beta at MIX 2009 on Tuesday, that NDA is no longer in effect, and as thousands of people start to play with the bits (and that private Alexandria forum is now closed), I can now share what I/we learned. There won’t be a complete solution provided here (because I simply do not have the time to build one), but the code and markup examples provided here should prove adequate if you’ve worked at all with the walkthroughs provided with the beta bits. If you need to know what you need to get started with Silverlight 3, go to the Silverlight 3 link above.

So to set the stage: we have started a new Silverlight Navigation Project in Visual Studio 2008, we have added a Microsoft Entity Framework item to the server project created by this template that points to the database we are using, and that contains Client and Partner tables. So we now have Clients and Partners entities in our model. We add a DataGrid, a DataForm and a DomainDataSource to a Silverlight Page. The Xaml markup might look like this (I’ll assume you can discern where you need to add your own namespace references, etc.):

<StackPanel>
    <StackPanel>
        <ods:DomainDataSource x:Name="dds" 
              LoadMethodName="LoadClients"
              AutoLoad="True"
              LoadSize="64">
            <ods:DomainDataSource.DomainContext>
            <ds:DocketsBusiness/>
            </ods:DomainDataSource.DomainContext>
        </ods:DomainDataSource>
        <StackPanel Orientation="Horizontal" Margin="7,7,0,0">
            <StackPanel>
                <datagrid:DataGrid x:Name="dataGrid1" Width="315" Height="410" HorizontalAlignment="Left"
AutoGenerateColumns="False"> <datagrid:DataGrid.ItemsSource> <Binding ElementName="dds" Path="Data"/> </datagrid:DataGrid.ItemsSource> <datagrid:DataGrid.Columns> <datagrid:DataGridTextColumn Header="Client Name" Binding="{Binding ClientName}" /> </datagrid:DataGrid.Columns> </datagrid:DataGrid> <dataControls:DataPager x:Name="pager1" Width="315" HorizontalAlignment="Left" PageSize="16"> <dataControls:DataPager.Source> <Binding ElementName="dds" Path="Data"/> </dataControls:DataPager.Source> </dataControls:DataPager> </StackPanel> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*" /> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Margin="3,1,0,0"> <dataControls:DataForm x:Name="dataForm1" Margin="5,0,0,0" Width="480" MinHeight="390" AutoEdit="False" AutoCommit="True" VerticalAlignment="Top" CommandButtonsVisibility="All" Header="Client Details" ScrollViewer.VerticalScrollBarVisibility="Visible" CurrentItem="{Binding ElementName=dataGrid1, Path=SelectedItem}" AutoGenerateFields="False" CanUserAddItems="True" CanUserDeleteItems="True"> <dataControls:DataForm.Fields> <dataControls:DataFormFieldGroup Orientation="Vertical" > <dataControls:DataFormTextField FieldLabelContent="Client Name: "
Binding="{Binding ClientName, Mode=TwoWay }" /> <dataControls:DataFormTextField FieldLabelContent="Nick Name: "
Binding="{Binding Nickname, Mode=TwoWay }" /> </dataControls:DataFormFieldGroup> <dataControls:DataFormSeparator/> <dataControls:DataFormFieldGroup Orientation="Horizontal"> <dataControls:DataFormComboBoxField x:Name="cboPartners"
FieldLabelContent="Partner:" DisplayMemberPath="Description"
Binding="{Binding PartnerID, Mode=TwoWay }"/> <dataControls:DataFormComboBoxField x:Name="cboForms"
FieldLabelContent="Main Tax Form:" DisplayMemberPath="Description"
Binding="{Binding MainFormID, Mode=TwoWay }"/> </dataControls:DataFormFieldGroup> </dataControls:DataForm.Fields> </dataControls:DataForm> </StackPanel> <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,12,0,0"> <Button x:Name="saveButton" Width="100" Height="30" Margin="14,0,0,0"
Content="Submit Changes" Click="submitButton_Click" Visibility="Collapsed"/> </StackPanel> </Grid> </StackPanel> </StackPanel> </StackPanel>

I have removed a number of fields from this form, but from the few I have left, you may notice a second DataFormComboBoxField. The pattern I am presenting here is exactly the same. Because of a Dependency Property bug in Silverlight, the UI elements inside a DataForm are not visible as identifiable objects in the Framework (i.e., x:Name is not a Dependency Property!). There are other properties we can check, and you should be able to see the resulting code smell, but it works! We will basically iterate through the FrameworkElements in the UI tree, and find our combo boxes so that we may bind them properly (i.e., set their ItemsSource). The below C# code is what Luke Longley came up with. Simple save this as a separate class in your Silverlight project and reference it in your code-behind (or ViewModel if you’ve gotten that far). 

public static class DataFormBinding
{
    public static DataFormBoundField GetFieldByBindingPath(DataForm dataForm, string bindingPath)
    {
        Stack<DataFormField> fieldStack = new Stack<DataFormField>();
        foreach (DataFormField field in dataForm.Fields)
        {
            fieldStack.Push(field);
        }
        while (fieldStack.Count > 0)
        {
            DataFormField curField = fieldStack.Pop();
            DataFormBoundField boundField = curField as DataFormBoundField;

            if (boundField != null &&
                boundField.Binding != null &&
                boundField.Binding.Path != null &&
                boundField.Binding.Path.Path == bindingPath)
            {
                return boundField;
            }
            else
            {
                DataFormFieldGroup fieldGroup = curField as DataFormFieldGroup;

                if (fieldGroup != null)
                {
                    foreach (DataFormField field in fieldGroup.Fields)
                    {
                        fieldStack.Push(field);
                    }
                }
            }
        }
        return null;
    }
}

Of course, you’ll need to add all the appropriate namespaces (including your server project’s namespace, etc.) to the code above. Another of the side-effect we’ll need to account for is the connection between the Foreign Keys. For this, we will need some Value Converters for our Unique Identifiers. I am currently using one Value Converter pair for each Combo Box/Lookup table. There is obviously some refactoring to be done with Generics here for sure. Here is an example for the Partners Entity Combo Box binding:

public class IDToPartnerConverter : IValueConverter
{
    public IEnumerable PartnerList
    {
        get;
        set;
    }

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value != null)
        {
            int id = (int)value;
            foreach (Partners partner in this.PartnerList)
            {
                if (id == partner.PartnerID)
                {
                    return partner;
                }
            }
        }
        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        Partners partner = value as Partners;
        if (partner != null)
        {
            return partner.PartnerID;
        }
        return null;
    }
}

Now let’s do some binding! In the code-behind for the above page, we need to set up a Page.Loaded event, and then Load all of our entities, and set a Loaded event on our DomainDataSource (in my case, _DocketsBusiness), so that we can then bind the results to the various UI elements.

Add something like this in the ctor for the page:

this.Loaded += new RoutedEventHandler(ClientsPage_Loaded);

Add the ClientsPage_Loaded handler:

void ClientsPage_Loaded(object sender, RoutedEventArgs e)
{            
    LoadFromServer();
    _DocketsBusiness.Loaded += new EventHandler<System.Windows.Ria.Data.LoadedDataEventArgs>(_DocketsBusiness_Loaded);
}

Add the LoadFromServer() method:

private void LoadFromServer()
{
    _DocketsBusiness.LoadPartners();
    // other lookup entities removed for brevity
}

Finally, add the DomainDataSource Loaded handler:

void _DocketsBusiness_Loaded(object sender, System.Windows.Ria.Data.LoadedDataEventArgs e)
{
  if (_DocketsBusiness.Partners.Count > 0)
  {
      // who cut the cheese? 
DataFormComboBoxField cboPartners = DataFormBinding.GetFieldByBindingPath(dataForm1, "PartnerID")
as DataFormComboBoxField; if (cboPartners.ItemsSource == null) { IDToPartnerConverter converter = new IDToPartnerConverter(); converter.PartnerList = _DocketsBusiness.Partners; cboPartners.Binding = new Binding("PartnerID") { Mode = BindingMode.TwoWay, Converter = converter }; cboPartners.ItemsSource = _DocketsBusiness.Partners; } } }

And, like magic, we have bound lookup data! Here’s a little screen shot showing the Partner assigned to 3DTek Information Systems in a ComboBox:

SL3Lookup

I’ll be working on some refactoring as well as performance improvements as I understand more about Silverlight 3 and .Net RIA Services. Some of my lookup tables have a lot of items, but load time is not significant, and UI Combo Box dropdown time is immediate. I am sure there are limits (think ALL US Zip Codes <g>).

Again, I apologize for not providing a complete solution to play with. Drop me an email if you get stuck, and I’ll see what I can do.

 

Bob Baker

P.S. I’m going to Orlando CodeCamp on March 28, 2009. Are you?