I am a bit new to MVVM, I have a complex model List<List<SomeClass>>
, lets call it a board, I want to bind it to a canvas
in WPF, the canvas
should act as map of Rectangle
s each Rectangle
should be binded to a bool
value in the board that will influence the color of the Rectangle
.
I have managed to bind it to the Rectangle
s but it was static one time binding because there was no notification happening in the Model
, now I am stuck trying to wrap it in the ViewModel
to keep complete seperation.
public class Cell(int x, int y)
{
public int X { get; set; } = x;
public int Y { get; set; } = y;
public bool IsOn { get; set;} = false;
// Other properties...
}
public partial class GameBoard(int width, int height) : IEnumerable<List<Cell>>
{
public int Height { get; private set; } = width;
public int Width { get; private set; } = height;
private List<List<Cell>> Board { get; set; } = Enumerable.Range(0, height)
.Select(y => Enumerable.Range(0, width)
.Select(x => new Cell(x, y)).ToList())
.ToList();
public IEnumerator<List<Cell>> GetEnumerator()
{
return Board.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return Board.GetEnumerator();
}
}
internal partial class BoardViewModel : ObservableObject
{
[ObservableProperty] int width = 5;
[ObservableProperty] int height = 5;
[ObservableProperty] GameBoard board;
public BoardViewModel()
{
Board = new(Width, Height);
}
}
public partial class BoardView : UserControl
{
private readonly BoardViewModel viewModel;
public BoardView()
{
InitializeComponent();
viewModel = new BoardViewModel();
DataContext = viewModel;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
// For testing
CellWidth = 30;
CellHeight = 30;
var offset = new Point(
(int)ActualWidth / 2 - viewModel.Width * (int)(CellWidth + CellOffset) / 2,
(int)ActualHeight / 2 - viewModel.Height * (int)(CellHeight + CellOffset) / 2);
for (int y = 0; y < viewModel.Height; y++)
{
for (int x = 0; x < viewModel.Width; x++)
{
viewModel.Board[x, y] = true;
var rect = new Rectangle
{
Width = CellWidth,
Height = CellHeight,
Tag = new Point(x, y),
};
var binding = new Binding()
{
Source = viewModel,
Path = new PropertyPath($"Board[{y},{x}]"),
Converter = new IsOnToFillConverter() // convert from bool to Brush
};
rect.MouseUp += Any_Click;
rect.SetBinding(Shape.FillProperty, binding);
Canvas.SetTop(rect, offset.Y + y * (rect.Height + CellOffset));
Canvas.SetLeft(rect, offset.X + x * (rect.Width + CellOffset));
canvasBoard.Children.Add(rect);
}
}
}
// For testing
private void Any_Click(object sender, RoutedEventArgs e)
{
var location = (Point)(sender as Rectangle)!.Tag;
viewModel.Board[location] = !viewModel.Board[location];
}
#region Dependency Properties
public double CellWidth
{
get { return (double)GetValue(CellWidthProperty); }
set { SetValue(CellWidthProperty, value); }
}
// Using a DependencyProperty as the backing store for CellWidth. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CellWidthProperty =
DependencyProperty.Register(nameof(CellWidth), typeof(double), typeof(BoardView), new PropertyMetadata(0d));
public double CellHeight
{
get { return (double)GetValue(CellHeightProperty); }
set { SetValue(CellHeightProperty, value); }
}
// Using a DependencyProperty as the backing store for CellHeight. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CellHeightProperty =
DependencyProperty.Register(nameof(CellHeight), typeof(double), typeof(BoardView), new PropertyMetadata(0d));
public double CellOffset
{
get { return (double)GetValue(CellOffsetProperty); }
set { SetValue(CellOffsetProperty, value); }
}
// Using a DependencyProperty as the backing store for CellOffset. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CellOffsetProperty =
DependencyProperty.Register(nameof(CellOffset), typeof(double), typeof(BoardView), new PropertyMetadata(5d));
#endregion
}
public partial class GameBoardViewModel(int width, int height) : ObservableObject
{
private readonly GameBoard board = new(width, height);
public bool this[int y, int x]
{
get => board[y, x].IsOn;
set
{
OnPropertyChanging($"board[{y},{x}]");
board[y, x].IsOn = value;
OnPropertyChanged($"board[{y},{x}]");
}
}
public bool this[Point location]
{
get => board[location].IsOn;
set => this[location.Y, location.X] = value;
}
}
Is there a way to wrap GameBoard
with minimal boilerplate and with preserving model as is?
And can you explain what's happening under the hood when OnPropertyChanged
is called with a PropertyPath
and what exactly is a PropertyPath
?
As suggested in comments by @GerrySchmitz I have used a combination of UniformGrid
and ItemsControl
. Then Bind the multidimensional list using a simple FlattenListConverter
.
As the name suggests the converter will flatten the multidimensional list into list of items, this will be fed by ItemsControl
to the UniformGrid
and will be evenly distributed using the uniformity provided by UniformGrid
, as previewed bellow.
<UserControl x:Class="Views.BoardView"
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:Views"
xmlns:viewmodels="clr-namespace:ViewModels"
xmlns:converters="clr-namespace:Converters"
xmlns:core="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Loaded="UserControl_Loaded"
d:DesignHeight="400"
d:DesignWidth="400"
d:Background="{StaticResource BoardBackground}"
d:DataContext="{d:DesignInstance Type=viewmodels:BoardViewModel}">
<UserControl.Resources>
<converters:FlattenListConverter x:Key="FlattenListConverter" />
</UserControl.Resources>
<Border Grid.Column="1"
Background="{Binding Background, RelativeSource={RelativeSource Self}}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch">
<ItemsControl ItemsSource="{Binding Board, Converter={StaticResource FlattenListConverter}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid x:Name="uniGridBoard"
Background="Transparent">
</UniformGrid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Border>
</UserControl>
using BoardLogic;
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Windows.Data;
using System.Linq;
namespace Converters;
public class FlattenListConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not ObservableCollection<ObservableCollection<Cell>> items)
return null!;
return items.SelectMany(items => items);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}