I’m exploring round trips using Silverlight 4 RIA and SQL Server. I have a complex screen that involves chaining dependencies between Silverlight List boxes and a combo box that belies the complexity of the normalised data model that exists in the SQL database.
Firstly the Keys Combo Box is populated with the Taxonomy Keys of a Particular Author (that has logged in previously). On Choosing a Key a set of Question sets are presented in a list box. On choosing a Question Set, the Questions in that set are retrieved VIA RIA from an SQL Database.These Questions may have images associated with them which are loaded from the local disk and passed up to the database or loaded from the database. Data flows up and down through the model and as can be seen there is a great deal of synchronisation between controls that needs to be triggered on the SL Client when data changes. The controls are populated or Bound (I think is what we are calling it now) using parameterised queries for the DDSs. The interface is shown below.
Here’s an object dependency diagram for the form
Datasets (as they used to be called in ADO.NET – which I believe is still under there somewhere) or Collections based on DDSs in code or in XAML that are the result of Parameterised Queries are not updatable using Add for ICollectionView (parameter based) or AddNew for IEditableCollectionView (see forum discussion for Hint that this is a framework bug – which led me to Kyle McClellan’s Microsoft Blog which may be useful) which means you cannot add data objects directly to the DDS on the SL4 client and have the data propagate backwards through the EF to the Database. So you have to access the domain service functions directly in code behind forms. I have found few patterns to be useful when calling functions in the LinqToEntitiesDomainService to update the data in the database and then propagating the changes down to the client by refreshing the effected controls. First some background
The XAML code for the Combo Box
1: <ComboBox Name="CboKeys" ItemsSource="{Binding ElementName=AuthorKeyDomainDataSource, Path=Data}"
2: SelectedValue="{Binding ElementName=AuthorKeyDomainDataSource, Path=Data.CurrentItem.TaxKeyID}" Grid.Row="0" Grid.Column="1" Width="111"
3: HorizontalAlignment="Right" VerticalAlignment="Top" Margin="10,10,120,10" SelectionChanged="CboKeys_SelectionChanged"
4: SelectedItem="{Binding Path=Data, Mode=OneWay, ElementName=AuthorKeyDomainDataSource}"
5: SelectedValuePath="TaxKeyID" />
6:
The XAML for the linked Domain Data Source
1: <riaControls:DomainDataSource AutoLoad="True" Height="0"
2: LoadedData="AuthorkeyDomainDataSource_LoadedData"
3: Name="AuthorKeyDomainDataSource"
4: QueryName="GetTaxKeysByAuthorQuery" Width="0" Margin="69,26,69,24">
5: <riaControls:DomainDataSource.DomainContext>
6: <my:TaxFullDbDom />
7: </riaControls:DomainDataSource.DomainContext>
8: <riaControls:DomainDataSource.QueryParameters>
9: <riaControls:Parameter ParameterName="AuthorID" Value="{Binding Path=Data}" />
10: </riaControls:DomainDataSource.QueryParameters>
11: </riaControls:DomainDataSource>
Now it’s a good idea to set up a resource for a common LinqToEntitiesDomainService my:TaxFullDbDom above <my:TaxFullDbDom x:Key="TaxContext"/>
Now to the lbxMasterNodes (or question set as it will appear to the user) List Box
1: <ListBox Grid.Column="1" ItemsSource="{Binding ElementName=view_NodesByTaxKeyDomainDataSource, Path=Data}"
2: Height="100" HorizontalAlignment="Left" Margin="5,32,0,0" Name="lbxMasterNodes" VerticalAlignment="Top"
3: Width="328" Background="AntiqueWhite" Style="{StaticResource SimpleListBox}"
4: MinWidth="300" Grid.Row="1" BorderBrush="{StaticResource LightCream}">
5: <ListBox.ItemTemplate>
6: <DataTemplate>
7: <StackPanel Orientation="Horizontal">
8: <TextBlock Text="{Binding NodeID, Mode=TwoWay}" FontSize="12" Margin="5,0"></TextBlock>
9: <TextBlock Text="{Binding MasterDescription, Mode=TwoWay}" ToolTipService.Placement="Mouse"
10: ToolTipService.ToolTip="{Binding MasterDescription}"
11: Margin="5,0" FontStyle="Italic" MinWidth="200"></TextBlock>
12: </StackPanel>
13: </DataTemplate>
14: </ListBox.ItemTemplate>
15: </ListBox>
16:
and the DDS and parameter query for this list box in XAML is
1: <riaControls:DomainDataSource AutoLoad="False"
2: Height="0" LoadedData="view_NodesByTaxKeyDomainDataSource_LoadedData"
3: Name="view_NodesByTaxKeyDomainDataSource"
4: QueryName="GetNodeMasterByKeyQuery" Width="0">
5: <riaControls:DomainDataSource.DomainContext>
6: <my:TaxFullDbDom />
7: </riaControls:DomainDataSource.DomainContext>
8: <riaControls:DomainDataSource.QueryParameters>
9: <riaControls:Parameter ParameterName="Tkey" Value="{Binding ElementName=CboKeys, Path=CboKeys.SelectedValue}" />
10: </riaControls:DomainDataSource.QueryParameters>
11: </riaControls:DomainDataSource>
Now to the code patterns….
1. By (re)setting the Query parameter(s) and reloading the Domain Data Source using the Load function you can cause the Domain Data Source .
1: protected override void OnNavigatedTo(NavigationEventArgs e)
2: {
3: if ((Author)((App)Application.Current).CurrentAuthor != null)
4: {
5: Author a = (Author)((App)Application.Current).CurrentAuthor;
6: this.AuthorKeyDomainDataSource.QueryParameters[0].Value = a.AuthorID;
7: this.AuthorKeyDomainDataSource.Load();
8:
9:
10: }
11: }
if you want to retrieve the active Domain Context Object associated with the Page in code behind forms then you can get it from any of the DomainDataSources using the following lines as they all share the same Domain Context. But for good code design use the DDS that you are working with for the control you are handling.
1: TaxFullDbDom dbCon = (TaxFullDbDom)nodeDomainDataSource.DomainContext;
then you can execute functions in the Domain Service that this Domain Context Object represents
1: private void CmdnewQS_Click(object sender, RoutedEventArgs e)
2: {
3: TaxFullDbDom dbDom = (TaxFullDbDom)this.view_NodesByTaxKeyDomainDataSource.DomainContext;
4: TaxKey T = (TaxKey)CboKeys.SelectedItem;
5: dbDom.AddNewNodeMaster(T.TaxKeyID, ret =>
6: {
7: view_NodesByTaxKeyDomainDataSource.QueryParameters[0].Value = T.TaxKeyID;
8: view_NodesByTaxKeyDomainDataSource.Load();
9: }, null);
NOTE: the Asynchronous call to the Domain Service Function and we wait for it to finish before we reload the DDS).
2. The second Pattern that will update the SL client controls after an Asynchronous call is to change the selected index which causes a refresh of the underlying DDS and parameter based query . This would allow you to reposition in the ListBox/ComboBox if desired as well.
1: private void CmdDelete_Click(object sender, RoutedEventArgs e)
2: {
3: if (lbxNodes.Items.Count > 0)
4: {
5: Node CurrentNode = (Node)lbxNodes.SelectedItem;
6: TaxFullDbDom dbTax = (TaxFullDbDom)nodeDomainDataSource.DomainContext;
7: dbTax.DeleteNode(CurrentNode, OnReturn => // Commits Domain Context changes in function
8: {
9: int lastMaster = lbxMasterNodes.SelectedIndex;
10: lbxMasterNodes.SelectedIndex = -1;
11: lbxMasterNodes.SelectedIndex = lastMaster;
12: lbxNodes.SelectedIndex = lbxNodes.Items.Count - 1;
13: }, null);
14: }
15: else MessageBox.Show("No Question to Delete" );
16: }
Just to tie off here are the Domain Service Functions
1: namespace tax_sil4_net35.Web
2: {
3: public partial class TaxFullDbDom : LinqToEntitiesDomainService<db1084688_TaxonomyEntities>
4: { …
5:
6:
7:
8: public IQueryable<TaxKey> GetTaxKeysByAuthor(int AuthorID)
9: {
10: return this.ObjectContext.TaxKeys.Where(k => k.TaxKeyAuthor == AuthorID).OrderBy(k => k.TaxKeyName);
11: }
12:
13: [Invoke]
14: public void AddNewNodeMaster(int K)
15: {
16: int CurrentMax = GetMaxMasterNodeID(K);
17: this.ObjectContext.NodeMasters.AddObject(new NodeMaster() { NodeID = CurrentMax + 1, TaxKeyID = K, MasterDescription = "Question Set Description to be filled" });
18: // When you add a new MasterNode you need to add a default node and Arc
19: this.ObjectContext.Arcs.AddObject(new Arc() { ArcDescription = "Not Set", ArcLabel = "Not Set" });
20: int NextArcId = this.GetArcMax(); // have to check for nullable otherwise it will throw an error is no arcs at all in DB
21: this.ObjectContext.Nodes.AddObject(new Node() { NodeID = CurrentMax + 1, ArcID = NextArcId, TaxKeyID = K });
22: this.ObjectContext.SaveChanges();
23: }
24:
25:
26:
27: [Invoke]
28: public void InsertNodeArc(NodeMaster nMaster)
29: {
30: //Have to deal with the case were user has deleted all the questions in a question set
31: // and the case were there are Qusestions
32: this.ObjectContext.Arcs.AddObject(new Arc() { ArcDescription = "Not Set", ArcLabel = "Not Set" });
33: this.ObjectContext.SaveChanges();
34: int NextArcId = this.ObjectContext.Arcs.Max(a => a.ArcID);
35: this.ObjectContext.Nodes.AddObject(new Node() { NodeID = nMaster.NodeID, ArcID = NextArcId, TaxKeyID = nMaster.TaxKeyID });
36: this.ObjectContext.SaveChanges();
37: }
In this app the amount of data being shipped up and down and in focus is kept to a minimum. This is especially important if you are dealing with collections of images as we are in this project.
I think DDSs in XAML are good for getting a solution up and going quickly, especially as touted in all the business app examples I’ve seen on the NET to date. But when complexity arises then, as with its predecessor the Data Control, you have to turn to code behind forms. RIA and EF thrown into the works obviously complicates matters, but its getting there I think. I remember first seeing data controls and the idea of drag and drop onto a form in VB6, and thinking now that’s nice and then seeing it all dissolve when you took the idea out of the norm. Lets hope DDSs fair better in SL5?
Hope this post helps some folk struggling with the complexities of RIA and Entity framework as well as the Asynchronous nature RIA from Silverlight or provides some food for thought at least.
Any questions or comments you can post them here.