In this article, you will learn about WPF datagrid with cells having defined fixed size but number of rows and columns updated dynamically in order to fill all available space.
Introduction
The post is devoted to the WPF datagrid
with cells that have defined fixed size but number of rows and columns is updated dynamically in order to fill all available space. For example, such grid could be used in games at infinite 2D field or implementation of cellular automaton. In the previous post, WPF data grid is considered such that it has dynamically defined number of rows and columns but all cells have the same size.
Features
The application demonstrates the following features:
- All cells has fixed width and height
- Size of cells could be changed at run-time
- Number of rows and columns are defined by user control size
- Grid occupies as much space as possible
- Input click switches the state of the cell
- Asynchronous method of adding/deleting cells
- Resize timer that prevents too frequent cell updating
- Preserve cell states
- Using of dependency container
- Logging
Background
The solution uses C#6, .NET 4.6.1, WPF with MVVM pattern, NuGet packages Unity and Ikc5.TypeLibrary.
Solution
WPF Application
WPF application is done in MVVM pattern with one main window. Dynamic grid is implemented as user control that contains DataGrid
control bound to observable collection of collections of cell view models. As was mentioned above, the code of this post is based on code from the previous post, so here, we focus on new or changed code.
View model of dynamic data grid contains cell, view and grid sizes, data model for cell set, and collection of collections of cell view models. View size properties are bound to actual size of data grid control. Actually, it is not a clear approach from the point of MVVM pattern, as view model should know nothing about view, but it is realized in an accurate way via binding and attached properties. Grid size, i.e., number of rows and columns, is calculated as view size divided by cell size. As number of rows and columns are integers, real size of cells on the view could not equal to values of cell width and height.
After control size is changed and number of rows and columns of grid are calculated, cell set is recreated, but state of cells are preserved. Then collection of cell view models is updated by asynchronous method. Method analyses necessary changes and removes or adds rows and removes or adds cell view models to rows. Asynchronous method allows to keep application responsible, and using cancellation token allows to cancel updating if control size is changed again.
Dynamic Grid Control
Dynamic grid view model implements IDynamicGridViewModel
interface that has size's properties, data model of cell set, observable collection of collections of cell view models, and several color properties:
public interface IDynamicGridViewModel
{
int ViewWidth { get; set; }
int ViewHeight { get; set; }
int CellWidth { get; set; }
int CellHeight { get; set; }
int GridWidth { get; }
int GridHeight { get; }
CellSet CellSet { get; }
ObservableCollection<ObservableCollection<ICellViewModel>>
Cells { get; }
Color StartColor { get; set; }
Color FinishColor { get; set; }
Color BorderColor { get; set; }
}
View width and height are bound to actual size of data grid control by attached properties (code is taken from this Stackoverflow's question):
attached:SizeObserver.Observe="True"
attached:SizeObserver.ObservedWidth="{Binding ViewWidth, Mode=OneWayToSource}"
attached:SizeObserver.ObservedHeight="{Binding ViewHeight, Mode=OneWayToSource}"
Resize Timer
There is an issue with binding to view size - as bindings are executed in single thread, new values of view width and height come in different moments. It means that it is necessary to wait for another one. In addition, in order to prevent too frequent changes of grid sizes if user are resizing window slowly, timer is used in application. The timer is created in the constructor and starts or restarts each time one view height or view width are changed.
public DynamicGridViewModel(ILogger logger)
{
_resizeTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(100),
};
_resizeTimer.Tick += ResizeTimerTick;
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (string.Equals(propertyName, nameof(ViewHeight),
StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(propertyName, nameof(ViewWidth),
StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(propertyName, nameof(CellHeight),
StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(propertyName, nameof(CellWidth),
StringComparison.InvariantCultureIgnoreCase))
{
ImplementNewSize();
}
}
private void ImplementNewSize()
{
if (ViewHeight == 0 || ViewWidth == 0)
return;
if (_resizeTimer.IsEnabled)
_resizeTimer.Stop();
_resizeTimer.Start();
}
When timer ticks, method checks that both width
and height
are valid and recreates cell set. Then method CreateOrUpdateCellViewModels
that update observable collection of collections of cell view models is executed:
private void ResizeTimerTick(object sender, EventArgs e)
{
_resizeTimer.Stop();
if (ViewHeight == 0 || ViewWidth == 0)
return;
var newWidth = System.Math.Max(1,
(int)System.Math.Ceiling((double)ViewWidth / CellWidth));
var newHeight = System.Math.Max(1,
(int)System.Math.Ceiling((double)ViewHeight / CellHeight));
if (CellSet != null &&
GridWidth == newWidth &&
GridHeight == newHeight)
{
return;
}
var currentPoints = CellSet?.GetPoints().Where
(point => point.X < newWidth && point.Y < newHeight);
CellSet = new CellSet(newWidth, newHeight);
GridWidth = CellSet.Width;
GridHeight = CellSet.Height;
if (currentPoints != null)
CellSet.SetPoints(currentPoints);
CreateOrUpdateCellViewModels();
}
Update Collection of Cell View Models
After new cells set is created, collection of cell view models should be updated. In the previous post, this collection was recreated each time and it leads to the application hanging. This issue is solved by asynchronous method of updating current collection. Due to WPF architecture and as dynamic grid user control item source is bound to Cells
collection, all changes of this collection is done via Dispatcher
. In the application, priority DispatcherPriority.ApplicationIdle
is used as it is executed after all data bindings, but other value could be used.
Start point is the method CreateOrUpdateCellViewModels
that creates Cells
collection at first time, creates cancellation token and starts asynchronous recurrent method CreateCellViewModelsAsync
for the first row.
private async void CreateOrUpdateCellViewModels()
{
_logger.LogStart("Start");
if (_cancellationSource != null && _cancellationSource.Token.CanBeCanceled)
_cancellationSource.Cancel();
if (Cells == null)
Cells = new ObservableCollection<ObservableCollection<ICellViewModel>>();
try
{
_cancellationSource = new CancellationTokenSource();
await CreateCellViewModelsAsync(0, _cancellationSource.Token).ConfigureAwait(false);
}
catch (OperationCanceledException ex)
{
_logger.Exception(ex);
}
catch (AggregateException ex)
{
foreach (var innerException in ex.InnerExceptions)
{
_logger.Exception(innerException);
}
}
finally
{
_cancellationSource = null;
}
_logger.LogEnd("Completed - but add cells in asynchronous way");
}
As cell view models is stored as collection of collections, each inner collection corresponds to the row of grid. Method CreateCellViewModelsAsync
is executed for each row position from 0
till Math.Max(Cells.Count, GridHeight)
. The following cases are possible:
rowNumber >= GridHeight
, that means that collection Cell
contains more rows than current size of grid. These rows should be removed:
Application.Current.Dispatcher.Invoke(
() => Cells.RemoveAt(positionToProcess),
DispatcherPriority.ApplicationIdle,
cancellationToken);
rowNumber < Cells.Count
, that means that row with such index exists in collection Cell
and index less than grid height. In this case, method UpdateCellViewModelRow
is called:
Application.Current.Dispatcher.Invoke(
() => UpdateCellViewModelRow(positionToProcess),
DispatcherPriority.ApplicationIdle,
cancellationToken);
Let's note that row is ObservableCollection<icellviewmodel></icellviewmodel>
. Depends on the relation between length of this collection and grid width, extra cell view models are removed, existent cell models are updated with new ICell
instance from dynamic grid data model, and missing cell view models are added:
private void UpdateCellViewModelRow(int rowNumber)
{
var row = Cells[rowNumber];
while (row.Count > GridWidth)
row.RemoveAt(GridWidth);
for (var pos = 0; pos < GridWidth; pos++)
{
var cell = CellSet.GetCell(pos, rowNumber);
if (pos < row.Count)
row[pos].Cell = cell;
else
{
var cellViewModel = new CellViewModel(cell);
row.Add(cellViewModel);
}
}
}
- "
else
" case, i.e., rowNumber >= Cells.Count
and rowNumber < GridHeight
, that means that collection Cell
does not contain necessary row. This row is created by method CreateCellViewModelRow
:
private void CreateCellViewModelRow(int rowNumber)
{
_logger.Log($"Create {rowNumber} row of cells");
var row = new ObservableCollection<ICellViewModel>();
for (var x = 0; x < GridWidth; x++)
{
var cellViewModel = new CellViewModel(CellSet.GetCell(x, rowNumber));
row.Add(cellViewModel);
}
_logger.Log($"{rowNumber} row of cells is ready for rendering");
Cells.Add(row);
}
Dependency Container
Unity is used as dependency container. In the post, we register EmptyLogger
as logger and create singleton for the instance of DynamicGridViewModel
. In WPF applications, the initialization of DI container is done in OnStartup
method in App.xaml.cs:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
IUnityContainer container = new UnityContainer();
container.RegisterType<ILogger, EmptyLogger>();
var dynamicGridViewModel = new DynamicGridViewModel(
container.Resolve<ILogger>())
{
};
container.RegisterInstance(
typeof(IDynamicGridViewModel),
dynamicGridViewModel,
new ContainerControlledLifetimeManager());
var mainWindow = container.Resolve<MainWindow>();
Application.Current.MainWindow = mainWindow;
Application.Current.MainWindow.Show();
}
MainWindow
constructor has parameter that is resolved by container:
public MainWindow(IDynamicGridViewModel dynamicGridViewModel)
{
InitializeComponent();
DataContext = dynamicGridViewModel;
}
Similarly, input parameter of DynamicGridViewModel
constructor is resolved by container:
public class DynamicGridViewModel : BaseNotifyPropertyChanged, IDynamicGridViewModel
{
private readonly ILogger _logger;
public DynamicGridViewModel(ILogger logger)
{
logger.ThrowIfNull(nameof(logger));
_logger = logger;
this.SetDefaultValues();
_logger.Log("DynamicGridViewModel constructor is completed");
}
}
History
- 4th January, 2017: Initial post