Follow @PDSAInc WPF ListView as a DataGrid – Part 2 - Paul Sheriff's Blog for the Real World

Paul Sheriff's Blog for the Real World

This blog is to share my tips and tricks garnered over 25+ years in the IT industry

Paul's Favorites

WPF ListView as a DataGrid – Part 2

In my last blog post I showed you how to create GridViewColumn objects on the fly from the meta-data in a DataTable. By doing this you can create columns for a ListView at runtime instead of having to pre-define each ListView for each different DataTable. Well, many of us use collections of our classes and it would be nice to be able to do the same thing for our collection classes as well. This blog post will show you one approach for using collection classes as the source of the data for your ListView.
 

Figure 1: A List of Data using a ListView

Load Property Names
You could use reflection to gather the property names in your class, however there are two things wrong with this approach. First, reflection is too slow, and second you may not want to display all your properties from your class in the ListView.

Instead of reflection you could just create your own custom collection class of PropertyHeader objects. Each PropertyHeader object will contain a property name and a header text value at a minimum. You could add a width property if you wanted as well. All you need to do is to create a collection of property header objects where each object represents one column in your ListView. Below is a simple example:

PropertyHeaders coll = new PropertyHeaders();

coll.Add(new PropertyHeader("ProductId", "Product ID"));
coll.Add(new PropertyHeader("ProductName", "Product Name"));
coll.Add(new PropertyHeader("Price", "Price"));

Once you have this collection created, you could pass this collection to a method that would create the GridViewColumn objects based on the information in this collection. Below is the full code for the PropertyHeader class. Besides the PropertyName and Header properties, there is a constructor that will allow you to set both properties when the object is created.

C#
public class PropertyHeader
{
  public PropertyHeader()
  {
  }

  public PropertyHeader(string propertyName, string headerText)
  {
    PropertyName = propertyName;
    HeaderText = headerText;
  }

  public string PropertyName { get; set; }
  public string HeaderText { get; set; }
}

VB.NET
Public Class PropertyHeader
  Public Sub New()
  End Sub

  Public Sub New(ByVal propName As String, ByVal header As String)
    PropertyName = propName
    HeaderText = header
  End Sub

  Private mPropertyName As String
  Private mHeaderText As String

  Public Property PropertyName() As String
    Get
      Return mPropertyName
    End Get
    Set(ByVal value As String)
      mPropertyName = value
    End Set
  End Property

  Public Property HeaderText() As String
    Get
      Return mHeaderText
    End Get
    Set(ByVal value As String)
      mHeaderText = value
    End Set
  End Property
End Class

You can use a Generic List class to create a collection of PropertyHeader objects as shown in the following code.

C#
public class PropertyHeaders : List<PropertyHeader>
{
}

VB.NET
Public Class PropertyHeaders
  Inherits List(Of PropertyHeader)
End Class

Create Property Header Objects

You need to create a method somewhere that will create and return a collection of PropertyHeader objects that will represent the columns you wish to add to your ListView prior to binding your collection class to that ListView. Below is a sample method called GetProperties that builds a list of PropertyHeader objects with properties and headers for a Product object.

C#
public PropertyHeaders GetProperties()
{
  PropertyHeaders coll = new PropertyHeaders();

  coll.Add(new PropertyHeader("ProductId", "Product ID"));
  coll.Add(new PropertyHeader("ProductName", "Product Name"));
  coll.Add(new PropertyHeader("Price", "Price"));

  return coll;
}

VB.NET
Public Function GetProperties() As PropertyHeaders
  Dim coll As New PropertyHeaders()

  coll.Add(New PropertyHeader("ProductId", "Product ID"))
  coll.Add(New PropertyHeader("ProductName", "Product Name"))
  coll.Add(New PropertyHeader("Price", "Price"))

  Return coll
End Function

WPFListViewCommon Class

Now that you have a collection of PropertyHeader objects you need a method that will create a GridView and a collection of GridViewColumn objects based on this PropertyHeader collection. Below is a static/Shared method that you might put into a class called WPFListViewCommon.

C#
public static GridView CreateGridViewColumns(
  PropertyHeaders properties)
{
  GridView gv;
  GridViewColumn gvc;

  // Create the GridView
  gv = new GridView();
  gv.AllowsColumnReorder = true;

  // Create the GridView Columns
  foreach (PropertyHeader item in properties)
  {
    gvc = new GridViewColumn();
    gvc.DisplayMemberBinding = new Binding(item.PropertyName);
    gvc.Header = item.HeaderText;
    gvc.Width = Double.NaN;
    gv.Columns.Add(gvc);
  }

  return gv;
}

VB.NET
Public Shared Function CreateGridViewColumns( _
   ByVal properties As PropertyHeaders) As GridView
  Dim gv As GridView
  Dim gvc As GridViewColumn

  ' Create the GridView
  gv = New GridView()
  gv.AllowsColumnReorder = True

  ' Create the GridView Columns
  For Each item As PropertyHeader In properties
    gvc = New GridViewColumn()
    gvc.DisplayMemberBinding = New Binding(item.PropertyName)
    gvc.Header = item.HeaderText
    gvc.Width = [Double].NaN
    gv.Columns.Add(gvc)
  Next

  Return gv
End Function

Build the Product Screen

To build the window shown in Figure 1, you might write code like the following:

C#
private void CollectionSample()
{
  Product prod = new Product();

  // Setup the GridView Columns
  lstData.View = WPFListViewCommon.CreateGridViewColumns(
       prod.GetProperties());
  lstData.DataContext = prod.GetProducts();
}

VB.NET
Private Sub CollectionSample()
  Dim prod As New Product()

  ' Setup the GridView Columns
  lstData.View = WPFListViewCommon.CreateGridViewColumns( _
       prod.GetProperties())
  lstData.DataContext = prod.GetProducts()
End Sub

The Product class contains a method called GetProperties that returns a PropertyHeaders collection. You pass this collection to the WPFListViewCommon’s CreateGridViewColumns method and it will create a GridView for the ListView. When you then feed the DataContext property of the ListView the Product collection the appropriate columns have already been created and data bound.

Summary

In this blog you learned how to create a ListView that acts like a DataGrid using a collection class. While it does take a little code to do this, it is an alternative to creating each GridViewColumn in XAML. This gives you a lot of flexibility. You could even read in the property names and header text from an XML file for a truly configurable ListView.

NOTE: You can download the complete sample code (in both VB and C#) at my website. http://www.pdsa.com/downloads. Choose Tips & Tricks, then "WPF ListView as a DataGrid – Part 2" from the drop-down.

Good Luck with your Coding,
Paul Sheriff

** SPECIAL OFFER FOR MY BLOG READERS **
Visit http://www.pdsa.com/Event/Blog for a free eBook on "Fundamentals of N-Tier".

 

Posted: Mar 29 2010, 11:04 AM by psheriff | with 3 comment(s)
Filed under: ,

Comments

Richard said:

I am looking for all-purpose routines like the one you have described to make code reuse much easier.  Your approach looks relatively straight forward and adaptable, which is what I want.

I can follow your article as far defining a set of methods for serving out a set of properties of my custom class and loading them into a listview along with column headers.  Where I am not clear is that you state that this design is meant to work with collection classes, which I gather is importatant to the step where you make the class a dataContext, but you do not give a description or example of a collection class and define the key features it needs to have to act as a dataContext.

Looking around the web I found articles that seem to indicate that a collection class should inherit from one of the available collections in .NET, such as a List or a BaseCollection, or implement certain collection interfaces, such as IEnumeration.  Can you provide any references to websites that define how best to design a collection class that works with your code, or can you give a more specific example of what the Products class implementation might actually look like and point out it's key features.

Also, it is important to me that the classes I create will be able to create listviews that can be sorted and subsorted on multiple properties, with each property having its own sort order, and then be able to refresh the listview.  The sort needs to let code external to the class or the user with the help of my interface define the array of property/sort orders pairs that will be used to sort and subsort the list at runtime.  I found an example that does what I need for the sorting, but it uses reflection rather than a collection class:

blog.josh420.com/.../sorting-generic-list-dynamically-in-vbnet.aspx

I would think this same kind of sorting functionality could be implemented with a collection class without having to resort to reflection. Ideally, I would want to minimize and consolidate the portions of code I would have to customize to make the sort work with each new collection class.  Any help you can provide on that would be appreciated.

Thanks for your article.  It has improved my understanding of WPF and contributes to the methods I can apply that will have a high degree of reusability.

# March 30, 2010 2:20 AM

psheriff said:

Richard,

Did you download the sample code? This has the collection class in it. I generally inherit from List<T> for my collection classes. This is a nice all-purpose collection class that I can turn into anything I want such as an ObservableCollection which works great with WPF.

I am sure you could modify my PropertyHeader to add an extra property to store your sort order.

Hope this helps.

Paul

# March 30, 2010 10:21 AM

Richard said:

Paul:

I downloaded your sample code and it helped me to better understand how this works.  Based on your suggestion and some ideas I got from this website:

www.switchonthecode.com/.../wpf-tutorial-using-the-listview-part-2-sorting

I decided to add two new properties to the PropertyHeader class that are optional parameters in class constructor to help me do custom sorting.  They are "DoSort" that holds a boolean of whether or not the property should to be sorted and "PropertySortDirection" which holds a ListSortDirection enumeration from the Systems.ComponentModel namespace (which cannot represent an unsorted value, which is why I added the DoSort property).

The additional code I added to the PropertyHeader class are:

Imports System.ComponentModel

Public Class PropertyHeader

' ... original code to the class field variables

   Private mSortDirection As ListSortDirection

   Private mDoSort As Boolean

' replacement constructor adding optional parameters

  Public Sub New(ByVal propName As String, ByVal header As String, Optional ByVal Sort As Boolean = False, Optional ByVal propSortOrder As ListSortDirection = ListSortDirection.Ascending)

       PropertyName = propName

       HeaderText = header

       PropertySortDirection = propSortOrder

       DoSort = Sort

   End Sub

   Private mPropertyName As String

   Private mHeaderText As String

   Private mDoSort As Boolean

   Private mSortDirection As ListSortDirection

' ... original code to the end of the Property declarations

   Public Property DoSort() As Boolean

       Get

           Return mDoSort

       End Get

       Set(ByVal value As Boolean)

           mDoSort = value

       End Set

   End Property

   Public Property PropertySortDirection() As ListSortDirection

       Get

           Return mSortDirection

       End Get

       Set(ByVal value As ListSortDirection)

           mSortDirection = value

       End Set

   End Property

End Class

Then I modified the GetProperties Function in the Products class to set some proerties to be sorted in varying directions:

#Region "GetProperties Method"

   Public Function GetProperties() As PropertyHeaders

       Dim coll As New PropertyHeaders()

       ' default unsorted property

       coll.Add(New PropertyHeader("ProductId", "Product ID"))

       ' sorted in default ascending order

       coll.Add(New PropertyHeader("ProductName", "Product Name", True))

       ' sorted with non-default descending order

       coll.Add(New PropertyHeader("Price", "Price", True, ListSortDirection.Descending))

       Return coll

   End Function

#End Region

Next to demonstrate that the sort works and that it is possible to sort multiple fields that are not necessarily in column order I modified CollectionSample code the winMain.xaml.vb file as follows:

   Private Sub CollectionSample()

       Dim prod As New Product()

       ' Setup the GridView Columns

       lstData.View = WPFListViewCommon.CreateGridViewColumns(prod.GetProperties())

       lstData.DataContext = prod.GetProducts(GetCurrentDirectory() & "\Xml\Product.xml")

       ' added to sort the listview by Price and then by ProductName

       ' create a PropertyHeaders instance

       Dim propHeaders As PropertyHeaders = prod.GetProperties

       ' first sort by Price, which was set descending in GetProperties

       lstData.Items.SortDescriptions.Add(New SortDescription(propHeaders.Item(2).PropertyName, propHeaders.Item(2).PropertySortDirection))

       ' next sort ny Product Name, which is ascending in GetProperties

       lstData.Items.SortDescriptions.Add(New SortDescription(propHeaders.Item(1).PropertyName, propHeaders.Item(1).PropertySortDirection))

   End Sub

The above implementation is hard coded just to demonstrate that it works.  I ignored the DoSort value in the code above, but could have used it if I just wanted to loop through the properties as retuned by GetProperties to determine if a property should be sorted or not.  Any time I want to restore the original order that was loaded from the file, I just clear the ListView's SortDirections as follows:

lstData.Items.SortDescriptions.clear()

Now that I can control this behavior of the GridView within the ListView, I definitely think I will be using your approach for building my list interfaces.   I should be able to develop various sorting algorithms and interface designs that interact with the underlying list in the PropertyHeaders class to create any sorting arrangement that I want.

Thanks for your code and your suggestions.

Rich

# March 31, 2010 2:37 AM
Leave a Comment

(required) 

(required) 

(optional)

(required)