Better Data Editing Features in WPF with SP1

One of the things I was really missing from WPF when I started to dig into data binding was feature consistency between the BindingListCollectionView and the Winforms BindingSource I had grown to love. The BindingListCollectionView provides the navigation, currency, filtering and sorting on the bound collection of data (or DataTable) just like the BindingSource in Winforms.

However transacted adding and removing of items to the collections was not supported. You'll notice in my WPF Forms over Data videos when I go to add or remove a row of my data I have to access the DataTable directly. This isn't really a problem when working with DataTables because they can do their own transacted editing (along with change tracking). However this is usually a necessary feature in order to work well with custom business collections implementing typical binding interfaces.

SP1 Adds New Properties/Methods to WPF's BindingListCollectionView

With the release of Visual Studio/.NET FX SP1 they've enhanced the BindingListCollectionView to include new properties and methods: CanAddNew property, CanCancelEdit property, CanRemove property, CurrentAddItem property, CurrentEditItem property, IsAddingNew property, IsEditingItem property, ItemProperties property, NewItemPlaceholderPosition property, AddNew method, CancelEdit method, CancelNew method, CommitEdit method, CommitlNew method, EditItem method, Remove method, RemoveAt method.

One thing that I want to point out here that's different is in Winforms we used to call EndEdit on the BindingSource to push all transacted changes whether they were edits or adds to the bound data source (i.e. the DataTable or your collection). In WPF there's a separate call for CommitEdit and CommitNew and you have to make sure you don't call CommitEdit if you are in the middle of an add that calls AddNew, instead you have to call CommitNew. I'm not sure why they separated this. When using DataSets I always commit the changes pretty much right away (after filling default values for instance) and rely on the DataSet's change tracking using Accept/RejectChanges.

Adding and Removing Data with the New AddNew and Remove Methods

When we want to add a new row of data to a DataTable we can now call AddNew directly on the BindingListCollectionView. For instance..

 Private OrderData As New OrdersDataSet
Private OrdersViewSource As BindingListCollectionView
 Sub New()
    ' This call is required by the Windows Form Designer.
    InitializeComponent()

    ' Add any initialization after the InitializeComponent() call.
    Me.LoadData()
    Me.DataContext = Me.OrderData.Orders
    Me.OrdersViewSource = CollectionViewSource.GetDefaultView(Me.DataContext)
End Sub

 Private Sub AddNewOrder()
    '--- Old Code ---
    'Add a new row to the collection
    'Dim order = Me.OrderData.Orders.NewOrdersRow
    'Me.OrderData.Orders.AddOrdersRow(order)
    'Up to us to update the position
    'Me.OrdersViewSource.MoveCurrentToLast()

    '--- New Code ---
    'Add a new row to the collection
    Me.OrdersViewSource.AddNew()
    'Push changes into the DataTable
    Me.OrdersViewSource.CommitNew()
End Sub

Above I'm handling all the validation and setting of default values via the DataSet partial classes so the AddNewOrder() method would remain unchanged if we were working against our own business object collections. This is what I came accustomed to in Winforms, you can easily swap out your data sources without messing with the data binding code. Keep in mind though that if you call CommitNew and the DataRow is invalid, you'll get an error when it's pushed into the DataSet -- so make sure to handle the TableNewRow event on the DataTable partial class and fill in valid default values.

Removing the current row is also very straightforward:

 Private Sub RemoveOrder()
    If Me.OrdersViewSource.CurrentPosition > -1 Then
        '--- Old Code ---
        'Dim order As OrdersDataSet.OrdersRow
        'order = CType(CType(Me.OrdersViewSource.CurrentItem, DataRowView).Row, OrdersDataSet.OrdersRow)
        'order.Delete()

        '--- New Code ---
        Me.OrdersViewSource.Remove(Me.OrdersViewSource.CurrentItem)
    End If
End SubĀ 

Working with Master-Detail Forms in WPF

These are good improvements but the BindingSource in Winforms is still easier to work with at the moment IMHO because the BindingSource is a visual component that handles the underlying CurrencyManager goo. One thing that's a pain is when you are working with master-detail binding scenarios in WPF. You need to obtain a reference to the details' BindingListCollectionView every time the detail view changes (this would be the ItemSource on a ListBox or ListView). If you've got the binding set up correctly in the XAML then the detail view changes automatically when the parent's position changes. However, if you want to AddNew into the child then you need to obtain a reference in code every time because the view is dynamic. Winforms BindingSources handle this scenario better.

Even though we need to obtain a reference every time, it's fairly straightforward:

 Private Sub AddNewDetail()
    If Me.OrdersViewSource.CurrentPosition > -1 Then
        '--- Old Code ---
        'Dim order As OrdersDataSet.OrdersRow
        'order = CType(CType(Me.OrdersViewSource.CurrentItem, DataRowView).Row, _
        '              OrdersDataSet.OrdersRow)
        'Dim detail = Me.OrderData.OrderDetail.NewOrderDetailRow
        'detail.OrderID = order.OrderID
        'Me.OrderData.OrderDetail.AddOrderDetailRow(detail)

        '--- New Code ---
        Dim detailView As BindingListCollectionView = _
                          CollectionViewSource.GetDefaultView(Me.lstDetails.ItemsSource)
        detailView.AddNew()
        'Note that the related OrderID is set for us automatically just like Winforms
        detailView.CommitNew()
    End If
End Sub

Private Sub RemoveDetail()
    If Me.OrdersViewSource.CurrentPosition > -1 Then

        Dim detailView As BindingListCollectionView = _
                          CollectionViewSource.GetDefaultView(Me.lstDetails.ItemsSource)

        If detailView.CurrentPosition > -1 Then
            '--- Old Code ---
            'Dim detail As OrdersDataSet.OrderDetailRow
            'detail = CType(CType(Me.OrderDetailsViewSource.CurrentItem, DataRowView).Row, _
            '               OrdersDataSet.OrderDetailRow)
            'detail.Delete()

            '--- New Code ---
            detailView.Remove(detailView.CurrentItem)
        End If
    End If
End Sub

By using the new AddNew and Remove methods it also seems to solve some issues I was seeing when working with LINQ to SQL generated child collections (EntitySets) as well so it's best to start taking advantage of these new methods. In a future blog post I'll take the LINQ to SQL N-Tier application we did a while back and slap a WPF Front end on it.

I'm probably not going to update the videos I've already done, but going forward I'll be using these new methods and properties where appropriate so go download SP1 ;-)

Enjoy!