Click here to Skip to main content
15,889,216 members
Articles / Desktop Programming / WPF

Dynamic Columns in a WPF DataGrid Control (Part 2)

Rate me:
Please Sign up or sign in to vote.
4.83/5 (8 votes)
12 Nov 2015CPOL5 min read 26.5K   996   19   5
Dynamic columns in a WPF DataGrid Control (part 2), correcting an architectural constraint using the interaction request framework

Introduction

This article extends the first "Dynamic Columns in a WPF DataGrid Control (Part 1)" article. In this article, I describe how to add dynamic columns to a WPF DataGrid control. This second part focuses on an architectural constraint which I  violated in part 1. The solution is based on my previous article "ViewModel to View Interaction Request". It will use functionality from the "small application framework" which is already introduced in that article.

The next article "MVVM Data Validation" extends the application framework with a base class for data validation and shows how the values that are entered into the data grid can be validated against data rules.

Background

As mentioned in the introduction, this article is about fixing an architectural constraint. So what is broken that has to be fixed? In the original solution, I placed the dynamic columns collection in the MainViewModel to minimize code complexity.

Because of this architectural decision, I had to reference the PresentationFramework library in the ViewModel assembly. This means that a part of the user interface layer crept into the view-model layer. This is a breach of the layered architecture pattern.

Using the Code

The Code Changes

The next class diagram shows the old situation in which the view model has an observable collection containing data grid columns. The data grid column collection has been removed in the new situation. The library references have been cleaned and the PresentationCore, PresentationFramework, System.Xaml and WindowsBase references have been removed.

Image 1

The ObjectTag class has also moved to the GUI layer. This class is used to create a dependency property called 'Tag'. This property allows instances (in this case, the role row) to be tagged to the corresponding grid column.

Small Application Framework

This library is introduced in my article about ViewModel to View Interaction Requests. It describes a similar situation in which logic in the view model layer has to be decoupled from the GUI layer. The previous article described how to deal with file open and save message boxes, and here I use the same mechanism to manage the dynamic columns in the data grid. I recommend you read this article first if you are not familiar with it.

The Data Model

The data model has stayed the same.

Image 2

The View Model

In the new system, the columns are managed by the grid control. The mutation of the columns is requested by the view model via notifications to the GUI layer. There are four notification types:

  1. Add text column, used for adding the text columns to the grid (first and last name columns)
  2. Add dynamic column, used for adding new roles to the grid
  3. Change dynamic column, used for updating the role column (role name change)
  4. Delete dynamic column, used for removing the role from the grid

The DataColumnService class contains the interaction request classes that are used to pass the notification from the logic to the GUI layer.

Image 3

The MainViewModel contains the database context and the logic to initialize the grid. It subscribes to the DataSet's data events and passes the data operations events (add, remove and modify roles) on to the GUI layer, using the interaction request notifications.

The Application

The MainWindow in the Application layer has the same  functionality and implementation as the previous version. The difference to the previous solution is that the attached behavior DataGridColumnsBehavior was removed. This class was responsible for the monitoring of the roles data view, and the update of the grid when the roles changed.

Image 4

The new version uses Triggers from the Interaction library to get the problems solved. There are four triggers: One for inserting a text column and three for inserting, removing and modifying the role columns. Each trigger calls an action implementation, that performs the actual task. All actions are associated to the main window. Each action has access to the main window and its child controls, so it can modify the grid's column collection.

An additional trigger is an event trigger that subscribes to the window's load event and calls the OnLoad method of the view model. This call is used for the initialization of the grid columns.

The Business Logic

Application Initialization

The application is initialized by routing the Loaded event to the view model, calling the OnLoaded method. The first task is to initialize the standard grid columns, the first and last name columns. The second task is adding the existing role columns.

C#
public void OnLoaded()
{
    this.GenerateDefaultColumns();
    this.InitializeRolesColumns();
}

private void GenerateDefaultColumns()
{
    this.AddTextColumn("First Name", "FirstName");
    this.AddTextColumn("Last Name", "LastName");
}

private void InitializeRolesColumns()
{
    foreach (var role in this.databaseContext.DataSet.Role)
    {
        this.AddRoleColumn(role);
    }
}

private void AddTextColumn(string header, string binding)
{
    var addTextColumnNotification = new AddTextColumnNotification
    {
        Header = header,
        Binding = binding
    };
    DataColumnService.Instance.AddTextColumn.Raise(addTextColumnNotification);
}

private void AddRoleColumn(UserRoleDataSet.RoleRow role)
{
    var notification = new AddDynamicColumnNotification { Role = role };
    DataColumnService.Instance.AddDynamicColumn.Raise(notification);
}

The interaction request framework takes care of the execution of the AddTextColumnAction instance for the insertion of the first and last name columns.

C#
public class AddTextColumnAction : TriggerActionBase<AddTextColumnNotification>
{
    protected override void ExecuteAction()
    {
        var mainWindow = this.AssociatedObject as MainWindow;
        if (mainWindow != null)
        {
            mainWindow.DataGridUsers.Columns.Add(
                new DataGridTextColumn
                    {
                        Header = this.Notification.Header,
                        Binding = new Binding(this.Notification.Binding)
                    });
        }
    }
}

Insertion of a Role Column

A new role column is added in the same way as a text column. The framework calls the AddDynamicColumnAction class that creates the column, and assigns the binding. The binding consists of:

  • Converter, a value converter. It handles the assignment of the roles to a user
  • RelativeSource, is the DataGridCell containing the checkbox control
  • Path, path to the columns bound object
  • Mode, two way binding for updates to and from the grid cell control

The column is a DataGridCheckBoxColumn which is created with the header from the role, binding, and the element style. The element style prevents the display of the checkbox when the control is a new item row (see part one).

Finally, the column is added to the grid control's column list.

C#
public class AddDynamicColumnAction : TriggerActionBase<AddDynamicColumnNotification>
{
    protected override void ExecuteAction()
    {
        var resourceDictionary = 
        ResourceDictionaryResolver.GetResourceDictionary("Styles.xaml");
        var userRoleValueConverter = 
        resourceDictionary["UserRoleValueConverter"] as IValueConverter;
        var checkBoxColumnStyle = resourceDictionary["CheckBoxColumnStyle"] as Style;
        var binding = new Binding
                          {
                              Converter = userRoleValueConverter,
                              RelativeSource =
                                  new RelativeSource
                                  (RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1),
                              Path = new PropertyPath("."),
                              Mode = BindingMode.TwoWay
                          };
        var dataGridCheckBoxColumn = new DataGridCheckBoxColumn
                                         {
                                             Header = this.Notification.Role.Name,
                                             Binding = binding,
                                             IsThreeState = false,
                                             CanUserSort = false,
                                             ElementStyle = checkBoxColumnStyle,
                                         };
        ObjectTag.SetTag(dataGridCheckBoxColumn, this.Notification.Role);
        var mainWindow = this.AssociatedObject as MainWindow;
        if (mainWindow != null)
        {
            mainWindow.DataGridUsers.Columns.Add(dataGridCheckBoxColumn);
        }
    }
}

Role Assignment

The role assignment is done in the UserRoleValueConverter. The logic is the same and is explained in the previous article.

Points of Interest

One of the first reactions I usually get is: "Why bother? The old solution works, so let it be". I understand that this article has a huge software evangelism potential. But I think that an architecture with clean layers is more manageable. It is worth the extra mile that one has to go for it.

Acknowledgements

I would like to thank my colleague and friend Thomas Britschgi for his inputs and the review of the article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead Seven-Air Gebr. Meyer AG
Switzerland Switzerland
I am a senior Program Manager, working for Seven-Air Gebr. Meyer AG since September 2022.
I started off programming in C++ and the MFC library, and moved on to the .Net and C# world with .Net Framework 1.0 and further. My projects consist of standalone, client-server applications and web applications.

Comments and Discussions

 
QuestionHow can we implement Sort and Filter on this dynamically added columns? Pin
Pandya Anil16-Dec-15 4:49
Pandya Anil16-Dec-15 4:49 
AnswerRe: How can we implement Sort and Filter on this dynamically added columns? Pin
Jeroen Richters5-Jan-16 21:33
professionalJeroen Richters5-Jan-16 21:33 
Hi Anil,
Yes, it is possible to sort the grid. I left it out, because I wanted to focus on the dynamic column handling.
I will leave the filtering out. This is not a standard grid functionality, and there are many ways to solve this.
The bottom line is that I didn't find a quick filtering solution that is not too complex to explain in a couple of lines.
But on to the sorting:
As you wrote, setting the CanUserSort flag to true doesn't solve your problem.
The reason for this is the grid's ItemsSource is bound to a DataView class (The MainViewModel.Users propety).
The DataView class allows sorting, but it can do this only on the properties of the data object it contains.
The check box columns are dynamic and not a part of the data row's properties , so this won't work.
The solution is to use a ListCollectionView instead of a DataView.
This class provides the same functionality and more, especially CustomSorting which is what is wanted.
The ListCollectionView has one hook that makes matters a bit more complicated: if you want to use the grid's "insert row" functionality, then you will have to wrap the data class in a view model that has a parameterless constructor.
I assume that the ListCollectionView uses the Activator to create new instances of a data class, and that can only be done when the constructor has no parameters.
The DataRow base class constructor need a DataBuilder instance, so that won't work.

Below I will describe the steps I performed on the original project.

So lets code:
1) I added a new folder to the ViewModel project called "ViewModels"
2) Added a new class "UserViewModel" to this folder
C#
namespace ViewModel.ViewModels
{
    using DataModel;
	
    public class UserViewModel
    {
        public UserViewModel()
        {
        }

        public UserViewModel(UserRoleDataSet.UserRow userRow)
        {
            this.UserRow = userRow;
        }

        public int Id
        {
            get
            {
                return this.UserRow.Id;
            }

            set
            {
                this.UserRow.Id = value;
            }
        }

        public string FirstName
        {
            get
            {
                return this.UserRow.FirstName;
            }

            set
            {
                this.UserRow.FirstName = value;
            }
        }

        public string LastName
        {
            get
            {
                return this.UserRow.LastName;
            }

            set
            {
                this.UserRow.LastName = value;
            }
        }

        public UserRoleDataSet.UserRow UserRow { get; set; }
    }
}

3) Update the MainViewModel class:
- Add a new field. This is the collection that contains the view models:
C#
private readonly ObservableCollection<UserViewModel> userViewModels; 


- Change the Users property (you might have to add some references):
C#
public ListCollectionView Users { get; private set; }


- Add a new function to fill the users list:
C#
private void CreateViewModels()
{
	foreach (var userRow in this.databaseContext.DataSet.User)
	{
		this.userViewModels.Add(new UserViewModel(userRow));
	}
}


- Update the constructor
C#
public MainViewModel()
{
	this.userViewModels = new ObservableCollection<UserViewModel>();
	this.userViewModels.CollectionChanged += this.UserViewModelsOnCollectionChanged;
	this.Users = new ListCollectionView(this.userViewModels);

	this.SaveCommand = new SaveCommand();
	this.databaseContext = DatabaseContext.Instance;

	this.databaseContext.DataSet.Role.RoleRowChanged += this.RoleOnRowChanged;
	this.databaseContext.DataSet.Role.RoleRowDeleted += this.RoleOnRoleRowDeleted;

	this.CreateViewModels();
}


- Add the collection changed funtion. This is called when the use adds or deletes rows from the data grid. This updates the data model.
C#
private void UserViewModelsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
{
	switch (notifyCollectionChangedEventArgs.Action)
	{
		case NotifyCollectionChangedAction.Add:
			// Filter the view models that already contain a user data row object, because these are added when the model is loaded.
			foreach (var newItem in notifyCollectionChangedEventArgs.NewItems.Cast<UserViewModel>().Where(x => x.UserRow == null))
			{
				// Create a new user data row for the new user view model.
				newItem.UserRow = this.databaseContext.DataSet.User.NewUserRow();
				this.databaseContext.DataSet.User.AddUserRow(newItem.UserRow);
			}

			break;
		case NotifyCollectionChangedAction.Remove:
			// The user is deleted, so remove the user data row.
			foreach (var oldItem in notifyCollectionChangedEventArgs.OldItems.Cast<UserViewModel>())
			{
				oldItem.UserRow.Delete();
			}

			break;
		case NotifyCollectionChangedAction.Replace:
			break;
		case NotifyCollectionChangedAction.Move:
			break;
		case NotifyCollectionChangedAction.Reset:
			break;
		default:
			throw new ArgumentOutOfRangeException();
	}
}



This is the view model part, now to the view:
4) Enable the sorting on the AddDynamicColumnAction
CanUserSort = true,


5) Update the UserRoleValueConverter, it has to compare the UserViewModel class instead of the UserRoleDataSet.UserRow
- Update the Convert function:
C#
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
	var result = false;
	var dataGridCell = value as DataGridCell;
	if (dataGridCell != null)
	{
		var userViewModel = dataGridCell.DataContext as UserViewModel;
		if (userViewModel != null)
		{
			var userRow = userViewModel.UserRow;
			var roleRow = ObjectTag.GetTag(dataGridCell.Column) as UserRoleDataSet.RoleRow;

			if (userRow != null && roleRow != null)
			{
				var checkBox = dataGridCell.Content as CheckBox;
				if (checkBox != null)
				{
					if (dataGridCell.IsEditing)
					{
						checkBox.Checked += this.CheckBoxOnChecked;
						checkBox.Unchecked += this.CheckBoxOnUnchecked;
					}
					else
					{
						checkBox.Checked -= this.CheckBoxOnChecked;
						checkBox.Unchecked -= this.CheckBoxOnUnchecked;
					}
				}

				result =
					DatabaseContext.Instance.DataSet.UserRole.Any(
						x => x.UserRow == userRow && x.RoleRow == roleRow);
			}
		}
	}

	return result;
}


- Update the GetUserAndRoleFromCheckBox function:
C#
private bool GetUserAndRoleFromCheckBox(
	CheckBox checkBox, out UserRoleDataSet.UserRow user, out UserRoleDataSet.RoleRow role)
{
	user = null;
	role = null;

	var dataGridCell = ControlHelper.FindVisualParent<DataGridCell>(checkBox);
	if (dataGridCell != null && dataGridCell.IsEditing)
	{
		var userViewModel = dataGridCell.DataContext as UserViewModel;
		if (userViewModel != null)
		{
			user = userViewModel.UserRow;
			role = ObjectTag.GetTag(dataGridCell.Column) as UserRoleDataSet.RoleRow;
		}
	}

	return user != null && role != null;
}


6) Add a folder to the Application project called "Behaviors"
7) Add the class "GridSortingBehavior" to this folder. This class will be attached to the DataGrid, and it will take care of the column sorting:
C#
namespace Application.Behaviors
{
    using System.ComponentModel;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;

    using Application.Helpers;

    using DataModel;

    public static class GridSortingBehavior
    {
        public static readonly DependencyProperty UseBindingToSortProperty =
            DependencyProperty.RegisterAttached(
                "UseBindingToSort",
                typeof(bool),
                typeof(GridSortingBehavior),
                new PropertyMetadata(new PropertyChangedCallback(GridSortPropertyChanged)));

        public static void SetUseBindingToSort(DependencyObject element, bool value)
        {
            element.SetValue(UseBindingToSortProperty, value);
        }

        private static void GridSortPropertyChanged(DependencyObject elem, DependencyPropertyChangedEventArgs e)
        {
            DataGrid grid = elem as DataGrid;
            if (grid != null)
            {
                if ((bool)e.NewValue)
                {
                    grid.Sorting += GridOnSorting;
                }
                else
                {
                    grid.Sorting -= GridOnSorting;
                }
            }
        }

        private static void GridOnSorting(object sender, DataGridSortingEventArgs dataGridSortingEventArgs)
        {
            DataGridCheckBoxColumn column = dataGridSortingEventArgs.Column as DataGridCheckBoxColumn;
            if (column != null)
            {
                DataGrid grid = (DataGrid)sender;
                dataGridSortingEventArgs.Handled = true;
                ListSortDirection direction = (column.SortDirection != ListSortDirection.Ascending)
                                                  ? ListSortDirection.Ascending
                                                  : ListSortDirection.Descending;
                column.SortDirection = direction;

                var role = ObjectTag.GetTag(column) as UserRoleDataSet.RoleRow;
                var listCollectionView = CollectionViewSource.GetDefaultView(grid.ItemsSource) as ListCollectionView;
                if (listCollectionView != null)
                {
                    listCollectionView.CustomSort = new UserRoleComparer(direction, role);
                }
            }
        }
    }
}


7) Add the class "UserRoleComparer" to the "Behaviors" folder. This will compare the list items for the dynamic columns:
C#
namespace Application.Behaviors
{
    using System.Collections;
    using System.ComponentModel;
    using System.Linq;

    using DataModel;

    using ViewModel.ViewModels;

    public class UserRoleComparer : IComparer
    {
        private readonly ListSortDirection direction;

        private readonly UserRoleDataSet.RoleRow role;

        public UserRoleComparer(ListSortDirection direction, UserRoleDataSet.RoleRow role)
        {
            this.direction = direction;
            this.role = role;
        }

        public int Compare(object x, object y)
        {
            int sortResult = 0;
            var userX = x as UserViewModel;
            var userY = y as UserViewModel;
            if (userX != null && userY != null)
            {
                bool userXHasRole = userX.UserRow.GetUserRoleRows().Any(userRoleRow => userRoleRow.RoleRow == this.role);
                bool userYHasRole = userY.UserRow.GetUserRoleRows().Any(userRoleRow => userRoleRow.RoleRow == this.role);

                if (userXHasRole && userYHasRole == false)
                {
                    sortResult = 1;
                }
                else if (userXHasRole == false && userYHasRole)
                {
                    sortResult = -1;
                }

                if (this.direction == ListSortDirection.Descending)
                {
                    sortResult = -sortResult;
                }
            }

            return sortResult;
        }
    }
}


8) Finally hook up the GridSortingBehavior to the DataGrid in the MainView:
XML
DataGrid x:Name="DataGridUsers"
		  ItemsSource="{Binding Users}"
		  AutoGenerateColumns="False"
		  EnableRowVirtualization="False"
		  my:GridSortingBehavior.UseBindingToSort="True"


And that is it. Sortable dynamic columns!
Kind regards,
Jeroen
QuestionGeneral question about WPF Pin
Member 800715513-Nov-15 14:14
Member 800715513-Nov-15 14:14 
AnswerRe: General question about WPF Pin
Jeroen Richters15-Nov-15 20:51
professionalJeroen Richters15-Nov-15 20:51 
GeneralRe: General question about WPF Pin
Member 800715516-Nov-15 0:48
Member 800715516-Nov-15 0:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.