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 SubPrivate 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!
Beth is a Program Manager on the Visual Studio Community Team at Microsoft and is responsible for producing and managing content for business application developers, driving community features and team participation onto MSDN Developer Centers (http://msdn.com), and helping make Visual Studio one of the best developer tools in the world. She also produces regular content on her blog (http://blogs.msdn.com/bethmassi), Channel 9, and a variety of other developer sites and magazines. As a community champion and a long-time member of the Microsoft developer community she also helps with the San Francisco East Bay .NET user group and is a frequent speaker at various software development events. Before Microsoft, she was a Senior Architect at a health care software product company and a Microsoft Solutions Architect MVP. Over the last decade she has worked on distributed applications and frameworks, web and Windows-based applications using Microsoft development tools in a variety of businesses. She loves teaching, hiking, mountain biking, and driving really fast.