我对 MVVM 有点陌生,我有一个复杂的模型
List<List<SomeClass>>
,我们称其为 board,我想将其绑定到 WPF 中的 canvas
,canvas
应该充当 Rectangle
的地图
s 每个 Rectangle
应该绑定到板中的 bool
值会影响 Rectangle
的颜色。
我已经设法将它绑定到
Rectangle
,但它是静态的一次绑定,因为Model
中没有发生通知,现在我一直试图将其包装在ViewModel
中以保持完全分离。
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;
}
}
有没有一种方法可以用最少的样板来包装
GameBoard
并按原样保留模型?
您能否解释一下,当使用
OnPropertyChanged
调用 PropertyPath
时,幕后发生了什么?PropertyPath
到底是什么?
正如@GerrySchmitz 的评论中所建议的,我使用了
UniformGrid
和 ItemsControl
的组合。然后使用简单的 FlattenListConverter
绑定多维列表。
顾名思义,转换器会将多维列表展平为项目列表,这将由 ItemsControl
馈送到 UniformGrid
,并将使用 UniformGrid
提供的均匀性进行均匀分布,如下预览。
<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();
}
}