Every time I work with WPF, I constantly think “I hate this shit” and “why is everything so damn hard”, but once I figure it out, I realize how powerful it really is and what I can really do with it.  I remember starting out with WPF and trying to figure out how to bind data to a control took me 2 days of reading several articles and a book.

Anyway, I am currently working on a MongoDb GUI – there are none out there – and I decided to build it in WPF.  I am using the MongoDb driver by samus (link).  Displaying the data returned from a query as a table proved to be a lot harder than I thought.

Remember that the application doesn’t know anything about the domain model, so I can’t use strongly-typed objects.  This is the code to retrieve the list of user objects stored in MongoDb:

var collection = db.GetCollection<Document>(User);
var results = collection.FindAll();

This will return the results as ICursor<Document>.  Think of Document as the base type for all objects stored in MongoDb.  The actual collection of documents/objects/users is in results.Documents.  A Document implements the ICollection<KeyValuePair<string,object>>, this means that each document is basically a key/value list of all the object properties.  So a user object will be stored like this:

Key Value
FirstName John
LastName Smith
CreatedOn 2010-07-20T05:39:35.5220000Z
Account { "SubscriptionType": "paid", "CreatedOn": "2010-07-20T05:39:35.5220000Z", "ModifiedOn": "2010-07-20T05:39:35.5220000Z" }

What I want to do is to take this list of documents and display as a grid.  I want it to look something like this:

image

I am no WPF expert but after Googling and reading tons of articles for hours, here is the solution I came up with.  If you know a better one, please let me know.

Here is the XAML for the ListView

<ListView Name="lvItems"  Margin="12,12,9,12"        IsSynchronizedWithCurrentItem="True" Grid.Column="1"        ItemsSource="{Binding}" >

</ListView>

The first thing I need to do is define the grid columns:

private static GridView CreateGridViewColumns(Document doc)
{
    // Create the GridView
    GridView gv = new GridView();
    gv.AllowsColumnReorder = true;

    if(doc ==null) return gv; //return empty grid if null

    // Create the GridView Columns)
    foreach (var item in doc.Keys)
    {
        var gvc = new GridViewColumn();
        gvc.Header = item;
        gvc.Width = Double.NaN;
        gvc.CellTemplateSelector = new CustomRowDataTemplateSelector();
        gv.Columns.Add(gvc);
    }

    return gv;
}

I use the returned GridView to set the ListView’s Grid

var gridView = CreateGridViewColumns(results.Documents.FirstOrDefault());
lvItems.View = gridView;

So that takes care of setting up the grid column header.  Now, I need to bind the data.  I want to treat each document as a “Row”, so I create a custom class called CustomRow.  Some of the stuff in there won’t make sense right now, but bear with me.

internal class CustomRow
{
    private int index = 0;
    private ArrayList _list;

    public CustomRow()
    {
        _list = new ArrayList();
    }

    public object Value
    {
        get
        {
            if (index < 0) index = 0;
            if (index >= _list.Count) index = 0; //wrap around and start from beginning

            return _list[index++];
        }
    }

    public object Current
    {
        get
        {
            if (index < 0) index = 0;
            if (index >= _list.Count) index = 0; //wrap around and start from beginning
            return _list[index];
        }
    }

    public void Add(object item)
    {
        _list.Add(item);
    }
}

 

Now, all I have to do is convert each document to CustomRow:

private object ConvertToRows(IEnumerable<Document> documents)
{
    var results = new List<CustomRow>();

    foreach (var document in documents)
    {
        var row = new CustomRow();
        foreach (var field in document)
        {
            row.Add(field.Value);
        }
        results.Add(row);
    }
    return results;
}

This is the full code used to setup the ListView:

lvItems.View = CreateGridViewColumns(results.Documents.FirstOrDefault());
lvItems.DataContext = ConvertToRows(results.Documents);

Did you notice the line with the CellTemplateSelector in the CreateGridViewColumns method above?  Well, I wanted my cells to display differently based on whether they are displaying string, date, an embedded object/document or a list of other documents.  So, I created a custom CellTemplateSelector:

public class CustomRowDataTemplateSelector : DataTemplateSelector
{
    public override DataTemplate
        SelectTemplate(object item, DependencyObject container)
    {
        FrameworkElement element = container as FrameworkElement;

        if (element != null && item != null)
        {
            var row = item as CustomRow;
            if (row != null)
            {
                var cell = row.Current;

                //set template based on cell type
                if (cell is Document)
                {
                    return element.FindResource("documentCell") as DataTemplate;
                }
                if (cell is IList)
                {
                    return element.FindResource("listCell") as DataTemplate;
                }

                if (cell is DateTime)
                {
                    return element.FindResource("dateCell") as DataTemplate;
                }

                if (cell is Oid)
                    return element.FindResource("idCell") as DataTemplate;
                return element.FindResource("stringCell") as DataTemplate;
            }

        }

        return null;
    }
}

Basically, I am returning a different DataTemplate based on the object stored in the cell.  As an example, here is dateCell template defined in the App.xaml file.

<DataTemplate x:Key="dateCell">
    <StackPanel>
        <TextBlock Style="{StaticResource cellStyle}"
                    Grid.Row="0" Grid.Column="0"
                    Text="{Binding Path=Value, Converter={StaticResource cellConverter}}" />
    </StackPanel>
</DataTemplate>

This is simply a TextBlock that is bound to the Value property of the CustomRow class (will explain later).  The cellConverter is custom logic that will convert the value of the cell accordingly.  For example, date is converted to short date, an embedded object is simply converted to an “(object)” string as shown above.  Here is my converter (ConvertBack hasn’t been implemented yet):

public class CellConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null)
            return "(null)";
        if (value is Document)
            return "(object)";
        if (value is DateTime)
            return ((DateTime)value).ToShortDateString();
        if (value is IList)
            return "(list)";

        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Explanations

At first, I tried to just bind directly to the list of values in each document.  The problem with that the DataTempalte only display the first item in the collection.  So my grid looks exactly like above but all the cells were set to the value of Id.  That is why I created the CustomRow class and that is why the Value property increments the indexer.  This way everytime I pull out the value for a cell, I increment the indexer so that the next bound cell will get the next value and so on. 

Again, I don’t know if this is the best solution, so if you do know of a better one then please let me know in the comments.  It will help all my readers.  Thanks in advance.

This entry was posted on Friday, July 30th, 2010 at 11:38 am and is filed under Programming. You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.