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 6 and 1 and type the answer here:
  • Post
  • And to think in Delphi I used to have to go to all the trouble of setting one property in a SQL component to achieve the same thing. Now I get to play with line after line of code like a real programmer. Right on! Thank goodness Microsoft outmarketed Borland to bring us these cutting edge productivity tools.

  • You can achieve a no code approach with DataSets and Winforms in Visual Studio right now. I admit I'm not showing off the benefits of using Entity Framework in this example because my mapping is super-simple. But the point was to show how you can use these frameworks now.

    Visual Studio 2010 will have built-in tools to generate the WPF controls, related data bindings, and loading of the data like you would expect in any form designer.

  • why is it that so many technical articles don't have a sample of the final product some where in it?

    I'd like to know what the final outcome would look/feel like, it doesn't have to be perfect just rough so that i don't have to implement something that in the end isn't quite what i wanted (UI wise).

  • Hi David,

    I'm not exactly sure what you mean. I have a link to a sample application that demonstrates this and other WPF/EF data binding techniques at the end of the post. Here's the direct link: http://code.msdn.microsoft.com/Project/Download/FileDownload.aspx?ProjectName=wpfdatavideos&DownloadId=6509

    Are you just looking for a screenshot of the WPF form I was talking about above?

    -B

  • In your example your have the OMSEntities context open the entire time you are working with the data.  Does this mean you have an open connection to the DB the whole time?  

  • Hi R.B. Davidson,

    Great question! And the answer is NO. Database connections are only opened for the period that the query is sent to SQL server (durring the load and then again durring the SaveChanges) and then they are closed just like you would do yourself if you were writing ADO.NET code manually. In this case Entity Framework is handling opening/closing connections and constructing the SQL statements for you. (You can control this manually if you want by accessing the ObjectContext.Connection property.)

    The ObjectContext (OMSEntities) exists here in memory because it's the thing that is tracking changes for us.

    You can read more about thist topic here: http://msdn.microsoft.com/en-us/library/bb738582.aspx

    HTH,

    -B

  • Hi Beth,

    I'm having trouble with the Master-Detail Scenario that you posted here: http://msdn.microsoft.com/en-us/vbasic/dd239277.aspx  The problem is with both Delete functions.  As soon as I hit the Save button I get an error related to a non-nullable item on the foreign key table.  I've been pulling my hair out trying to find a solution, but I can't.  The only thing that works is to allow the foreign key item to be a null value.  Which I don't want to do.  What am I doing wrong?

  • Hi C.Adams,

    In that video I'm using LINQ to SQL, not Entity Framework so it's a little trick you need to do if you want to allow deletion of just the child. Take a look at this post for an explanation and a fix:

    http://blogs.msdn.com/bethmassi/archive/2008/02/19/one-to-many-master-detail-forms-with-linq-to-sql.aspx

    HTH,

    -B

  • Hi Beth,

    I'm new to Wpf.

    I copied you're database and code and it indeed works.

    Now i'm trying to understand the code which i believe i managed for 80%. I'm teaching myself vb.net for 2 years now.

    But my question is when youre blog wasn't here how could i manage to self create this code?? This is all new or not? I believe you must have many years of programming-experience to create this kind of code.

    Is it bad to just use this code in my own application and alter it wherever needed? And not understand the code 100% ?

    Thnx in advanced!

  • I really really appreciate your EF + WPF efforts. Every piece of time you put into these samples I appreciate.

    There are many - bad - wpf + EF (or Mock) samples out there but sadly no WPF + MVVM + EF. It would be very very nice of you to share your knowledge you soon gain with us :)

  • Hi Ma'am Beth, we have implemented the master-detail WPF using C# as the code behind. It works but i have experience something strange in the master detail specially on the first record entry. The detail disappear in the UI during the entry of first record but it is already saved in the table. This happens only when i am saving first record. Any help? Thank you very much.

  • Hi Beth and thanks for your sample.

    I was wondering what happens to the connection if I place few queries one after the other, is the connection opened and closed few times or there is connection time out default that only then it's closed?

    should I handle the connection manually when executing sevaral connection in one bunch?

  • Hi Beth:

    I have watched your videos "WpfLookup_CS and Master-Detail along with others. I am having a problem in that when the combo box on the look_up form is updated the filtering doesn't work. I experienced the same thing with Master-Detail. I have looked around but can't seem to see why this is happening.

    Any direction would be appreciated. Thanks

    Roger

  • Hi Shimmy,

    Object Services controls opening and closing the connection but if you need more control take a look here: http://msdn.microsoft.com/en-us/library/bb738582.aspx

    Remember that underneath it all, ADO.NET enables connection pooling to the database so even if you close the connection object in your code, that doesn't mean the physical conneciton to the DB is closed, it means it's put back in the pool for another command to use. So typically you do not need to control the connection yourself unless you have a lot of queries/transactions happening in a row or you need more control over the connection lifetime. Remember though that you never want to keep the connection open too long for scalability reasons. For more infor on conneciton pooling see this topic:

    http://msdn.microsoft.com/en-us/library/bb399543.aspx

    HTH,

    -Beth

  • Hi Roger,

    The lookup sample is not meant as a filter, the combobox is used as a selection of values on the row itself. If you want to use the combobox as the master to filter the detail then you would hook it up to the MasterView CollectionViewSource. Something like:

    <ComboBox IsEditable="False

    IsSynchronizedWithCurrentItem="True"

    ItemsSource="{Binding Source={StaticResource MasterView}}" />

    HTH,

    -B

Page 1 of 2 (27 items) 12