I'm currently trying to make a grid consisting of Cell objects in WPF. I need to bind the cells to objects, which needs to be in a 2D array. - And i need it to be big, scalable and to change color of cells and store data in the objects!
I have an implementation made, but it seems to be very slow to draw the grid! (100x100 grid takes >10 secs!)
Here is a picture of what i have already made:
I'm using databinding in XAML in an ItemsControl. Here is my XAML:
<ItemsControl x:Name="GridArea" ItemsSource="{Binding Cellz}" Grid.Column="1" BorderBrush="Black" BorderThickness="0.1">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:Cell}">
<Border BorderBrush="Black" BorderThickness="0.1">
<Grid Background="{Binding CellColor}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseMove" >
<ei:CallMethodAction TargetObject="{Binding}" MethodName="MouseHoveredOver"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="{Binding Rows}" Columns="{Binding Columns}" MouseDown="WrapPanelMouseDown" MouseUp="WrapPanelMouseUp" MouseLeave="WrapPanelMouseLeave" >
<!--<UniformGrid.Background>
<ImageBrush/>
</UniformGrid.Background>-->
</UniformGrid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
In my codebehind i instantiate a object of a class called Grid, which makes a 2D array (and a list, which is what im binding to) with objects of the Cell class. After some checking with a stopwatch, i can see that this is not taking the time. Its the actual binding and drawing the grids, so i guess my optimization should happen in my XAML, if any optimization is available.
But to provide everything, here is my code behind and the grid class and the cell class:
public MainWindow()
{
InitializeComponent();
NewGrid = new Grid(75, 75);
DataContext = NewGrid;
}
public class Grid
{
public int Columns { get; set; }
public int Rows { get; set; }
public ObservableCollection<Cell> Cellz {get;set;}
public Cell[,] CellArray { get; set; }
public Grid(int columns, int rows)
{
Columns = columns;
Rows = rows;
Cellz = new ObservableCollection<Cell>();
CellArray = new Cell[Rows,Columns];
InitializeGrid();
}
public void InitializeGrid()
{
Color col = Colors.Transparent;
SolidColorBrush Trans = new SolidColorBrush(col);
for (int i = 0; i < Rows; i++)
{
for (int j = 0; j < Columns; j++)
{
var brandNewCell = new Cell(i, j) { CellColor = Trans};
Cellz.Add(brandNewCell);
CellArray[i, j] = brandNewCell;
}
}
}
public class Cell : INotifyPropertyChanged
{
public int x, y; // x,y location
public Boolean IsWall { get; set; }
private SolidColorBrush _cellcolor;
public SolidColorBrush CellColor
{
get { return _cellcolor; }
set
{
_cellcolor = value;
OnPropertyChanged();
}
}
public Cell(int tempX, int tempY)
{
x = tempX;
y = tempY;
}
public bool IsWalkable(Object unused)
{
return !IsWall;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(
[CallerMemberName] string caller = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
}
}
I like the quite simple implementation with binding, however the loadtime is really unacceptable - Any advice would be greatly appreciated!
Well, i recreated your example, with some changes. I mainly got rid of the bindings on the DataContext, and created a viewmodel specifically for your use case, that gets bound directly to the itemscontrol.
The speed of drawing is definitely under the 10 seconds, but I thought i gave you as much of the relevant code as possible so you could compare the solutions...
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using TestSO.model;
namespace TestSO.viewmodel
{
public class ScreenViewModel : INotifyPropertyChanged, IDisposable
{
public event PropertyChangedEventHandler PropertyChanged;
private IList<Cell> cells;
public IList<Cell> Cells
{
get
{
return cells;
}
set
{
if (object.Equals(cells, value))
{
return;
}
UnregisterSource(cells);
cells = value;
RegisterSource(cells);
RaisePropertyChanged("Cells");
}
}
private int rows;
public int Rows
{
get
{
return rows;
}
set
{
if (rows == value)
{
return;
}
rows = value;
RaisePropertyChanged("Rows");
}
}
private int columns;
public int Columns
{
get
{
return columns;
}
set
{
if (columns == value)
{
return;
}
columns = value;
RaisePropertyChanged("Columns");
}
}
private Cell[,] array;
public Cell[,] Array
{
get
{
return array;
}
protected set
{
array = value;
}
}
protected void RaisePropertyChanged(string propertyName)
{
var local = PropertyChanged;
if (local != null)
{
App.Current.Dispatcher.BeginInvoke(local, this, new PropertyChangedEventArgs(propertyName));
}
}
protected void RegisterSource(IList<Cell> collection)
{
if (collection == null)
{
return;
}
var colc = collection as INotifyCollectionChanged;
if (colc != null)
{
colc.CollectionChanged += OnCellCollectionChanged;
}
OnCellCollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, collection, null));
}
protected virtual void OnCellCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
Array = null;
}
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
var cell = item as Cell;
if (cell == null)
{
continue;
}
if (Array == null)
{
continue;
}
Array[cell.X, cell.Y] = null;
}
}
if (e.NewItems != null)
{
if (Array == null)
{
Array = new Cell[Rows, Columns];
}
foreach (var item in e.NewItems)
{
var cell = item as Cell;
if (cell == null)
{
continue;
}
if (Array == null)
{
continue;
}
Array[cell.X, cell.Y] = cell;
}
}
}
protected void UnregisterSource(IList<Cell> collection)
{
if (collection == null)
{
return;
}
var colc = collection as INotifyCollectionChanged;
if (colc != null)
{
colc.CollectionChanged -= OnCellCollectionChanged;
}
OnCellCollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public ScreenViewModel()
{
}
public ScreenViewModel(int row, int col)
: this()
{
this.Rows = row;
this.Columns = col;
}
bool isDisposed = false;
private void Dispose(bool disposing)
{
if (disposing)
{
if (isDisposed)
{
return;
}
isDisposed = true;
Cells = null;
}
}
public void Dispose()
{
Dispose(true);
}
}
}
And i created as an extra, a controller, who is the owner of the ObservableCollection, main purpose would be to not do any changes on the viewModel, but rather change the collection inside the controller (or add add, remove, clear methods to it), and let the event chain do the work for me, keeping the 2Dimensional Array up to date in the ScreenViewModel
using System.Collections.Generic;
using System.Collections.ObjectModel;
using TestSO.model;
namespace TestSO.controller
{
public class GenericController<T>
{
private readonly IList<T> collection = new ObservableCollection<T>();
public IList<T> Collection
{
get
{
return collection;
}
}
public GenericController()
{
}
}
public class CellGridController : GenericController<Cell>
{
public CellGridController()
{
}
}
}
And your cell class, i slightly adjusted it with only raising the change event in case there was actually a change
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;
namespace TestSO.model
{
public class Cell : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
var local = PropertyChanged;
if (local != null)
{
Application.Current.Dispatcher.BeginInvoke(local, this, new PropertyChangedEventArgs(propertyName));
}
}
private int x;
public int X
{
get
{
return x;
}
set
{
if (x == value)
{
return;
}
x = value;
RaisePropertyChanged("X");
}
}
private int y;
public int Y
{
get
{
return y;
}
set
{
if (y == value)
{
return;
}
y = value;
RaisePropertyChanged("Y");
}
}
private bool isWall;
public bool IsWall
{
get
{
return isWall;
}
set
{
if (isWall == value)
{
return;
}
isWall = value;
RaisePropertyChanged("IsWall");
}
}
private SolidColorBrush _cellColor;
public SolidColorBrush CellColor
{
get
{
// either return the _cellColor, or say that it is transparent
return _cellColor ?? Brushes.Transparent;
}
set
{
if (SolidColorBrush.Equals(_cellColor, value))
{
return;
}
_cellColor = value;
RaisePropertyChanged("CellColor");
}
}
public Cell()
{
}
public Cell(int x, int y)
: this()
{
this.X = x;
this.Y = y;
}
}
}
Then I changed the xaml a bit (though didn't take over the interaction points), by creating the resources for the ScreenViewModel, the controller, and a DataTemplate, this Template, is then also DataTemplate directly added to the ItemsControl over the ItemTemplate, instead of using the DataTemplate functionality (didn't see that as a requirement above?)
<Window x:Class="TestSO.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model="clr-namespace:TestSO.model"
xmlns:viewmodel="clr-namespace:TestSO.viewmodel"
xmlns:controller="clr-namespace:TestSO.controller"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<controller:CellGridController x:Key="CellController" />
<viewmodel:ScreenViewModel x:Key="GridViewModel" Rows="75" Columns="75" />
<DataTemplate x:Key="CellTemplate">
<Border BorderBrush="Black" BorderThickness="0.5">
<Grid Background="{Binding CellColor}">
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding Cells,Source={StaticResource GridViewModel}}" BorderBrush="Black" BorderThickness="0.1" ItemTemplate="{StaticResource CellTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid
Rows="{Binding Rows,Source={StaticResource GridViewModel}}"
Columns="{Binding Columns,Source={StaticResource GridViewModel}}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</Window>
And inside main.cs page, i loaded I linked the Collection of the controller with the ScreenViewModel.Cells property, and loaded some template data. Just pretty basic mock data (you could also attach the screenmodel to the DataContext and define the controller somewhere else, and change the bindings in the xaml to go back to the DataContext, however over the resources, you can also get to the already created instances (after initializeComponent)
protected ScreenViewModel ScreenViewModel
{
get
{
return this.Resources["GridViewModel"] as ScreenViewModel;
}
}
protected CellGridController Controller
{
get
{
return this.Resources["CellController"] as CellGridController;
}
}
protected void Load()
{
var controller = Controller;
controller.Collection.Clear();
string[] rows = colorToCellSource.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
string row;
for (int x = 0; x < rows.Length; x++)
{
int length = rows[x].Length;
ScreenViewModel.Rows = rows.Length;
ScreenViewModel.Columns = length;
row = rows[x];
for (int y = 0; y < length; y++)
{
Cell cell = new Cell(x, y);
cell.CellColor = row[y] == '0' ? Brushes.Transparent : Brushes.Blue;
controller.Collection.Add(cell);
}
}
}
public MainWindow()
{
InitializeComponent();
if (Controller != null && ScreenViewModel != null)
{
ScreenViewModel.Cells = Controller.Collection;
Load();
}
}
The screen takes gets redrawn here under 1 second, resizing and maximizing takes a small delay, but i guess that can be expected... (My test template was 105x107)
It sounds like the delay is being caused by resolving the template for and rendering so many items.
Some suggestions to improve it :
- Don't store empty cells. Only store cells with "data" in them.
- Bind using a static
ItemTemplate
instead of resolving the type dynamically
- Try simplifing your ItemTemplate
For the first point, I would try using two layers here instead of just one: one layer to draw the grid, and another sitting on top of it to draw objects at specific locations in the Grid.
XAML would look something like this
<Grid>
<UniformGrid Rows="{Binding RowCount}" Columns="{Binding ColumnCount}" />
<ItemsControl ItemsSource="{Binding Cells}" .... />
</Grid>
Where the ItemsControl
uses a Canvas
for the ItemContainerTemplate
, and binds the Canvas.Top
and Canvas.Left
to the X,Y cell data in the ItemContainerStyle
, probably using some kind of Converter to multiply the X,Y value by the grid cell size.
To get GridLines to show, you can either settle with ShowGridLines=True
to get the faint dotted lines, or use a customized Grid like this one that overwrites OnRender
to draw grid lines. This would also help reduce the number of objects in your ItemTemplate for the 3rd point, as you don't need the <Border>
object now.
I'm also not entirely sure how you want to interact with your Grid, but it may help with initial load times to put all the mouse handlers on the background Grid itself instead of on each individual item too, and simply calculate the item below the mouse by the X,Y position. You'd probably have to ensure the IsHitTestVisible="False"
is set in the ItemContainerStyle
for this.
For the second point (and as Icepickle pointed out), using a static template instead of a dynamic one based on type of object will definitely help.
So instead of using an implicit template, give your template an x:Key
property and bind it using a static binding
<DataTemplate x:Key="CellTemplate" DataType="{x:Type local:Cell}">
<Border BorderBrush="Black" BorderThickness="0.1">
<Grid Background="{Binding CellColor}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseMove" >
<ei:CallMethodAction TargetObject="{Binding}" MethodName="MouseHoveredOver"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Grid>
</Border>
</DataTemplate>
<ItemsControl ItemTemplate="{StaticResource CellTemplate}" ... />