You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF MVVM架构下战舰游戏多模型依赖视图的解决方案咨询

嘿,我之前在WPF里折腾过类似的战舰游戏UI,你的困境我太能共情了!既要守住MVVM的原则,又要让单元格的样式动态跟着战舰状态走,确实得找个巧办法。我给你推荐一个优雅的包装器ViewModel方案,完美解决你之前五个方案的痛点:


核心思路:给每个Coordinate套个职责单一的视图模型

我们不需要修改原有Coordinate模型,也不用搞复杂的混合类,而是给每个坐标创建一个专门的CoordinateCellViewModel——它只负责衔接Coordinate、Battlefield和视图,处理单元格的状态计算与交互逻辑。

1. 定义CoordinateCellViewModel(包装器)

这个VM会持有Coordinate和Battlefield的引用,通过计算属性暴露单元格的状态,同时实现INotifyPropertyChanged让UI自动更新:

public class CoordinateCellViewModel : INotifyPropertyChanged
{
    private readonly Coordinate _coordinate;
    private readonly Battlefield _battlefield;

    // 保留对原始Coordinate的引用,满足你"Border仍依赖Coordinate"的需求
    public Coordinate Coordinate => _coordinate;
    public int Row => _coordinate.Row;
    public int Column => _coordinate.Column;

    // 计算单元格状态:根据Battlefield的数据动态生成
    public CellState CurrentState
    {
        get
        {
            // 先找当前坐标所属的战舰
            var ship = _battlefield.Ships.FirstOrDefault(s => s.Coordinates.Contains(_coordinate));
            
            if (ship == null)
            {
                // 无战舰:未击中是蓝色,击中过(空炮)也是蓝色
                return _battlefield.HitCoordinates.Contains(_coordinate) ? CellState.Missed : CellState.Empty;
            }
            else
            {
                // 有战舰:沉没则黑色,被击中则橙色,未被击中则保持初始蓝色(隐藏战舰位置)
                if (ship.IsSunk)
                    return CellState.ShipSunk;
                return _battlefield.HitCoordinates.Contains(_coordinate) ? CellState.ShipHit : CellState.Empty;
            }
        }
    }

    // 点击命令:处理用户点击单元格的逻辑
    public ICommand CellClickedCommand => new RelayCommand(() =>
    {
        // 通知Battlefield处理这次点击(比如标记击中、判断战舰是否沉没)
        _battlefield.ProcessCellHit(_coordinate);
        // 因为Battlefield数据变化,触发CurrentState的更新
        OnPropertyChanged(nameof(CurrentState));
    });

    public CoordinateCellViewModel(Coordinate coordinate, Battlefield battlefield)
    {
        _coordinate = coordinate;
        _battlefield = battlefield;
        
        // 监听Battlefield和战舰的变化,自动触发UI更新
        _battlefield.PropertyChanged += (s, e) =>
        {
            if (e.PropertyName == nameof(Battlefield.HitCoordinates))
                OnPropertyChanged(nameof(CurrentState));
        };
        
        // 监听所有战舰的沉没状态变化
        foreach (var ship in _battlefield.Ships)
        {
            ship.PropertyChanged += (s, e) =>
            {
                if (e.PropertyName == nameof(Ship.IsSunk) && ship.Coordinates.Contains(_coordinate))
                    OnPropertyChanged(nameof(CurrentState));
            };
        }
    }

    // INotifyPropertyChanged实现
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

// 枚举单元格状态,方便绑定和转换
public enum CellState
{
    Empty,
    Missed,
    ShipHit,
    ShipSunk
}

2. 调整父ViewModel

把原来的List<Coordinate>换成List<CoordinateCellViewModel>,初始化时批量包装所有坐标:

public class BattlefieldViewModel : INotifyPropertyChanged
{
    public List<CoordinateCellViewModel> CoordinateCells { get; }

    public BattlefieldViewModel(Battlefield battlefield)
    {
        CoordinateCells = battlefield.AllCoordinates
            .Select(c => new CoordinateCellViewModel(c, battlefield))
            .ToList();
    }

    // 其他属性和INotifyPropertyChanged实现...
}

3. 视图绑定与样式转换

用ValueConverter把CellState转换成对应的背景色,同时绑定点击命令:

<Window.Resources>
    <local:CellStateToBrushConverter x:Key="CellStateToBrushConverter"/>
</Window.Resources>

<!-- 以ItemsControl绑定Grid布局为例 -->
<ItemsControl ItemsSource="{Binding CoordinateCells}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid local:GridHelpers.RowCount="{Binding Battlefield.Rows}"
                  local:GridHelpers.ColumnCount="{Binding Battlefield.Columns}"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="Grid.Row" Value="{Binding Row}"/>
            <Setter Property="Grid.Column" Value="{Binding Column}"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Border Width="30" Height="30"
                    Background="{Binding CurrentState, Converter={StaticResource CellStateToBrushConverter}}"
                    Margin="1"
                    Cursor="Hand">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseLeftButtonUp">
                        <i:InvokeCommandAction Command="{Binding CellClickedCommand}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
                <Label Content="{Binding Coordinate}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
            </Border>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

实现状态到颜色的转换器:

public class CellStateToBrushConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is not CellState state) return Brushes.Transparent;
        
        return state switch
        {
            CellState.Empty or CellState.Missed => Brushes.Blue,
            CellState.ShipHit => Brushes.Orange,
            CellState.ShipSunk => Brushes.Black,
            _ => Brushes.Transparent
        };
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

为什么这个方案完美解决你的痛点?

  • 守住MVVM原则:视图完全通过绑定与VM交互,不直接操作模型,也没有硬编码逻辑
  • 不破坏原有绑定:CoordinateCellViewModel保留了对原始Coordinate的引用,视图可以随时访问
  • 无冗余数据同步:所有状态都是从Battlefield和Coordinate计算而来,模型变化时自动触发UI更新
  • 职责单一:每个CoordinateCellViewModel只负责单个单元格的状态和交互,代码清晰易维护
  • 满足所有样式需求:完美覆盖“未击中/击中/沉没”三种状态的颜色变化,连战舰沉没时所有关联单元格的颜色更新都能自动处理

内容的提问来源于stack exchange,提问作者pro100tom

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 08:05:36