Master-Detail Data Binding in WPF with Entity Framework

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!