Editing Tabular Data in WPF Using the Winforms DataGridView

A pretty common requirement of any business application is to be able to edit data in a "spreadsheet" or tabular style. Usually we use some sort of data grid to do this. When building WPF applications in Visual Studio 2008 you'll notice right away that there is no such control available in the toolbox. Using WPF puts you in complete control of your UI, which is awesome, but in most cases you just need an "out-of-the-box" control for handling data editing like a grid. The WPF team is working on a data grid control which you can check out but in the meantime there are a couple things you can do besides buying a third-party data grid control. In this first post I'll discuss how you can use the Windows Forms DataGridView, in the next post I'll show you how you can "roll your own".

Using WindowsFormsHost

One option is to use the Winforms DataGridView control on your WPF windows. One of the controls available in your WPF toolbox is called a WindowsFormsHost. This control allows you to display any Windows.Forms.Control, including the DataGridView on a WPF window. An easy way to set this up is to create a Winforms UserControl with your data-bound DataGridView on it using the standard technique via the Data Sources window.

For instance, create a new Windows Forms UserControl (Project -> Add New Item -> Under Windows Forms select User Control). Then connect to your data source via the Data Sources window like normal and drag the table you want to edit onto the UserControl. The designer will automatically create the BindingSource and any other components you need. If you are working with DataSets this will include the TableAdapter and TableAdapterManager as well. This process also adds the usual Winforms BindingNavigator and a ToolStrip onto the top of the control. You can remove that if you're planing on creating your own WPF controls for this. I also like setting the BackgroundColor of the DataGridView to the system color "Control" so it looks more WPF-ish ;-)

I named my Winforms UserControl MyGridControl. You'll probably want to add Public methods to Fill and Save the data in the UserControl but for this example we'll interact with the data components on the WPF window directly (TableAdapter, TableAdapterManager and DataSet in this case). Since the default scope is Friend for these components this works for now.

Now we can go back to our WPF window and drag a WindowsFormsHost from the toolbox onto the design surface.

Now we need to set the WindowsFormsHost's Child property to our WinForms UserControl. You cannot do this through the designer so we have to write a couple lines of code (you won't see any of your Winforms components when you have the WPF designer open.) Instead just double-click on the window to add a Window_Loaded event handler. Then just set the Child property of the WindowsFormsHost to a new instance of the UserControl:

 Class Window1 
    Dim WithEvents UC As New MyGridControl

    Private Sub Window1_Loaded() Handles MyBase.Loaded
        Me.UC.CustomerTableAdapter.Fill(Me.UC.CustomerDataSet.Customer)

        Me.WindowsFormsHost1.Child = Me.UC
    End Sub
End Class

Notice that I'm simply filling the CustomerDataSet from here by accessing the TableAdapter directly. But typically you would want to encapsulate this in a public Fill method on the UserControl itself. Okay so now when you run the application, you will see the DataGridView filled with data. But what if we want this data to interact with other controls (WPF controls that is) on our Window?

Interacting with Data Across Winforms and WPF Controls

So now we want some WPF buttons on our window for New and Delete as well as Save. The easiest thing to do in this case is interact directly with the UserControl's BindingSource.

 Private Sub btnNew_Click() Handles btnNew.Click
    With Me.UC.CustomerBindingSource
        .AddNew()
        'Data must be valid before this call (implement validation on the DataSet)
        .EndEdit()
    End With
End Sub

Private Sub btnDelete_Click() Handles btnDelete.Click
    With Me.UC.CustomerBindingSource
        If .Position > -1 Then
            .RemoveAt(.Position)
        End If
    End With
End Sub

Private Sub btnSave_Click() Handles btnSave.Click
    With Me.UC
        .Validate()
        .CustomerBindingSource.EndEdit()
        .TableAdapterManager.UpdateAll(.CustomerDataSet)
    End With
End Sub

When we run the form again, clicking on the New and Delete buttons puts the DataGridView into the correct row selection and state. But what if you want to also display information from rows here in other WPF controls on the form as well? For instance, what if we also want to data bind some WPF TextBoxes , how do we coordinate what's displayed in the TextBoxes with the selection in the DataGridView?

Like I've discussed before, WPF manages currency through what's called a CollectionView. The BindingListCollectionView gets created when binding WPF controls to BindingList collections or DataTables. We just need to coordinate the BindingSource.Position with the BindingListCollectionView.CurrentPosition. First let's set up a couple data bound TextBoxes in our XAML:

 <TextBox 
    Name="txtFirst"
    Text="{Binding Path=FirstName}"
    Height="28"  Width="150" Margin="2" HorizontalAlignment="Left" />
<TextBox 
    Name="txtLast" 
    Text="{Binding Path=LastName}"
    Height="28" Width="150" Margin="2" HorizontalAlignment="Left" />
<TextBox 
    Name="txtCity" 
    Text="{Binding Path=City}"
    Height="28" Width="150" Margin="2" HorizontalAlignment="Left" />

Then in our Window_Loaded method we can set the window's DataContext property to the Customer table and then we can grab a reference to the BindingListCollectionView. So our code now looks like this:

 Class Window1 

    Dim WithEvents UC As New MyGridControl
    Dim View As BindingListCollectionView

    Private Sub Window1_Loaded() Handles MyBase.Loaded
        Me.UC.CustomerTableAdapter.Fill(Me.UC.CustomerDataSet.Customer)
        Me.WindowsFormsHost1.Child = Me.UC

        Me.DataContext = Me.UC.CustomerDataSet.Customer
        Me.View = CType(CollectionViewSource.GetDefaultView(Me.DataContext), BindingListCollectionView)
    End Sub

The easiest way to sync the position is to have the UserControl raise an event when the BindingSource current item changes and pass the new position to a handler on our WPF window. So back in the UserControl let's write some code to do this. I'm first going to create an EventArgs class that I can use to store the position called PositionChangedEventArgs:

 Public Class PositionChangedEventArgs
    Inherits EventArgs

    Sub New(ByVal position As Integer)
        _position = position
    End Sub

    Private _position As Integer
    Public Property Position() As Integer
        Get
            Return _position
        End Get
        Set(ByVal value As Integer)
            _position = value
        End Set
    End Property

End Class

Then in the UserControl we declare a public event and pass this event argument. We're just handling the BindingSource.CurrentChanged event and raising our own with the data that we need to send (the position).

 Public Class MyGridControl

    Public Event PositionChanged(ByVal sender As Object, ByVal e As PositionChangedEventArgs)

    Private Sub OnPositionChanged(ByVal position As Integer)
        RaiseEvent PositionChanged(Me, New PositionChangedEventArgs(position))
    End Sub

    Private Sub CustomerBindingSource_CurrentChanged() Handles CustomerBindingSource.CurrentChanged
        Me.OnPositionChanged(Me.CustomerBindingSource.Position)
    End Sub

End Class

So now anytime the user clicks on a row in the DataGridView this event will fire. Back in the WPF window you'll notice that I declared the UC variable WithEvents which mean we can add a declarative event handler using the Handles clause (or just drop down the Class Name DropDown at the top of the editor, select the UC variable and then in the Method Name DropDown you'll see our PositionChanged event). In the handler just set the CurrentPosition of the BindingListCollectionView to the Position sent in the PositionEventArgs parameter.

 Private Sub UC_PositionChanged(ByVal sender As Object, _
                               ByVal e As PositionChangedEventArgs) _
                               Handles UC.PositionChanged

    If Me.View IsNot Nothing Then
        Me.View.MoveCurrentToPosition(e.Position)
    End If
End Sub

You'll first want to make sure that the View reference is not nothing (null) before trying to set it. This is because this event will fire when the data is filled. Now when you run the form you will see the coordination between the Winforms control and the WPF controls.

I uploaded the above sample onto Code Gallery here:https://code.msdn.microsoft.com/wpftabulardata. In the next post I'll show a way to create your own basic grid using the WPF ListView.

Enjoy!