MVVM -在WPF中直接更新集合时网格不更新

u5rb5r59  于 2023-08-07  发布在  其他
关注(0)|答案(1)|浏览(84)

我正在尝试使用MVVM设计模式在WPF中实现一个井字游戏。游戏现在运行正常,但是当我试图重新设置游戏重新开始时,问题就出现了。在这种情况下,我所做的是直接更新集合,然后窗口中的网格不会更新为新值。
除此之外,我希望从MVVM的Angular 对我的实现提供一些反馈。下面是我的实现:
BoardView.xaml:

<Grid Name="boardGrid" Background="White">
        <Grid.Resources>
            <Style TargetType="Label">
                <EventSetter Event="MouseLeftButtonUp" Handler="Label_Click"/>
            </Style>
        </Grid.Resources>

        <UniformGrid Rows="3" Columns="3">
            <Label Content="{Binding Path=Board[0], Mode=TwoWay}"/>
            <Label Content="{Binding Path=Board[1], Mode=TwoWay}"/>
            <Label Content="{Binding Path=Board[2], Mode=TwoWay}"/>
            <Label Content="{Binding Path=Board[3], Mode=TwoWay}"/>
            <Label Content="{Binding Path=Board[4], Mode=TwoWay}"/>
            <Label Content="{Binding Path=Board[5], Mode=TwoWay}"/>
            <Label Content="{Binding Path=Board[6], Mode=TwoWay}"/>
            <Label Content="{Binding Path=Board[7], Mode=TwoWay}"/>
            <Label Content="{Binding Path=Board[8], Mode=TwoWay}"/>
        </UniformGrid>
    </Grid>

字符串
BoardView.xaml.cs:

private BoardViewModel _viewModel;
        public BoardView()
        {
            InitializeComponent();
            _viewModel = new BoardViewModel();
            this.DataContext = _viewModel;
        }

        private void Label_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            _viewModel.LabelClick(sender);
        }


Board.cs:

private string[] tiles;

        public Board()
        {
            tiles = new string[9];

            for (int i = 0; i < 9; i++)
                tiles[i] = TurnSymbol._.ToString();
        }

        public string[] Tiles
        {
            get { return tiles; }
            set { tiles = value; }
        }


以及BoardViewModel.cs中的相关部分:

enum TurnSymbol
    {
        x,
        o,
        _
    }
internal class BoardViewModel : INotifyPropertyChanged
    {
        private TurnSymbol currentSymbol;
        private Board board;
        public event PropertyChangedEventHandler? PropertyChanged;

        public string[] Board
        {
            get { return board.Tiles; }
            set
            {
                board.Tiles = value;
                OnPropertyChanged(nameof(Board));
            }
        }

        public BoardViewModel()
        {
            board = new Board();
        }

        protected virtual void OnPropertyChanged(string propertyName) 
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        internal void LabelClick(object sender)
        {
            if((string)((Label)sender).Content == TurnSymbol._.ToString())
            {
                ((Label)sender).Content = currentSymbol.ToString();
                nextSymbol();

                //Need completion
                IsGameOver();
            }
            else
            {
                MessageBox.Show("This cell is already filled! Choose another one.");
            }
        }

        private bool HandleGameOverMessage(string winningSymbol)
        {
            if(MessageBox.Show($"Game over! {winningSymbol} wins!", "Play again?", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
            {
                return true;
            }
        }

        private void HandleGameOver(string winningSymbol)
        {
            //Need completion
            bool playAgain = HandleGameOverMessage(winningSymbol);

            //Not needed but for extra clarity
            if(playAgain) 
                Reset();
        }

        private void Reset()
        {
            for(int i = 0; i < 9; i++)
                Board[i] = TurnSymbol._.ToString();

            currentSymbol = TurnSymbol.x;

            MessageBox.Show("x starts!", "", MessageBoxButton.OK);
        }

        internal bool IsGameOver()
        {
            //Check rows
            if (board.Tiles[0] != TurnSymbol._.ToString() && board.Tiles[0] == board.Tiles[1] && board.Tiles[1] == board.Tiles[2])
            {
                HandleGameOver(board.Tiles[1]);
            }
            else if (board.Tiles[3] != TurnSymbol._.ToString() && board.Tiles[3] == board.Tiles[4] && board.Tiles[4] == board.Tiles[5])
            ...

            return false;
        }
    }


我尝试在xaml中添加UpdateSourceTrigger=PropertyChanged。我还调试了应用程序,可以看到Board属性和board.Tiles已更新并显示正确的值,所以我猜是我缺少了一些东西,或者我在绑定方面做错了。

gkn4icbw

gkn4icbw1#

为了更新标签,使用ObservableCollection而不是字符串数组:

public class BoardViewModel
{
    // an ObservableCollection with 9 empty strings
    public ObservableCollection<string> Board { get; }
        = new ObservableCollection<string>(
            Enumerable.Range(0, 9).Select(i => ""));
}

字符串
不要在XAML中显式创建9个Label元素,而是使用将其ItemsSource属性绑定到视图模型的Board属性的ItemsControl。ItemsControl使用UniformGrid作为其ItemsPanel,并在其ItemTemplate中声明Label。

<ItemsControl ItemsSource="{Binding Board}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Label Content="{Binding}"
                   HorizontalContentAlignment="Center"
                   VerticalContentAlignment="Center"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>


不要访问视图模型中的任何视图元素(这是违反MVVM的),而只需更新集合元素,如

viewModel.Board[4] = "X";


这通常由视图模型中的ICommand执行,例如a RelayCommand
这种命令的一个简单示例如下所示:

public class BoardViewModel
{
    public ObservableCollection<string> Board { get; }
        = new ObservableCollection<string>(
            Enumerable.Range(0, 9).Select(i => ""));

    public RelayCommand<int> ClickCommand { get; }

    public ViewModel()
    {
        ClickCommand = new RelayCommand<int>(i => Board[i] = "X");
    }
}


使用这样一个命令并将被点击的单元格的索引传递给视图模型的一个简单方法可能看起来像下面所示的那样,使用Button而不是Label。注意属性CommandCommandParameterAlternationCountAlternationIndex的分配。

<ItemsControl ItemsSource="{Binding Board}" AlternationCount="9">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Content="{Binding}"
                    Command="{Binding Path=DataContext.ClickCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
                    CommandParameter="{Binding Path=(ItemsControl.AlternationIndex), RelativeSource={RelativeSource AncestorType=ContentPresenter}}"
                    HorizontalContentAlignment="Center"
                    VerticalContentAlignment="Center"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>


作为替代方案,您可以有一个带有数据项类的视图模型,该数据项类保存单元格的内容和更改单元格内容的命令。
一个简单的例子:

public class BoardItem : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string text = "";

    public string Text
    {
        get { return text; }
        set
        {
            text = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
        }
    }

    public RelayCommand ClickCommand { get; }

    public BoardItem()
    {
        ClickCommand = new RelayCommand(() => Text = "X");
    }
}

public class BoardViewModel
{
    public BoardItem[] Board { get; }
        = Enumerable.Range(0, 9).Select(i => new BoardItem()).ToArray();
}


它的用法如下:

<ItemsControl ItemsSource="{Binding Board}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Content="{Binding Text}"
                    Command="{Binding Path=ClickCommand}"
                    HorizontalContentAlignment="Center"
                    VerticalContentAlignment="Center"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

相关问题