Dynamic Grid View in WPF with MVVM pattern

24 May 2020

Category: Mini

Hi everyone,
This was a hard case. One would think, this use-case is so common that there must something to implement it easily. Well, not really. I don’t know if this solution is the best one, but it’s what I put together after extensive research and fiddling with many techniques. In the end, I had to create a cell template for GridView in code, which is the “least recommended way”, but I wasn’t able to find a more elegant solution. Most of the code is adapted from Tawani Anyangwe and his post on Code Project and I take no credit for this adaptation. I merely refactored Tawani’s 11 year’s old code into a more modern syntax, and removed the need to use a custom Iterator. However, I recommend you to check the above mentioned post, because the technique introduced there may be very valid in some use-cases.
I tried to show ways to style Grid created like that, so you can draw inspiration from it. It’s good to mention, that ListView Item can be styled from ListView.ItemTemplate as usual. The biggest challenge was to create grid cells and columns dynamically.
Also, I must mention using XCeed WPF toolkit WPF package in this project, very helpful.

I hope this could help somebody on their path to conquer WPF. And as usual, there is a link to the repo with the project.

Cheers!

Petr

Main Window:

<Window x:Class="GridView.View.GridViewComponentWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
        xmlns:local="clr-namespace:GridView.View"
        xmlns:uc="clr-namespace:GridViewRef.View.Controls"
        mc:Ignorable="d"
        Title="Grid View Component" Height="450" Width="867"
        SizeToContent="WidthAndHeight"
        MinHeight="300"
        MinWidth="880"
        MaxWidth="900"
        MaxHeight="700"
        >
    <Grid x:Name="TopContainer">
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>

        <Border BorderBrush="Coral" BorderThickness="1" CornerRadius="10" 
                Grid.Row="0" 
                >
            <Label VerticalAlignment="Stretch" HorizontalAlignment="Center"
               Content="{Binding Title}"
               FontSize="25"               
               />
        </Border>
        <uc:ListViewExtension Grid.Row="1"
                              MatrixSource="{Binding SourceCollection}"/>
        <Grid Grid.Row="2" HorizontalAlignment="Center">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
            
            <Label Grid.Column="0"
                   VerticalAlignment="Center"
                   FontSize="25"
                Content="Number of Columns:"/>
            <xctk:IntegerUpDown Grid.Column="1"
                                HorizontalAlignment="Center"
                                VerticalAlignment="Center"
                                Value="{Binding ColumnCount}"
                                FontSize="25"
                                Margin="20"
                                Width="70"
                                />
            <Label Grid.Column="2"
                   VerticalAlignment="Center"
                   FontSize="25"
                Content="Number of Rows:"/>
            <xctk:IntegerUpDown Grid.Column="3"
                                HorizontalAlignment="Center"
                                VerticalAlignment="Center"
                                Value="{Binding RowCount}"
                                FontSize="25"
                                Margin="20"
                                Width="70"
                                />
        </Grid>
    </Grid>
</Window>

MainWindow cs:

using GridView.ViewModel;
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace GridView.View
{
    /// <summary>
    /// Interaction logic for GridViewComponentWindow.xaml
    /// </summary>
    public partial class GridViewComponentWindow : Window
    {
        private MainVM MainVM;

        public GridViewComponentWindow()
        {
            InitializeComponent();
            MainVM = new MainVM();
            TopContainer.DataContext = MainVM;
        }
    }
}

MainVM:

using GridViewRef.Model;
using JetBrains.Annotations;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace GridView.ViewModel
{
    public class MainVM : INotifyPropertyChanged
    {
        private string title;

        public string Title
        {
            get { return title; }
            set { title = value; OnPropertyChanged(); }
        }


        private DataMatrix sourceCollection;

        public DataMatrix SourceCollection
        {
            get => sourceCollection;
            set { sourceCollection = value; OnPropertyChanged(); }
        }

        private int columnCount;

        public int ColumnCount
        {
            get { return columnCount; }
            set { columnCount = value < 1 ? 1 : value; CreateTable();  OnPropertyChanged(); }
        }

        private int rowCount;

        public int RowCount
        {
            get { return rowCount; }
            set { rowCount = value < 1 ? 1 : value; CreateTable(); OnPropertyChanged(); }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public MainVM()
        {
            Title = "Dynamic creation of a table with ViewList and GridView.";

            RowCount = ColumnCount = 1;

            CreateTable();
        }

        private DataMatrix generateData()
        {
            List<object[]> rows = new List<object[]>();
            for (int i = 0; i < RowCount; i++)
            {
                var line = new object[ColumnCount];
                for (int j = 0; j < ColumnCount; j++)
                {
                    line[j] = ($"Data Entry Line: {i+1} Entry: {j+1}");
                }
                rows.Add(line);
            }

            List<string> columns = new List<string>();
            for (int i = 0; i < ColumnCount; i++)
            {
                columns.Add($"Column {i + 1}");
            }
            return new DataMatrix() { Columns = columns, Rows = rows };
        }

        public void CreateTable()
        {
            SourceCollection = generateData();
        }

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

ControlXAML

<UserControl x:Class="GridViewRef.View.Controls.ListViewExtension"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:GridViewRef.View.Controls"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">

    <Grid>
        <ListView x:Name="MainListView" ItemsSource="{Binding MatrixSource}"
                  HorizontalAlignment="Stretch"
                  Grid.IsSharedSizeScope="True"
                  >
            <ListView.View>
                <GridView>
                    <GridView.ColumnHeaderContainerStyle>
                        <Style TargetType="GridViewColumnHeader">
                            <Setter Property="Background" Value="Black"/>
                            <Setter Property="Foreground" Value="White"/>
                        </Style>
                    </GridView.ColumnHeaderContainerStyle>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</UserControl>

Control .cs

using GridViewRef.Model;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace GridViewRef.View.Controls
{
    /// <summary>
    /// Interaction logic for ListViewExtension.xaml
    /// </summary>
    public partial class ListViewExtension : UserControl
    {
        public DataMatrix MatrixSource
        {
            get { return (DataMatrix)GetValue(MatrixSourceProperty); }
            set { SetValue(MatrixSourceProperty, value); }
        }

        public static readonly DependencyProperty MatrixSourceProperty =
            DependencyProperty.Register("MatrixSource", typeof(DataMatrix), typeof(ListViewExtension), new PropertyMetadata(OnMatrixSourceChanged));

        private static void OnMatrixSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ListViewExtension control = d as ListViewExtension;

            if (control.MainListView == null)
            {
                return;
            }

            DataMatrix dataMatrix = e.NewValue as DataMatrix;

            control.MainListView.ItemsSource = dataMatrix.Rows;
            var gridView = control.MainListView.View as System.Windows.Controls.GridView;
            int count = 0;
            gridView.Columns.Clear();
            foreach (var col in dataMatrix.Columns)
            {
                var gridViewColumn = new GridViewColumn { Header = "IM" };
                var dataTemplate = new DataTemplate();

                var gridFactory = new FrameworkElementFactory(typeof(Grid));
                var textBlockFactory = new FrameworkElementFactory(typeof(TextBlock));

                dataTemplate.VisualTree = gridFactory;

                var newBinding = new Binding($"[{count}]");
                textBlockFactory.SetBinding(TextBlock.TextProperty, newBinding);
                textBlockFactory.SetValue(ForegroundProperty, new SolidColorBrush(Colors.Red));
                textBlockFactory.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Stretch);

                gridFactory.SetValue(WidthProperty, double.NaN);
                gridFactory.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Stretch);
                gridFactory.AppendChild(textBlockFactory);

                var newHeader = new GridViewColumnHeader();
                newHeader.Content = col;
                newHeader.Width = double.NaN;

                gridViewColumn.CellTemplate = dataTemplate;
                gridViewColumn.Header = newHeader;
                gridViewColumn.Width = double.NaN;
                gridViewColumn.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Stretch);

                gridView.Columns.Add(gridViewColumn);
               
                count++;
            }
        }

        public ListViewExtension()
        {
            InitializeComponent();
        }
    }
}

Cheers!

Petr