WPF MVVM 多维复杂对象模型绑定和包装

问题描述 投票:0回答:1

我对 MVVM 有点陌生,我有一个复杂的模型

List<List<SomeClass>>
,我们称其为 board,我想将其绑定到 WPF 中的
canvas
canvas
应该充当
Rectangle 的地图
s 每个
Rectangle
应该绑定到板中的
bool
值会影响
Rectangle
的颜色。

我已经设法将它绑定到

Rectangle
,但它是静态的一次绑定,因为
Model
中没有发生通知,现在我一直试图将其包装在
ViewModel
中以保持完全分离。

模型.cs

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();
    }
}

ViewModel.cs

internal partial class BoardViewModel : ObservableObject
{
    [ObservableProperty] int width = 5;
    [ObservableProperty] int height = 5;
    [ObservableProperty] GameBoard board;

    public BoardViewModel()
    {
        Board = new(Width, Height);
    }
}

查看.cs

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
到底是什么?

c# wpf mvvm viewmodel community-toolkit-mvvm
1个回答
0
投票

正如@GerrySchmitz 的评论中所建议的,我使用了

UniformGrid
ItemsControl
的组合。然后使用简单的
FlattenListConverter
绑定多维列表。 顾名思义,转换器会将多维列表展平为项目列表,这将由
ItemsControl
馈送到
UniformGrid
,并将使用
UniformGrid
提供的均匀性进行均匀分布,如下预览。

查看.xaml

<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>

FlattenListConverter.cs

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();
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.