Editing Tabular Data in WPF - Building a WPF Grid

Editing Tabular Data in WPF - Building a WPF Grid

  • Comments 19

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>
        <Style TargetType="ListViewItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
        </Style>
    </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: http://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!

Leave a Comment
  • Please add 3 and 2 and type the answer here:
  • Post
  • PingBack from http://housesfunnywallpaper.cn/?p=5855

  • Sorry for the off topic post, but I just noticed that this blog looks very much like WordPress... only difference (and the reason I'm confused now) is that it's not coded in php...

  • Hi Beth,

    Not to be snobbish or mean or anything, but I noticed a typo in this post lol:

    "... focus on the XAML ..."

    Logan

    P.S. Sorry for abusing your blog today :P

  • Hi Logan,

    That's not a typo. XAML is the markup language used in WPF (Windows Presentation Foundation). Start here: http://msdn.microsoft.com/en-us/library/ms754130.aspx

    HTH,

    -B

  • Chris Cavenagh has his YouCube interactive, Jesse Liberty on Custom Controls, Pete Brown with SL TechFest

  • Hi Beth,

    Boy do I feel like a n00b now lol my bad, keep up the awesome blog :-)

  • Hey again Beth,

    Just been having a quick look at the link you gave me (can't really browse much at work) and I can't wait to get into using WPF.

    I just have one worry: I'm still trying to learn vb.NET (I'm only starting to dive into data access and the like). I'm just concerned with the possibility that by the time I've learned what I need to know as a foundation, there's something out that's replaced WPF...

    Any suggestions about the best way you think I should learn would be great.

    Logan

  • Hi Beth,

    Im using VS2005. Is it possible to delete row

    in gridview using button?

    How to do that?

  • I don't mean to siss on your parade, but you've hit almost everything that bugs me about WPF in one post.

    "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."

    Well, technically, other than UI enhancements which come from other assemblies which are (but didn't need to be) designed to work primarily with WPF, there's nothing I can see here that can't be done with WinForms. On the other hand, to use WPF, as you note, it takes learning XAML (which is large, complex, syntax-free and poorly documents) - BUT - also requires knowing traditional .Net anyway. And if you're learning .Net - you'll be learning WinForms except that with WPF, get ready to a LOT of typing just to deal with the basics (ever notice that everyone who demos WPF *types* the XAML in?)Sooo...

    And as for "ultimate flexibility", this is what class inheritence and the mighty override is for.

    "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[...]" Again, we're back into manual mode here. In WinForms, it's a three step process that's braindead easy: create a connection to the database (no coding), create a data source that's bound to the connect (no coding), drag the datasource to the form and voila - it not only creates a form, it adds a navigator to the project and wires most of it for me.

    Even the code that gets generated is simpler and less 'magic' oriented (by which I mean - everything you need is in the same language and is connected obviously - unlike XAML.)

    To me, WPF feels like something invented to make web-designers feel like programmers, which would be fine if they also designed it to make it compatible with existing technologies.

    Was there really a need to come up with an entirely new paradigm just to create flashy UI?

  • Hi Jeff,

    Why did we move from DOS entry screens to windows forms? I had the exact same arguments before I started learning WPF as well. And no-one is telling you to move. Of course most line-of-business applications do not need much more than grey forms with black labels. You don't have to move if you don't want to move. And WPF and Winforms do interoperate so you can even use both if you need to.

    And we do know that there is a huge cry for RAD designers for WPF when working with data --- *especially* from me! And that's why they are adding it to the next version of Visual Studio. I actually showed some of the drag-drop WPF data binding in one of my sessions at DevTeach and people were excited and relieved to see the similar type of RAD development we have with Windows Forms today.

    Here's a demo of the feature if you are interested:

    http://blogs.msdn.com/bethmassi/archive/2008/11/26/channel-9-interview-wpf-drag-drop-data-binding-in-visual-studio-2010.aspx

    Cheers,

    -B

  • Beth - thanks!  I was trying to figure out how to get the selected row to change when a control within it got focus.  I kept trying to capture the Click event, but that always gets handled by the control and stops bubbling.  GotFocus makes more sense though, since clicking isn't always how controls get focus!

  • hello..

    I can't call the method BindingListCollectionView.Addnew

    why?

  • Hi Iwin,

    Make sure you're using SP1: http://msdn.microsoft.com/en-us/vstudio/products/cc533448.aspx

    HTH,

    -B

  • I've got a question for you.  In this example, you have one listview and one click event.  What if your form has say 5 listviews?  Right now the only way I can get it to work is to replicate this function 5 times referring to each of the 5 listviews.  Is there a way you can find the name of the listview of the listviewitem that got clicked?  That way you only need to code one event handler for all 5 listviews.

    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

  • I found a solution to my previous post.

    http://www.codeplex.com/wpf/Wiki/View.aspx?title=Single-Click%20Editing&referringTitle=Tips%20%26%20Tricks

    Add a function to find the parent control using VisualTreeFinder.

    Private Function FindVisualParent(Of T As UIElement)(ByVal element As UIElement) As T

           Dim parent As UIElement = element

           While Not parent Is Nothing

               Dim correctlyTyped As T = TryCast(parent, T)

               If Not correctlyTyped Is Nothing Then

                   Return correctlyTyped

               End If

               parent = VisualTreeHelper.GetParent(parent)

           End While

           Return Nothing

       End Function

    then rewrite the eventhandler to use the findvisualparent function

    Private Sub Items_GotFocus(ByVal sender As Object, ByVal e As RoutedEventArgs)

           Dim item As ListViewItem = CType(sender, ListViewItem)

           Dim listview As ListView = FindVisualParent(Of ListView)(item)

           listview.SelectedItem = item.DataContext

       End Sub

Page 1 of 2 (19 items) 12