ListView with dynamic columns

Quite a while a ago I needed a ListView with columns to be determined at runtime. Using my favourite seach engine I came across an article on codeproject offering a simple enough solution. I also found a thread on stackoverflow to which I posted some remarks. Now someone asked for the whole thing – so I thought it might be a good idea to write it down.

The first thing was that changed the DataMatrix class so that rows are dictionaries mapping column names to cell object instead of relying on the order. I’m also not quite sure what the purpose of the GenericEnumerator class is – why not use the one which the collection already implements? So I got rid of that as well. Last step was to extract an interface so I didn’t have to tie the ListView to a concrete implementation

This is the result:

public interface IDataMatrix : IEnumerable
{
    List<MatrixColumn> Columns { get; set; }
}

public class DataMatrix : IDataMatrix
{
    public List<MatrixColumn> Columns { get; set; }
    public Dictionary<string, Dictionary<string, object>> Rows { get; set; }

    public DataMatrix()
    {
        Columns = new List<MatrixColumn>();
        Rows = new Dictionary<string, Dictionary<string, object>>();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
       return Rows.Values.GetEnumerator();
    }
}

public class MatrixColumn
{
    public string Name { get; set; }
}

In the codeproject article Tawani uses attached properties to add the binding functionality which is quite nice as it makes it a bit more independent. However we already have an ExtendedListView class which incorporates some other changes so I decided to integrate the matrix binding as well. The main changes are that the ColumnHeaderTemplate is copied so you can style the headers and that the display binding is to the column name instead of the index.

    public class ExtendedListView : ListView
    {
        static ExtendedListView()
        {
            ViewProperty.OverrideMetadata(typeof(ExtendedListView), new PropertyMetadata(new PropertyChangedCallback(OnViewPropertyChanged)));
        }

        private static void OnViewPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            UpdateGridView(d as ExtendedListView, (IDataMatrix)d.GetValue(MatrixSourceProperty));
        }

        public static readonly DependencyProperty MatrixSourceProperty =
            DependencyProperty.Register("MatrixSource",
                                                typeof(IDataMatrix), typeof(ExtendedListView),
                                                new FrameworkPropertyMetadata(null,
                                                                              new PropertyChangedCallback(
                                                                                  OnMatrixSourceChanged)));

        public IDataMatrix MatrixSource
        {
            get { return (IDataMatrix)GetValue(MatrixSourceProperty); }
            set { SetValue(MatrixSourceProperty, value); }
        }

        private static void OnMatrixSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var listView = d as ExtendedListView;
            var dataMatrix = e.NewValue as IDataMatrix;

            UpdateGridView(listView, dataMatrix);
        }

        private static void UpdateGridView(ExtendedListView listView, IDataMatrix dataMatrix)
        {
            if (listView == null || listView.View == null || !(listView.View is GridView) || dataMatrix == null)
                return;

            listView.ItemsSource = dataMatrix;
            var gridView = listView.View as GridView;
            gridView.Columns.Clear();
            foreach (var col in dataMatrix.Columns)
            {
                var column = new GridViewColumn
                {
                    Header = col.Name,
                    HeaderTemplate = gridView.ColumnHeaderTemplate,
                    DisplayMemberBinding = new Binding(string.Format("[{0}]", col.Name))
                };
                gridView.Columns.Add(column);
            }
        }
    }

Almost done – you can bind to a DataMatrix and the columns will be automatically generated. The next thing to do was to add customizable cell templates. That proofed a little bit tricky because somehow the data context of the cell always ended up being the whole matrix instead of an individual element. After searching a while for a solution on the web and finding nothing I decided to cheat a little bit and had a look at what’s happening under the hood with reflector. That basically showed that the ContentPresenter is setting the DataContext of the template to it’s own content (which is the matrix). So I added a wrapper to set the content of the presenter to the actual object instead of the whole matrix and then letting the presenter do its magic to pass it on to the template. It’s a bit ugly and relies on an undocumented behaviour so it might break in the future but so far (up to .Net 4.0) it still works.

        public class DataMatrixCellTemplateSelectorWrapper : DataTemplateSelector
        {
            private readonly DataTemplateSelector _ActualSelector;
            private readonly string _ColumnName;
            private Dictionary _OriginalRow;

            public DataMatrixCellTemplateSelectorWrapper(DataTemplateSelector actualSelector, string columnName)
            {
                _ActualSelector = actualSelector;
                _ColumnName = columnName;
            }

            public override DataTemplate SelectTemplate(object item, DependencyObject container)
            {
                // remember old data context
                if (item is Dictionary)
                {
                    _OriginalRow = item as Dictionary;
                }

                if (_OriginalRow == null)
                    return null;

                // get the actual cell object
                var obj = _OriginalRow[_ColumnName];

                // select the template based on the cell object
                var template = _ActualSelector.SelectTemplate(obj, container);

                // find the presenter and change the content to the cell object so that it will become
                // the data context of the template
                var presenter = Utils.GetFirstParentForChild(container);
                if (presenter != null)
                {
                    presenter.Content = obj;
                }

                return template;
            }
        }

The only bit missing is to add a CellTemplateSelector to the list view and we are done.

 public class ExtendedListView : ListView
    {
        static ExtendedListView()
        {
            ViewProperty.OverrideMetadata(typeof(ExtendedListView), new PropertyMetadata(new PropertyChangedCallback(OnViewPropertyChanged)));
        }

        private static void OnViewPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            UpdateGridView(d as ExtendedListView, (IDataMatrix)d.GetValue(MatrixSourceProperty));
        }

        public static readonly DependencyProperty MatrixSourceProperty =
            DependencyProperty.Register("MatrixSource",
                                                typeof(IDataMatrix), typeof(ExtendedListView),
                                                new FrameworkPropertyMetadata(null,
                                                                              new PropertyChangedCallback(
                                                                                  OnMatrixSourceChanged)));

        public IDataMatrix MatrixSource
        {
            get { return (IDataMatrix)GetValue(MatrixSourceProperty); }
            set { SetValue(MatrixSourceProperty, value); }
        }

        private static void OnMatrixSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var listView = d as ExtendedListView;
            var dataMatrix = e.NewValue as IDataMatrix;

            UpdateGridView(listView, dataMatrix);
        }

        public static readonly DependencyProperty CellTemplateSelectorProperty =
           DependencyProperty.Register("CellTemplateSelector",
                                               typeof(DataTemplateSelector), typeof(ExtendedListView),
                                               new FrameworkPropertyMetadata(null,
                                                                             new PropertyChangedCallback(
                                                                                 OnCellTemplateSelectorChanged)));

        public DataTemplateSelector CellTemplateSelector
        {
            get { return (DataTemplateSelector)GetValue(CellTemplateSelectorProperty); }
            set { SetValue(CellTemplateSelectorProperty, value); }
        }

        private static void OnCellTemplateSelectorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var listView = d as ExtendedListView;
            if (listView != null)
            {
                UpdateGridView(listView, listView.MatrixSource);
            }
        }

        private static void UpdateGridView(ExtendedListView listView, IDataMatrix dataMatrix)
        {
            if (listView == null || listView.View == null || !(listView.View is GridView) || dataMatrix == null)
                return;

            listView.ItemsSource = dataMatrix;
            var gridView = listView.View as GridView;
            gridView.Columns.Clear();
            foreach (var col in dataMatrix.Columns)
            {
                var column = new GridViewColumn
                {
                    Header = col.Name,
                    HeaderTemplate = gridView.ColumnHeaderTemplate
                };
                if (listView.CellTemplateSelector != null)
                {
                    column.CellTemplateSelector = new DataMatrixCellTemplateSelectorWrapper(listView.CellTemplateSelector, col.Name);
                }
                else
                {
                    column.DisplayMemberBinding = new Binding(string.Format("[{0}]", col.Name));
                }
                gridView.Columns.Add(column);
            }
        }
    }

The code is by no means perfect – there are always things which could be improved:

  • Encapsulate the DataMatrix better and give it a nicer interface
  • Instead of having to use a CellTemplateSelector it would be nice if the templates could be selected by DataType
  • Make the matrix columns observable and react to dynamic changes
  • Have a bit mor intelligent update mechanism than to rebind the whole matrix

You can download the whole solution here: DataGridListView.zip

About these ads

2 thoughts on “ListView with dynamic columns

  1. Great Approach — I was able to modify it to use Reflection and attributes to identify and display columns from the ViewModel(s). However, being a novice at WPF, I was unable to get your ExtendedListView to do grouping. I tried setting the GroupStyle in the code behind, but, either I got it wrong, or I put it in the wrong place… or both. Any ideas?

    Thanks, though — I appreciate the work you did.

    Reply
    • Thanks for the feedback. I haven’t used grouping much together with ListView so I can’t say what the problem could be. I might have a look at it when I got some spare time on my hands.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s