admin管理员组

文章数量:1335832

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

Model.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);
    }
}

View.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
}

Attempt

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?

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

Model.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);
    }
}

View.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
}

Attempt

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?

Share Improve this question edited Nov 20, 2024 at 16:24 ycsvenom asked Nov 19, 2024 at 20:42 ycsvenomycsvenom 331 silver badge8 bronze badges 9
  • Your "view" is bleeding into your model. There is no clear distinction between what the "view" should be aware of; and what the model needs to know. Your "board" (view) should be a "grid" type layout control containing elements that can "bind" to a corresponding "logical" collection in the model. Updating the model's collection should raise "property changed" events that the view responds to; based on the "path" of the properties it has bound to; e.g. a color or "color converter". – Gerry Schmitz Commented Nov 20, 2024 at 19:44
  • @GerrySchmitz Actually I am just testing the code sample above, I am planning to move nearly everything in View.cs to ViewModel.cs to apply MVVM, but can you elaborate more about the suggested solution?, I mean what is the added value in Grid over Canvas, I chose Canvas because it gives me the freedom to set the position of elements with ease mathematically; and how can I bind it since it's a 2d List?. – ycsvenom Commented Nov 21, 2024 at 16:51
  • @GerrySchmitz I have been thinking about changing Canvas to Grid and it's indeed beneficial but what about the binding? – ycsvenom Commented Nov 21, 2024 at 23:29
  • I use a Canvas for "random" motion. Everything else is something that starts left (or right), top corner; and then flows right (or left) and down. Building a "board" (like checkers; Go; Chess), is easier using a "layout control" with (sizable) rows and columns; where you just load elements in sequence; and not worry about calculating coordinates. The UI elements can be Rectangles; Buttons; UC's; whatever meets you objective. I built a "Go Game" using "Ellipses" (in the "squares") to function as "stones"; bound to a "player stones" list. Use the (UI element) .Tag property to "tramp" references. – Gerry Schmitz Commented Nov 23, 2024 at 4:40
  • If you consider that every UI element "can" have it's own (personal) DataContext; and does not need to "share" a view's (single) DataContext; the possibilities are endless. Every element, in effect, is a "view". As for "view models"; they can simply be an "Interface" (to the backing model); they don't have to be separate "pieces of code" (IMO). – Gerry Schmitz Commented Nov 24, 2024 at 16:03
 |  Show 4 more comments

1 Answer 1

Reset to default 0

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.

View.xaml

<UserControl x:Class="Views.BoardView"
             xmlns="http://schemas.microsoft/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats./markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft/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();
    }
}

本文标签: cWPF Multidimensional Complex Object Model Binding and Wrapping for MVVMStack Overflow