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
相关产品推荐
相关产品推荐

