Editing Tabular Data in WPF - Building a WPF Grid

In my last post on WPF I showed how you could use a Winforms DataGridView on a WPF form in order to edit data in a tabular style. Today I'll show you how you can customize the WPF ListView in order to edit data without having to use any Winforms controls at all.

WPF gives us the ultimate flexibility when it comes to designing our UI, it just takes learning what XAML we need to get the job done. The WPF ListView control is easily customizable using templates in a GridView mode where we can specify exactly what controls should display for each column of data for each of our rows, along with being able to style the column headers, so that we can build our very own editable grid. (I'd recommend going over the ListView How-To topics in the library especially this one for a good overview on common styles and techniques).

Getting Data on Our Form

Let's take a simple example. Create a new WPF project and drop a ListView from the toolbox onto Window1. Unlike the last example that used a Winforms DataGridView, we can't use the designer in Visual Studio 2008 to automatically data bind our data to WPF controls and set up our object instances, so we'll need to set that up manually like I've shown in this video. I'm using Visual Studio 2008 SP1 so for this example I'm going create an ADO.NET Entity Data Model against my database. I'm not going to go into the details of the Entity Framework here, I'll save that for a later post. And this is just one of many ways to access data -- remember that you can use any data source that makes sense for your application in WPF, even typed DataSets or LINQ to SQL classes.

For this example we'll just work with a standard Customer table so just right-click on the project, select ADO.NET Entity Data Model, point it at the database, and then select the Customer table. This sets up a simple model that maps directly to our database table in similar fashion as LINQ to SQL. Next we'll fill the data in our Window.Loaded event handler using a LINQ to Entities query that selects just the customers in California. Then we set those results to the Window's DataContext. We'll also want to get at the BindingListCollectionView, so that we can control the position and add and remove rows to the grid easily (which is a new SP1 feature of this class like I explained in this post). So our code that sets up our data should look something like this:

 Class Window1

    Private MyData As New MyDataEntities
    Private CustomerView As BindingListCollectionView

    Private Sub Window1_Loaded() Handles MyBase.Loaded

        Dim customersInCali = From c In MyData.Customer _
                              Where c.State = "CA" _
                              Order By c.LastName

        Me.DataContext = customersInCali
        Me.CustomerView = CType(CollectionViewSource.GetDefaultView(Me.DataContext),  _
                                BindingListCollectionView)
    End Sub

Now that we have the code to load our data we can focus on the XAML we need to create an editable grid.

ListView Simple Data Binding

So let's just get this thing displaying the list of our customers. Because we set the Window's DataContext to the list of customers we can use a special syntax to tell the ListView to pick up that same DataContext of the Window. Note that if we had a parent/child association on our objects (i.e. each of our Customer objects had a list called "Orders" for instance) we could specify the property name of the child list here. Also we'll want this ListView to pay attention to the position of the BindingListCollectionView so we need to set the IsSynchronizedWithCurrentItem property to True as well. Finally, we can specify a simple property name to display in the list via the DisplayMemberPath property, here I'm just using the LastName field.

 <ListView Name="ListView1"  
          ItemsSource="{Binding Path=''}"
          IsSynchronizedWithCurrentItem="True"
          DisplayMemberPath="LastName"/>

So now our ListView now looks like a simple Listbox, simply displaying the LastName columns in a display-only list. To make it look more like a grid we need to add a GridView to the ListView's View property. The GridView lends itself well to a grid-style table that lets you define multiple columns for the items in our data source which will display in multiple rows. Multiple GridViewColumn objects can be added to the GridView which can either automatically size to their content or we can specify a width. So instead of specifying the DisplayMemberPath property above, we're going to use a GridView, add a couple GridViewColumns, and then set up the field names we want to bind to. Let's add columns for LastName, FirstName and State columns:

 <ListView Name="ListView1"  
          ItemsSource="{Binding Path=''}"
          IsSynchronizedWithCurrentItem="True">
    <ListView.View>
        <GridView>
            <GridViewColumn DisplayMemberBinding="{Binding Path=LastName}" 
                            Header="Last Name" Width="100"/>
            <GridViewColumn DisplayMemberBinding="{Binding Path=FirstName}" 
                            Header="First Name" Width="100"/>
            <GridViewColumn DisplayMemberBinding="{Binding Path=State}" 
                            Header="State" Width="50"/>
        </GridView>
    </ListView.View>
</ListView>

Editing the Data

Now we're starting to look more like a data grid. However, by default the GridView cannot directly update the data that it displays. For this we need to add what's called a DataTemplate to the GridViewColumn's CellTemplate property. This allows us to configure exactly what the controls should be used for each column. For this example we can use TextBoxes in our DataTemplates like so:

 <ListView Name="ListView1"  
          ItemsSource="{Binding Path=''}"
          IsSynchronizedWithCurrentItem="True">
    <ListView.View>
        <GridView>
           <GridViewColumn Header="Last Name" Width="100">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBox Text="{Binding Path=LastName}" />
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
           </GridViewColumn>
           <GridViewColumn Header="First Name" Width="100">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBox Text="{Binding Path=FirstName}" />
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
           </GridViewColumn>
           <GridViewColumn Header="State" Width="50">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBox Text="{Binding Path=State}" />
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
           </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

However you'll notice that this doesn't quite line up our TextBoxes how we'd like them because they will only take up the length of the data inside. Instead, we want them to take up the same width as the column. For this we need to set the Margin on each individual TextBox and then add a property setter to the ListView's ItemContainerStyle in order to set the same HorizontalContentAlignment style for each of the rows to "Stretch".

 <ListView Name="ListView1"  
          ItemsSource="{Binding Path=''}"
          IsSynchronizedWithCurrentItem="True">
     < ListView.ItemContainerStyle ><br>        <Style TargetType ="ListViewItem"><br>            <Setter Property="HorizontalContentAlignment" Value ="Stretch" /><br>        </Style ><br>    </ListView.ItemContainerStyle > 
    <ListView.View>
        <GridView>
           <GridViewColumn Header="Last Name" Width="100">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBox Text="{Binding Path=LastName}" 
                                Margin ="-6,0,-6,0"  />
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
           </GridViewColumn>
            <GridViewColumn Header="First Name" Width="100">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBox Text="{Binding Path=FirstName}" 
                                 Margin ="-6,0,-6,0"  />
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
            <GridViewColumn Header="State" Width="50">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBox Text="{Binding Path=State}" 
                                 Margin ="-6,0,-6,0"  />
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

Now this is starting to look more like a data grid. And you'll notice now we can edit the data:

Of course you can play with the styles and style templates to get the exact effects you want but as you can see it's pretty straight forward to hook up our data binding and edit the rows using DataTemplates.

Adding, Deleting and Saving

In order to Add and Delete items we can use the BindingListCollectionView to work with our data source, in this case a simple list of customers. Using the BindingListCollectionView the adding and deleting of items of data in the list is the same no matter what the data source. However the mechanism for saving your data will vary. In my case I'm using Entity Framework so I can tell the ObjectContext to SaveChanges. So I'll add three buttons onto the Window for Add, Delete and Save. Then in the Click Event handlers we can write some code:

 Private Sub btnNew_Click() Handles btnNew.Click
    Dim customer = CType(Me.CustomerView.AddNew, Customer)
    customer.LastName = "<new>"
    Me.CustomerView.CommitNew()
End Sub

Private Sub btnDelete_Click() Handles btnDelete.Click
    If Me.CustomerView.CurrentPosition > -1 Then
        Me.CustomerView.RemoveAt(Me.CustomerView.CurrentPosition)
    End If
End Sub

Private Sub btnSave_Click() Handles btnSave.Click
    Try
        MyData.SaveChanges()
        MsgBox("Saved customers successfully.")
    Catch ex As Exception
        MsgBox(ex.ToString)
    End Try
End Sub

Run the application, make some changes and then click Save and you should see your data save properly. However you should notice that if you try to delete a row, it seems to be deleting the wrong one sometimes. This is because that even though we specified the IsSynchronizedWithCurrentItem property on the ListView, if we select a TextBox directly in the rows, the selection doesn't actually move. It only moves correctly if we click outside of the TextBoxes on the row. To fix this we can add a simple Event Handler to the ListBoxItem.GotFocus event that will force the selected item to be that of the ListBoxItem's DataContext. You can do this easily by adding an EventSetter in the ListView.ItemContainerStyle template:

 <ListView.ItemContainerStyle>
     <Style TargetType="ListViewItem">
        <Setter Property="HorizontalContentAlignment" Value="Stretch" />
         < EventSetter Event="GotFocus" Handler ="Item_GotFocus"/> 
     </Style>
</ListView.ItemContainerStyle>

Then in the code we can handle the event like so:

 Private Sub Item_GotFocus(ByVal sender As System.Object, _
                          ByVal e As System.Windows.RoutedEventArgs)

    Dim item = CType(sender, ListViewItem)
    Me.ListView1.SelectedItem = item.DataContext
End Sub

There's other handlers we could add in this manner for controlling other aspects of the grid. Take a look at the ListView Overview for more information on what you can do. I've uploaded this sample onto Code Gallery along with the last one that used a Winforms DataGridView: https://code.msdn.microsoft.com/wpftabulardata

I hope this gives you a good start into understanding how to bind data into an editable tabular-style user interface. It's pretty simple to get something basic together as you can see but if you have extremely complex needs or don't want to spend the time on building one yourself, I urge you to take a look at third-party WPF grids on the market as well as what the WPF team is building on CodePlex.

Enjoy!