Master-Detail Data Binding in WPF with Entity Framework

Master-Detail Data Binding in WPF with Entity Framework

  • Comments 27

Today I thought I would talk about a really common scenario in data applications, creating a master-details (one-to-many) data entry form. I’ve written about WPF data binding and Entity Framework a lot in the past:

Posts:

Videos:

Today I want to pull these concepts together and walk through one way to create a master-detail form in WPF using entities from the Entity Framework. Specifically, we’ll declare CollectionViewSources in our XAML like I showed here, to bind to an ObservableCollection of entities like I showed here, where the children are explicitly loaded like I showed here. Everybody got that? ;-)

Creating the Entity Data Model

First let’s create a simple Entity Data Model (EDM) that demonstrates a Master-Detail relationship. I’ll use a simple database called OMS that has Customer and Orders tables with a non-nullable foreign key set up between them on CustomerID, meaning that no Order can exist without a Customer. This relationship is inferred by Entity Framework (EF) to set up the navigation properties. Notice that there is an Orders EntityCollection on Customer.

What we want to do is build a simple form that will let us Edit, Add, and Delete Customers and their Orders. First let’s set up the WPF Data Binding in XAML.

Defining the CollectionViewSource and Data Bindings

To recap, a CollectionViewSource is a proxy for the CollectionView which manages the currency (the position) in the list of entities. It has a property called Source which can be set in our code behind. This way, we can set up CollectionVieSources in XAML for all our data lists and bind them to the corresponding controls all in XAML. Then at runtime in our code we set the Source properties and only at that time does the data pull from the database.

To define a Master-Detail relationship we define two CollectionViewSources one for the master and one for the detail collections. Then on the detail we set the Source property to the master CollectionViewSource and then specify the Path property as the name of the child collection. In our case the name of the collection on Customer is “Orders”. So we can specify the XAML like so:

<Window.Resources>
    <CollectionViewSource x:Key="MasterViewSource" />
    <CollectionViewSource x:Key="DetailsViewSource" 
                    Source="{Binding Source={StaticResource MasterViewSource}, Path='Orders'}" />
</Window.Resources>

Now as the position changes in the MasterViewSource to point to a new Customer, the DetailsViewSource will filter automatically to only those related Orders for that Customer. We can now set the rest of the data bindings on the controls on the form by setting the BindingContext of the container controls to the CollectionViewSource we want to display. For example, we can set up a StackPanel to contain the Customer fields and set the StackPanel.DataContext to the MasterViewSource. Under that we can set up a ListView to display the Orders by setting the ListView.ItemsSource to the DetailsViewSource.

<Grid>
...
<StackPanel Name="StackPanel2"
Grid.Column="1"
DataContext="{Binding Source={StaticResource MasterViewSource}}"> <TextBox Name="TextBox1" IsReadOnly="True" Text="{Binding Path=CustomerID, Mode=OneWay}"/> <TextBox Name="TextBox5"
Text="{Binding Path=LastName}"/>
...
</StackPanel> ...
<ListView Grid.Row="3" Name="ListView1" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Source={StaticResource DetailsViewSource}}"> <ListView.View> <GridView> <GridViewColumn Header="ID" Width="75"> <GridViewColumn.CellTemplate> <DataTemplate> <Label Content="{Binding Path=OrderID}" Margin="-6,0,-6,0"/> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> <GridViewColumn Header="Order Date" Width="100"> <GridViewColumn.CellTemplate> <DataTemplate> <TextBox Text="{Binding Path=OrderDate}" Margin="-6,0,-6,0"/> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> ...

The only thing we need to do now is set the Source property of the MasterViewSource in code to the collection of our Customer entities.

Defining the Master-Detail Entities in an ObservableCollection

I showed before how we can create a collection of entities that inherits from ObservableCollection in this post to make it easier to work with WPF data binding. But in that example we were only working with a simple collection of Customers and not their Orders. If you recall, the ObjectContext is what tracks changes on entities so in order for the ObjectContext to be notified that adds and deletes to the ObservableCollection need to be tracked you need to override the InsertItem and RemoveItem methods so that you can tell the ObjectContext to either add or delete the entity which will ultimately execute against the database. In the constructor I pass a reference to the ObjectContext. You can also pass in any collection of entities, say from a LINQ query, and then add them to the ObservableCollection. However, we need to make a couple modifications to our collection so that we can also track the child order entities correctly.

Adds to the Customer.Orders EntityCollection will will cause the addition of a new Order to the collection as well as the association to Customer automatically. However removing the Order from the Customer.Orders EntityCollection will only remove the association and will not attempt to actually delete the Order from the database. Instead it attempts to set the CustomerID to NULL (to remove the association from the Customer) but since we have referential integrity set up to disallow this we will get an error if we attempt to SaveChanges.

In a lot of scenarios it makes sense to just remove the association and set the foreign key to NULL in the database. But in this example we really mean to delete the Order record completely when the Order is removed from the collection. So the key is adding an event handler to the AssociationChanged event on the Orders EntityCollection that’s hanging off our Customer entity and telling the ObjectContext to explicitly delete the Order.

Public Class CustomerCollection
    Inherits ObservableCollection(Of Customer)

    Private _context As OMSEntities
    Public ReadOnly Property Context() As OMSEntities
        Get
            Return _context
        End Get
    End Property


    Sub New(ByVal customers As IEnumerable(Of Customer), ByVal context As OMSEntities)
        MyBase.New(customers)
        _context = context

        For Each c In customers
            AddHandler c.Orders.AssociationChanged, AddressOf Orders_CollectionChanged
        Next
    End Sub

    Protected Overrides Sub InsertItem(ByVal index As Integer, ByVal item As Customer)
        AddHandler item.Orders.AssociationChanged, AddressOf Orders_CollectionChanged

        'Tell the ObjectContext to start tracking this customer entity
        Me.Context.AddToCustomers(item)
        MyBase.InsertItem(index, item)
    End Sub

    Protected Overrides Sub RemoveItem(ByVal index As Integer)
        Dim customer = Me(index)
        RemoveHandler customer.Orders.AssociationChanged, AddressOf Orders_CollectionChanged

        For i = customer.Orders.Count - 1 To 0 Step -1
            'When deleting a customer, delete any orders if any exist
            Me.Context.DeleteObject(customer.Orders(i))
        Next

        'Tell the ObjectContext to delete this customer entity
        Me.Context.DeleteObject(customer)
        MyBase.RemoveItem(index)
    End Sub

    Private Sub Orders_CollectionChanged(ByVal sender As Object, _
                                         ByVal e As CollectionChangeEventArgs)
        If e.Action = CollectionChangeAction.Remove Then
            'Adding an order to a customer is handled automatically 
            ' for us but we need to tell the ObjectContext to delete the order
            ' if an order is removed from the Orders EntityCollection 
            Me.Context.DeleteObject(CType(e.Element, Order))
        End If
    End Sub
End Class

Loading the Master-Detail Entities

Finally we’re ready to write a LINQ query to load the entities into our CustomerCollection and then set that as the Source property of the MasterViewSource. In this example I’m loading the Orders explicitly by calling .Include(“Orders”) on the LINQ query which constructs a single statement to retrieve the Customer and all their Orders from the database. I discuss explicit load in this post.

We can then grab a reference to the MasterViewSource & DetailViewSource’s View property in order to add/remove items in the collections. When we’re done, we can call SaveChanges on the ObjectContext and the database will be updated.

Private db As New OMSEntities 'EF ObjectContext connects to database and tracks changes
Private CustomerData As CustomerCollection 'inherits from ObservableCollection

Private MasterViewSource As CollectionViewSource
Private DetailViewSource As CollectionViewSource

'provides currency to controls (position & movement in the collections)
Private WithEvents MasterView As ListCollectionView
Private DetailsView As BindingListCollectionView

Private Sub Window_Loaded() Handles MyBase.Loaded

    Dim query = From c In db.Customers.Include("Orders") _
                Where c.CustomerID = 1 _
                Select c

    Me.CustomerData = New CustomerCollection(query, db)

    Me.MasterViewSource = CType(Me.FindResource("MasterViewSource"), CollectionViewSource)
    Me.DetailViewSource = CType(Me.FindResource("DetailsViewSource"), CollectionViewSource)
    Me.MasterViewSource.Source = Me.CustomerData

    Me.MasterView = CType(Me.MasterViewSource.View, ListCollectionView)
    Me.DetailsView = CType(Me.DetailViewSource.View, BindingListCollectionView)
End Sub
Private Sub MasterView_CurrentChanged() Handles MasterView.CurrentChanged
    'We need to grab the new child view when the master's position changes
    Me.DetailsView = CType(Me.DetailViewSource.View, BindingListCollectionView)
End Sub

Private Sub btnSave_Click() Handles btnSave.Click
    Try
        db.SaveChanges()
        MessageBox.Show("Customer data saved.", Me.Title, MessageBoxButton.OK, MessageBoxImage.Information)
    Catch ex As Exception
        MsgBox(ex.ToString())
    End Try
End Sub

Private Sub btnDelete_Click() Handles btnDelete.Click
    If Me.MasterView.CurrentPosition > -1 Then
        Me.MasterView.RemoveAt(Me.MasterView.CurrentPosition)
    End If
End Sub

Private Sub btnAdd_Click() Handles btnAdd.Click
    Dim customer = CType(Me.MasterView.AddNew, Customer)
    Me.MasterView.CommitNew()
End Sub

Private Sub btnPrevious_Click() Handles btnPrevious.Click
    If Me.MasterView.CurrentPosition > 0 Then
        Me.MasterView.MoveCurrentToPrevious()
    End If
End Sub

Private Sub btnNext_Click() Handles btnNext.Click
    If Me.MasterView.CurrentPosition < Me.MasterView.Count - 1 Then
        Me.MasterView.MoveCurrentToNext()
    End If
End Sub

Private Sub btnAddDetail_Click() Handles btnAddDetail.Click
    Dim order = CType(Me.DetailsView.AddNew, Order)
    Me.DetailsView.CommitNew()
End Sub

Private Sub btnDeleteDetail_Click() Handles btnDeleteDetail.Click
    If Me.DetailsView.CurrentPosition > -1 Then
        Me.DetailsView.RemoveAt(Me.DetailsView.CurrentPosition)
    End If
End Sub

Now we can Add, Edit, and Delete Customer and their Orders at the same time and changes will be propagated properly to the database through Entity Framework in one call to SaveChanges. I’ve updated this complete sample application that demonstrates this as well as other aspects of WPF Data Binding with Entity Framework so have a look. 

UPDATE: Milind talks about some of the tooling improvements in Visual Studio 2010 on the VSData blog regarding building WPF forms against Entity Data Models so check it out --> WPF Data Binding: Creating a Master-Details form in Visual Studio 2010

Enjoy!

Leave a Comment
  • Please add 4 and 7 and type the answer here:
  • Post
  • Hi Beth,

    These posts and videos are a brilliant resource for EF/WPF.  It would be very useful to have some information on whether some of this may be simplified with VS 2010.

    You derive a class from ObservableCollection(Of T) in your examples; how does this fit in with the code generated automatically by the dev environment?  Do we still need to re-work it to follow that pattern in order to, for example, delete from the database as expected?

    Thanks,

    Olly

  • Hi Beth, thank you for the post. it is very helpfull. I downloaded the project and build it with VS2008, it built with success. but when i run it runtime error with "The underlying provider failed on Open." on Sub New(ByVal query As IEnumerable(Of T), ByVal context As OMSEntities)

           MyBase.New(query)

           _context = context

       End Sub

    please help.

  • Hi,

    I have the same error as GG.

    I thought I had made some mistakes so I downloaded the code but I get the same error in

    CustomerCollection.vb

    Sub New(ByVal customers As IEnumerable(Of Customer), ByVal context As OMSEntities)

          MyBase.New(customers)

           _context = context

       End Sub

  • @GG @Giancarlo,

    The only thing I can suggest is to make sure the database is accessible. Tyr to create a manual connection to it in code. It sounds like EF cannot connect the the database.

    You probably want to ask your question on the EF forum:

    http://social.msdn.microsoft.com/Forums/en-US/adodotnetentityframework/

    HTH,

    -B

  • I have another error which I can't locate.

    I run the application in debug, just after the line

    ---

    Dim orders = From o In db.Orders _

                        Where o.OrderDate >= #1/1/2009# _

                        Order By o.OrderDate Descending, _

                        o.Customer.LastName, o.Customer.FirstName _

                        Select o

    ---

    if I move the mouse over the word "orders" I get the exception error «the name of the column 'Modified' is not valid» (I have translated literally, I have the italian version of vbexpress).

    There is no mention of the column in the instruction, the name in the db is correct, I don't understand why an error and, above all, HOW the above instruction is connected to that column.

    Thanks

  • P.S.: I have tried adding the OMS db to the server using sql server management studio and it works

    BUT

    if I copy the same db to the project folder and use it in the project to create the OMSmodel etc, this error occurs.

    There is something missing because it happens only for the order query, the «Dim customers = From ...» works in both ways.

  • Hi Beth,

    I have implemented a version of your example using the POCO Template objects instead of interacting directly with EF.  This means I do not have access to the AssociationChanged events in my ObservableCollection.  

    I am not sure if this is the cause but when I try to add a new MasterRecord the DetailViewSource.View becomes a ListCollectionView instead of a BindingListCollectionView. (In the MasterView_CurrentChanged function.)  Can you provide any help?

  • Hi Tim, here are a couple explanations from the EF Team:

    1. The POCO template allows for POCO change tracking proxies. This means that when you get an entity form the database, the actual runtime type of the collection navigation properties are going to be EntityCollection<T>, and not the ObservableCollection based collections that you get when you create a new instance of an entity using “new”.

    2. EntityCollection<T> implements IListSource.GetList that returns an IBindingList and has AssociationChanged, but the default ObservableCollections you get in new instances don’t. That is proabably why sometimes you see BindingListCollectionView on existing entities and ListCollectionView on new entities.

    3. You can use ObjectContext.CreateObject<TEntity>() instead of new to obtain a proxy for a new entity. The proxy will have EntityCollection<T> instances in its collection properties.

    4. Not sure what the behavior is for AssociationChanged in added entities, but suspect it won’t help in a detached entity: don’t think you will get AssociationChanged until the new entity gets actually added to its set.

    5. While you don’t get AssociatedChanged on the collections of new instances, they are based on ObservableCollection<T>, so you could listen to the CollectionChanged event to learn about changes.

    HTH,

    -Beth

  • Dear Beth , i have gone through your c# version of masterdetail with Entity framework, the issue is with MasterView.AddNew, when there is data in the order and order details table , your app works fine , but when i deleted the data in both these table, it fails giving the follwing error because MasterView.AddNew.CanAddNew = false.

    "'AddNew' is not allowed for this view".

    can you please tell me how to overcome this with C#

  • HI Beth, thanks for your post.

    Do you have any posts/videos using a DataGrid rather than a ListView object bound to Entity Framework?  I am having problems replicating your master-detail example using a DataGrid and would appreciate being able to work through such an example.

  • hi beth,

    im a .Net newbie. My question is: is there a way to create and manage a master/details relation only by VB code?

    In my Window.Xaml I created two DataGrids, masterDataGrid and detailsDataGrid, which should be linked to TAB_MASTER and TAB_DETAILS DB tables. No binding is set in my XAML code.

    Then, in VB code, I created a DataSet, two CollectionViewSource objects, vMaster and vDetails; I loaded them with data from my tables, and bound them to my data grids by these statements:

    masterDataGrid.ItemsSource = vMaster.View

    detailsDataGrid.ItemsSource = vDetails.View

    when I run my application, data are loaded to the grids; but changing the masterDataGrid selected item, detailsDataGrid is not refreshed. If I create my relation at design time, all works. Where is the bug? Please help, thanks

  • Hi Beth, great video series on WPF/EF. I used your sample to build 2 Master Detail applications for my 1st WPF application. How would I incorporate data validation on the listview data?

Page 2 of 2 (27 items) 12