Better Data Editing Features in WPF with SP1

Better Data Editing Features in WPF with SP1

  • Comments 25

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!

Leave a Comment
  • Please add 5 and 5 and type the answer here:
  • Post
  • Hi Chio,

    The key is that you need to obtain a new reference to the DetailView anytime the CurrentItem changes in the MasterView and that the MasterView must have called CommitNew to commit the data to the data source. Without these two things happening your code will fail. Notice in the code above I'm getting a reference to the DetailView everytime I add a detail row and that I'm always calling CommitNew on the parent after I add the row.

    HTH,

    -B

  • I swear I have both the required ingredients (1) new reference to DetailView and (2) CommitNew, but the values in DetailView fields mysteriously disappear after DetailView.CommitNew!

    Private Sub btnAdd_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnAdd.Click

           Me.MasterView.AddNew()

           Me.MasterView.CommitNew() '<--- here is CommitNew

       End Sub

    Private Sub btnAddDetail_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnAddDetail.Click

           Me.DetailView = CType(Me.DetailViewSource.View, BindingListCollectionView) '<--- here is new reference to the DetailView

           Me.DetailView.AddNew()

           Me.DetailView.CurrentItem("OrderID") = Me.MasterView.CurrentItem("OrderID") 'solves Foreign Key Constraint

           Me.DetailView.CommitNew() 'CommitNew at this point makes values at DetailView fields disappear

       End Sub

    Private Sub btnSave_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnSave.Click

               taManager.UpdateAll(Me.ds)

       End Sub

    My little workaround:

    After UpdateAll, refill the Table Adapters.

    Me.ds.Clear()

    taMaster.Fill(ds.Orders)

    taDetail.Fill(ds.OrderDetail)

    Me.MasterView.MoveCurrentToLast() 'go to the added master record

    I can see the added master and detail records in MasterView and DetailView after doing this workaround.

    I suspect DetailView

    <CollectionViewSource Source="{Binding Source={StaticResource MasterView}, Path=FK_OrderDetail_Orders}" x:Key="DetailView" />

    is unable to establish relationship (FK_OrderDetail_Orders) with newly added rows in MasterView. Hence DetailView has a NullReference when MasterView CurrentItem is a newly added row (despite MasterView.CommitNew).

    The problem is solved with refill table adapters but I find it rather inefficient to perform table adapters' Fill after every MasterView CommitNew and UpdateAll.

  • Hi Chio,

    You definitely should not have to refill the tables. It still seems like the DataSet or the relation path is incorrect because you shouldn't have to specify to manually set the foreign key like you're doing.

    Make sure your DataSet is set up with a relation like shown in this video/code: http://msdn.microsoft.com/en-us/vbasic/cc138241.aspx

    Also make sure the master-detial example here runs for you: http://code.msdn.microsoft.com/wpfdatavideos/Release/ProjectReleases.aspx?ReleaseId=1234

    HTH,

    -B

  • Hi Beth,

    I watched your videos and tried many ways to work out a solution but I am still stuck. I have posted my problem on MSDN forum and hope someone can help me out. http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/4441e590-76fa-4e2c-8a0c-51bdfddbf5e3

    I wonder if it is a bug. Thanks so much for patiently guiding me through. :)

  • Hi Chio,

    It may be a problem with your how your database is assigning new primary keys and how your dataset is configured. Does the code in this sample work?: http://msdn.microsoft.com/en-us/vbasic/cc138241.aspx

    HTH,

    -B

  • Hi Beth,

    Your "SavingRelatedDataTables" sample works fine. I can perform add, update and delete with no problems.

    How I tried to troubleshoot:

    In case there is a problem with my own database and dataset, I used your OMS database from the Master Detail sample (see my comments dated December 5). I add dataset to that sample as you use LINQ to SQL.

    My problem also occurs in the OMS database. Maybe it is the way I configure dataset?

    This is how I configure dataset:

    - The SelectCommand, InsertCommand, UpdateCommand and DeleteCommand are generated by the TableAdapter Configuration Wizard.

    - Under "Advanced Options", I select "Generate Insert, Update and Delete statements", "Use optimistic concurrency" and "Refresh the data table".

    - Relation I choose "Both Relation and FK Constraint", Update and Delete rule set to "Cascade".

    -Primary key I use "AutoIncrement =True", "AutoIncrementSeed = -1" and "AutoIncrementStep = -1"

    I am not sure what I possibly could have done wrong with this configuration. :(

  • to avoid update and delete problems when updating the master row I use a multibind.

    I noticed that the dataset realtions do the job but the binding of the itemssource of the child itemscontrols to the related view don't.

    the converter recreate the child view evey time needed.

    The second binding if for updating the child view while then parentcolumn of the master row is changing.

    Dim myb As New MultiBinding()

                   Dim b1 As New Binding

                   Dim b2 As New Binding

                   b1.Source = Me.MasterGrid.mySelector

                   b1.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged

                   b1.Path = New PropertyPath("SelectedItem")

                   myb.Bindings.Add(b1)

                   b2.Source = Me.MasterGrid.mySelector

                   b2.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged

                   b2.Path = New PropertyPath("SelectedItem." & MasterField)

                   myb.Bindings.Add(b2)

                   myb.Converter = New MyChildViewConverter

                   myb.ConverterParameter = Me.MasterGrid.Name & "__" & Me.Name

                   Me.mySelector.SetBinding(ItemsControl.ItemsSourceProperty, myb)

    Public Class MyChildViewConverter

               Implements IMultiValueConverter

               Public Function Convert(ByVal value() As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IMultiValueConverter.Convert

                   Dim r As DataRowView = TryCast(value(0), DataRowView)

                   Dim c As String = parameter

                   If r Is Nothing Then Return Binding.DoNothing

                   Dim d As Object = r.CreateChildView(c)

                   Return d

               End Function

               Public Function ConvertBack(ByVal value As Object, ByVal targetType() As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack

                   Return Nothing

               End Function

           End Class

  • Hi mbm,

    Or you could just handle the MasterView.CurrentChanged event to grab a new reference to the ChildView. And it's probably better to use a couple CollectionViewSource objects in the XAML to set up the related binding.

    Thanks for the comment!

    -B

  • Hi

    I don't know if my issue is related. I also had a master detail foreign key error when inserting a new master and detail record at the same time with my dataset update and delete rules on Cascade with "Both Relation and Foreign Key Constraint" selected. Instead of running the new dataset wizard which I used for above, I used the "Add New Data Source" from the forms menu to create the dataset after which I set same rules as above and then it worked. I hope it helps.

  • Hey Beth,

    Wonderful article.

    I am in the midst of moving from Windows forms to WPF.

    It helped me a lot. Thanks.

    Vijay

Page 2 of 2 (25 items) 12