ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#打字游戏实战:从MVVM架构到WPF动画的完整开发指南

C#打字游戏实战:从MVVM架构到WPF动画的完整开发指南

1. 项目概述:从“打字”到“架构”的实战演练

看到“打字游戏”这个标题,很多人的第一反应可能是:“这不就是个简单的键盘输入练习程序吗?”如果你也这么想,那可能就错过了这个项目背后真正的价值。作为一个在C#和.NET生态里摸爬滚打了十多年的开发者,我可以负责任地告诉你,一个设计精良的打字游戏,其复杂度和技术深度远超想象。它绝不仅仅是Console.WriteLineConsole.ReadKey的简单组合,而是一个融合了事件驱动编程、MVVM架构、数据绑定、动画系统、状态机设计、资源管理乃至性能优化的综合性实战沙盒。

这个项目之所以被众多培训机构和高阶教程选为经典案例,正是因为它麻雀虽小,五脏俱全。它以一个用户感知明确(打字)、交互逻辑清晰(按键、计时、计分)的应用为载体,迫使开发者去思考并实践那些在大型商业项目中才会遇到的架构问题。你不仅要处理用户的每一次击键事件,还要管理游戏的不同状态(准备、进行中、暂停、结束),要实时更新UI反馈(如命中特效、分数跳动),要持久化用户成绩,甚至要考虑如何让动画流畅、让资源加载高效。

对于初学者,它能帮你巩固C#语法、熟悉WinForms或WPF的基础控件;对于希望进阶的开发者,它是理解观察者模式(事件)、命令模式、数据绑定、依赖注入等概念的绝佳场景;而对于资深开发者,如何设计一个高内聚、低耦合、易于测试和扩展的游戏核心引擎,同样是一个值得深入探讨的课题。接下来,我们就抛开那些花哨的界面,深入到代码层面,看看如何从零开始,搭建一个经得起推敲的C#打字游戏。

2. 核心架构设计与技术选型解析

在动手写第一行代码之前,花时间在架构设计上是绝对值得的。一个混乱的架构会让后续的功能添加和BUG修复变得举步维艰。我们的目标是构建一个清晰、可维护、可测试的应用程序。

2.1 为什么选择WPF与MVVM?

虽然WinForms入门更快,但我强烈建议在这个项目中使用WPF(Windows Presentation Foundation)并结合MVVM(Model-View-ViewModel)模式。原因如下:

  1. 数据驱动的UI:打字游戏的核心是数据(当前单词、已输入字符、时间、分数、准确率)的实时变化。WPF强大的数据绑定机制,可以让你几乎不用编写后台更新UI的代码。当ViewModel中的属性发生变化时,UI会自动刷新。例如,分数Score属性增加,绑定到它的TextBlock就会自动更新数字。
  2. 命令与事件解耦:在MVVM中,用户操作(如点击“开始”按钮)通过ICommand接口绑定到ViewModel的命令,而不是在View的后台代码里写事件处理程序。这彻底将UI逻辑与业务逻辑分离,使得ViewModel可以独立进行单元测试。
  3. 丰富的样式与动画:WPF的样式(Style)、模板(Template)和故事板(Storyboard)能轻松实现华丽的视觉效果。比如,击中字母时的放大、变色动画,或者游戏结束时的渐变遮罩,用WPF实现远比WinForms优雅和高效。
  4. 未来的可扩展性:掌握了MVVM,就为未来开发更复杂的XAML平台应用(如UWP、MAUI、Avalonia)打下了坚实的基础。

实操心得:很多从WinForms转过来的开发者会觉得MVVM“很重”、“没必要”。但对于一个哪怕中等复杂度的应用,前期引入MVVM所增加的一点学习成本,会在中后期被其带来的清晰结构、易测试性和低维护成本加倍偿还。你可以先从简单的绑定开始,逐步引入ICommandINotifyPropertyChanged

2.2 核心模型(Model)设计

Model代表你的数据和业务规则。对于打字游戏,我们需要抽象出几个核心模型:

  1. WordItem(单词项)

    public class WordItem : ObservableObject // 继承自实现了INotifyPropertyChanged的基类 { private string _content; public string Content { get => _content; set => SetProperty(ref _content, value); } private int _length; public int Length { get => _length; set => SetProperty(ref _length, value); } // 单词的难度系数(可根据长度、字符组成计算) public double Difficulty { get; set; } // 单词类型(普通、奖励、障碍等) public WordType Type { get; set; } }

    这里使用了ObservableObject基类(通常来自社区工具包如CommunityToolkit.Mvvm)来简化属性变更通知的实现。

  2. GameSession(游戏会话): 这个类记录一次游戏的核心数据。

    public class GameSession { public DateTime StartTime { get; set; } public DateTime? EndTime { get; set; } public int TotalKeystrokes { get; set; } public int CorrectKeystrokes { get; set; } public int Score { get; set; } public double Accuracy => TotalKeystrokes > 0 ? (CorrectKeystrokes / (double)TotalKeystrokes) * 100 : 0; public List<WordItem> WordsPlayed { get; } = new List<WordItem>(); // 可以添加更多指标,如每分钟击键数(KPM)、每秒单词数(WPS) }
  3. PlayerProfile(玩家档案): 如果需要持久化数据,可以设计玩家档案。

    public class PlayerProfile { public string PlayerName { get; set; } public List<GameSession> HistorySessions { get; } = new List<GameSession>(); public int HighestScore { get; set; } public double HighestAccuracy { get; set; } // ... 其他统计信息 }

注意事项:Model应该尽量保持“纯净”,不包含任何与UI或平台相关的引用(如System.Windows命名空间下的类)。它的职责就是持有数据和定义核心业务逻辑。

2.3 视图模型(ViewModel)作为中枢

ViewModel是连接View和Model的桥梁,也是整个应用的“大脑”。对于打字游戏,一个主ViewModel(例如TypingGameViewModel)可能包含以下内容:

  1. 游戏状态:使用枚举和属性来管理。

    public enum GameState { Idle, CountingDown, Playing, Paused, GameOver } private GameState _currentState; public GameState CurrentState { get => _currentState; set => SetProperty(ref _currentState, value); }

    状态改变时,可以触发相应的命令启用/禁用。例如,在Playing状态时,“开始”按钮应禁用,“暂停”按钮应启用。

  2. 可观察集合:用于绑定到UI列表控件(如ListBox),显示当前需要打字的单词队列。

    public ObservableCollection<WordItem> ActiveWords { get; } = new ObservableCollection<WordItem>();
  3. 当前输入与反馈

    private string _currentInput = string.Empty; public string CurrentInput { get => _currentInput; set { if (SetProperty(ref _currentInput, value)) { // 输入变化时,触发输入验证逻辑 ValidateInputAgainstCurrentWord(); // 清空输入的命令是否可以执行?(例如,当输入不为空时) ClearInputCommand.NotifyCanExecuteChanged(); } } } // 用于高亮显示当前单词中已正确输入的部分 public string CurrentWordHighlighted { get; private set; }
  4. 命令(ICommand)

    public ICommand StartGameCommand { get; } public ICommand PauseGameCommand { get; } public ICommand SubmitWordCommand { get; } // 提交当前单词(如按空格) public ICommand ClearInputCommand { get; } // 在构造函数中初始化命令,通常使用RelayCommand或AsyncRelayCommand public TypingGameViewModel() { StartGameCommand = new RelayCommand(ExecuteStartGame, CanExecuteStartGame); // ... 其他命令初始化 } private void ExecuteStartGame() { CurrentState = GameState.CountingDown; // 启动倒计时和游戏逻辑 InitializeGameRound(); } private bool CanExecuteStartGame() => CurrentState == GameState.Idle || CurrentState == GameState.GameOver;
  5. 定时器与异步:游戏倒计时、单词下落动画等都需要定时驱动。在WPF的MVVM中,要小心处理线程问题。可以使用DispatcherTimer(UI线程)或者用Task.Run配合Dispatcher.Invoke来更新UI属性。对于更复杂的异步流,可以考虑使用Reactive Extensions (Rx.NET)

踩过的坑:不要在ViewModel中直接引用System.Windows.Threading.Dispatcher,这会让ViewModel与WPF平台耦合。一种更好的做法是通过依赖注入传递一个IDispatcher接口,或者在命令执行和属性设置时,确保它们在UI线程上执行(CommunityToolkit.MvvmRelayCommand在内部处理了部分线程上下文)。

3. 核心功能模块的详细实现

架构搭好,我们就可以填充血肉了。下面拆解几个最关键的功能模块。

3.1 单词池管理与动态难度

单词不能是硬编码的,最好从外部文件(如txt, json)加载。我们可以创建一个WordPoolService服务类。

public interface IWordPoolService { Task<List<WordItem>> LoadWordsAsync(); WordItem GetRandomWord(double targetDifficulty); } public class WordPoolService : IWordPoolService { private List<WordItem> _allWords; private Random _random = new Random(); public async Task<List<WordItem>> LoadWordsAsync() { // 异步从文件加载 var filePath = "Assets/words.txt"; var lines = await File.ReadAllLinesAsync(filePath); _allWords = lines.Select(line => new WordItem { Content = line.Trim(), Length = line.Trim().Length, Difficulty = CalculateDifficulty(line.Trim()) // 根据长度、音节、特殊字符计算 }).ToList(); return _allWords; } public WordItem GetRandomWord(double targetDifficulty) { if (_allWords == null || !_allWords.Any()) return new WordItem { Content = "hello", Length = 5 }; // 默认值 // 简单实现:在一定难度范围内随机选取 var tolerance = 0.5; var candidates = _allWords.Where(w => Math.Abs(w.Difficulty - targetDifficulty) <= tolerance).ToList(); if (candidates.Any()) { return candidates[_random.Next(candidates.Count)]; } // 如果没有匹配的,返回随机一个 return _allWords[_random.Next(_allWords.Count)]; } private double CalculateDifficulty(string word) { // 一个简单的难度计算公式示例 double baseScore = word.Length; // 长度越长越难 double specialCharBonus = word.Count(c => !char.IsLetter(c)) * 2; // 非字母字符加分 return baseScore + specialCharBonus; } }

在ViewModel中,通过依赖注入引入IWordPoolService,在游戏开始时调用GetRandomWord来生成单词队列。可以根据玩家当前的表现(连续正确率)动态调整targetDifficulty,实现自适应难度。

3.2 输入检测与实时反馈

这是游戏交互的核心。我们需要在UI上捕获全局键盘事件,或者监听一个输入文本框的变化。在WPF中,更MVVM的方式是绑定到CurrentInput属性。

方案一:使用TextBox绑定(简单直接)在View中,放置一个透明的、或者样式化后的TextBox,将其Text属性双向绑定到ViewModel的CurrentInput

<TextBox Text="{Binding CurrentInput, UpdateSourceTrigger=PropertyChanged}" Background="Transparent" BorderThickness="0" CaretBrush="Transparent" IsTabStop="True" Focusable="True"/>

UpdateSourceTrigger=PropertyChanged使得每次按键都会更新绑定源。然后在ViewModel的CurrentInput属性的setter中,执行验证逻辑ValidateInputAgainstCurrentWord()

方案二:全局键盘钩子(更接近游戏体验)如果你希望即使窗口不是焦点也能接收按键(或者不想显示输入框),可以使用全局键盘钩子。但这涉及Win32 API调用(SetWindowsHookEx),代码更复杂,且需要处理卸载钩子防止内存泄漏。对于大多数打字游戏,方案一已经足够好且更安全。

验证逻辑实现

private void ValidateInputAgainstCurrentWord() { if (string.IsNullOrEmpty(CurrentInput) || !ActiveWords.Any()) return; var currentWord = ActiveWords.First(); // 假设第一个是当前目标单词 var input = CurrentInput; // 构建高亮字符串:正确部分绿色,错误部分红色,未输入部分默认色 StringBuilder highlighted = new StringBuilder(); for (int i = 0; i < currentWord.Content.Length; i++) { if (i < input.Length) { if (input[i] == currentWord.Content[i]) { highlighted.Append($"<Run Foreground=\"Green\">{currentWord.Content[i]}</Run>"); // 记录正确击键 } else { highlighted.Append($"<Run Foreground=\"Red\">{currentWord.Content[i]}</Run>"); // 记录错误击键 } } else { highlighted.Append($"<Run Foreground=\"Gray\">{currentWord.Content[i]}</Run>"); } } CurrentWordHighlighted = highlighted.ToString(); OnPropertyChanged(nameof(CurrentWordHighlighted)); // 通知UI更新 // 如果输入完全匹配,触发单词完成逻辑 if (input == currentWord.Content) { HandleWordCompleted(currentWord); } }

在XAML中,使用TextBlock配合Inline来显示这个带格式的字符串(需要转换器或允许Inline的属性)。

3.3 游戏循环、计时与状态管理

游戏需要一个核心循环来驱动,比如控制单词的生成速度、更新倒计时等。这里可以使用一个主游戏计时器。

private DispatcherTimer _gameTimer; private TimeSpan _timeRemaining; public TimeSpan TimeRemaining { get => _timeRemaining; private set => SetProperty(ref _timeRemaining, value); } private void InitializeGameTimer() { _gameTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) }; // 约60FPS _gameTimer.Tick += OnGameTimerTick; } private void OnGameTimerTick(object sender, EventArgs e) { if (CurrentState != GameState.Playing) return; // 1. 更新游戏时间 TimeRemaining = TimeRemaining.Subtract(TimeSpan.FromMilliseconds(16)); if (TimeRemaining <= TimeSpan.Zero) { EndGame(); return; } // 2. 更新所有活动单词的状态(如下落位置) UpdateActiveWordsPosition(); // 3. 根据时间或条件,生成新单词 TryGenerateNewWord(); // 4. 检查是否有单词“掉落”到底部(失败) CheckForDroppedWords(); } private void TryGenerateNewWord() { _wordGenerationAccumulator += _gameTimer.Interval.TotalMilliseconds; if (_wordGenerationAccumulator >= _wordGenerationIntervalMs) { _wordGenerationAccumulator = 0; var newWord = _wordPoolService.GetRandomWord(CalculateCurrentDifficulty()); // 将新单词加入到ActiveWords集合,UI会自动更新 Application.Current.Dispatcher.Invoke(() => { ActiveWords.Insert(0, newWord); // 从顶部加入 }); } }

注意事项:所有对ObservableCollection的修改(如Add, Insert, Remove)必须在UI线程上执行,否则会抛出异常。这就是上面使用Dispatcher.Invoke的原因。如果计时器本身就是在UI线程创建的DispatcherTimer,则其Tick事件处理程序本身就在UI线程,可以省略Dispatcher调用。

3.4 动画与视觉效果实现

WPF的动画系统非常强大。我们可以为单词的击中、消失、下落等添加效果。

示例:单词被正确输入后的消失动画

  1. 在ViewModel中:当单词完成时,从ActiveWords集合中移除该项。
  2. 在View(XAML)中:为呈现单词的控件(如ListBoxItem)定义DataTemplateStyle,并添加EventTriggerDataTrigger来启动动画。
<!-- 假设使用ListBox显示ActiveWords,ItemTemplate如下 --> <DataTemplate DataType="{x:Type local:WordItem}"> <Border x:Name="WordBorder" Background="LightBlue" Padding="10" Margin="5"> <TextBlock Text="{Binding Content}" FontSize="20" HorizontalAlignment="Center"/> <Border.Triggers> <EventTrigger RoutedEvent="Border.Loaded"> <!-- 可以在这里触发一个初始的下落动画 --> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" From="-50" To="100" Duration="0:0:10" RepeatBehavior="Forever"/> </Storyboard> </BeginStoryboard> </EventTrigger> <!-- 更复杂的状态触发动画可以通过DataTrigger绑定到WordItem的某个状态属性(如IsCompleted)来实现 --> </Border.Triggers> </Border> </DataTemplate>

更高级的做法是,在ViewModel中为WordItem添加一个DisplayState属性,当状态变为Completed时,View通过DataTrigger触发一个放大、淡出并移除的动画。这需要更精细的控件模板和故事板设计。

实操心得:WPF动画性能很好,但滥用也会导致卡顿。避免在短时间内创建大量永不停歇的动画。对于像单词下落这种持续运动,可以考虑在OnGameTimerTick中直接计算并更新单词的Canvas.Top属性(通过绑定),这有时比用Storyboard更高效且易于控制。

4. 数据持久化与配置管理

游戏设置(如难度、时间、主题色)和玩家成绩需要保存。我们可以使用JSON进行序列化。

4.1 使用Newtonsoft.Json或System.Text.Json

.NET Core/5+ 自带的System.Text.Json性能很好,基本够用。如果你需要更灵活的功能(如更宽松的反序列化、自定义转换器),Newtonsoft.Json仍然是经典选择。

安装NuGet包System.Text.JsonNewtonsoft.Json

定义配置类

public class GameSettings { public int GameDurationSeconds { get; set; } = 60; public string Difficulty { get; set; } = "Medium"; public string ThemeColor { get; set; } = "Blue"; public bool SoundEnabled { get; set; } = true; // ... 其他设置 }

持久化服务

public interface ISettingsService { Task<GameSettings> LoadSettingsAsync(); Task SaveSettingsAsync(GameSettings settings); } public class FileSettingsService : ISettingsService { private readonly string _settingsFilePath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MyTypingGame", "settings.json"); public async Task<GameSettings> LoadSettingsAsync() { if (!File.Exists(_settingsFilePath)) { return new GameSettings(); // 返回默认设置 } try { var json = await File.ReadAllTextAsync(_settingsFilePath); return JsonSerializer.Deserialize<GameSettings>(json); } catch (Exception ex) { // 日志记录 System.Diagnostics.Debug.WriteLine($"加载设置失败: {ex.Message}"); return new GameSettings(); } } public async Task SaveSettingsAsync(GameSettings settings) { var directory = Path.GetDirectoryName(_settingsFilePath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } try { var options = new JsonSerializerOptions { WriteIndented = true }; var json = JsonSerializer.Serialize(settings, options); await File.WriteAllTextAsync(_settingsFilePath, json); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"保存设置失败: {ex.Message}"); // 可以考虑抛出异常或提供错误反馈 } } }

在App.xaml.cs或主ViewModel中,通过依赖注入加载和保存设置。

4.2 玩家成绩记录

成绩记录与配置类似,可以序列化一个List<GameSession>PlayerProfile对象。为了更好的查询体验(如按分数排序、按日期筛选),可以考虑嵌入一个轻量级数据库,如SQLiteLiteDB。但对于初学者项目,JSON文件完全足够。

注意事项:序列化时,要处理好循环引用问题(如果你的对象模型中有双向引用)。使用System.Text.Json时,可以配置ReferenceHandler.Preserve或使用[JsonIgnore]特性忽略某些属性。

5. 高级主题与性能优化

当基本功能完成后,可以考虑以下进阶内容来提升项目的完整度和你的技术深度。

5.1 依赖注入与模块化

使用依赖注入容器(如.NET内置的IServiceCollection或第三方库如Autofac)来管理你的服务(IWordPoolService,ISettingsService等)。这会让你的代码更可测试、更灵活。

  1. 在App.xaml.cs中配置服务:
    public partial class App : Application { public IServiceProvider ServiceProvider { get; private set; } protected override void OnStartup(StartupEventArgs e) { var serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); ServiceProvider = serviceCollection.BuildServiceProvider(); var mainWindow = ServiceProvider.GetRequiredService<MainWindow>(); mainWindow.Show(); } private void ConfigureServices(IServiceCollection services) { services.AddSingleton<IWordPoolService, WordPoolService>(); services.AddSingleton<ISettingsService, FileSettingsService>(); services.AddSingleton<GameEngine>(); // 可能的核心游戏逻辑类 services.AddSingleton<TypingGameViewModel>(); services.AddSingleton<MainWindow>(); // 注册窗口本身 } }
  2. 在主窗口的构造函数中注入ViewModel:
    public partial class MainWindow : Window { public MainWindow(TypingGameViewModel viewModel) { InitializeComponent(); DataContext = viewModel; // 设置数据上下文 } }

5.2 单元测试

为你的核心逻辑编写单元测试,特别是ViewModel和Model中的业务规则。使用测试框架如xUnit、NUnit或MSTest。

[TestClass] public class TypingGameViewModelTests { [TestMethod] public void ValidateInput_CompletelyCorrect_ShouldCompleteWord() { // 准备 (Arrange) var mockWordService = new Mock<IWordPoolService>(); var vm = new TypingGameViewModel(mockWordService.Object); var testWord = new WordItem { Content = "hello" }; vm.ActiveWords.Add(testWord); // 执行 (Act) vm.CurrentInput = "hello"; // 断言 (Assert) // 检查ActiveWords中是否移除了testWord // 检查分数是否增加 Assert.IsFalse(vm.ActiveWords.Contains(testWord)); Assert.IsTrue(vm.Score > 0); } [TestMethod] public void CalculateAccuracy_WithMixedKeystrokes_ReturnsCorrectPercentage() { var session = new GameSession { TotalKeystrokes = 100, CorrectKeystrokes = 85 }; Assert.AreEqual(85.0, session.Accuracy); } }

5.3 性能优化点

  1. UI虚拟化:如果单词列表可能很长,确保用于显示列表的控件(如ListBox,ListView)启用了UI虚拟化(VirtualizingStackPanel.IsVirtualizing="True"),这只会渲染可视区域内的项,极大提升滚动性能。
  2. 动画性能:对于大量独立运动的物体(如下落的单词),使用Canvas并在游戏循环中直接更新其位置,可能比为每个物体单独运行一个Storyboard更高效。
  3. 数据绑定优化:避免在getter中执行复杂计算。对于需要频繁计算且依赖其他属性的值,可以考虑使用缓存,或者在依赖属性变化时手动触发计算和通知。
  4. 资源管理:声音、图片等资源在使用后及时释放。对于需要预加载的资源,可以在游戏开始前异步加载。

5.4 可访问性与国际化

  • 可访问性:为控件设置AutomationProperties.Name,方便屏幕阅读器识别。确保键盘可以导航所有功能(Tab键顺序)。
  • 国际化:将UI字符串提取到资源文件(.resx)中,方便支持多语言。

6. 常见问题与调试技巧

在开发过程中,你肯定会遇到各种问题。这里记录一些典型场景和解决思路。

6.1 数据绑定失败

  • 症状:UI不更新,输出窗口显示绑定错误。
  • 排查
    1. 检查DataContext是否正确设置。可以在XAML中临时添加一个TextBlock绑定到一个简单属性测试。
    2. 检查属性是否实现了INotifyPropertyChanged接口,并且属性名在OnPropertyChanged方法中拼写正确。使用nameof操作符可以避免拼写错误。
    3. 对于集合(如ObservableCollection),确保修改(增、删、改)是在UI线程上进行的。
    4. 检查绑定模式(Mode)和更新触发器(UpdateSourceTrigger)是否合适。

6.2 动画或游戏循环卡顿

  • 症状:界面反应迟钝,动画不流畅。
  • 排查
    1. 使用性能分析工具(如Visual Studio的性能探查器)找出热点。
    2. 检查OnGameTimerTick事件处理函数中的逻辑是否过于耗时。将复杂的计算(如路径查找、大量字符串操作)移到后台线程(Task.Run),然后通过Dispatcher.Invoke更新UI。
    3. 减少不必要的布局更新和渲染。例如,频繁改变控件宽度/高度会触发整个布局系统的重新计算。
    4. 确保没有内存泄漏,特别是事件订阅(如Timer.Tick)在不需要时要记得取消订阅。

6.3 击键事件处理异常

  • 症状:输入不响应,或焦点问题。
  • 排查
    1. 确保输入控件(TextBox)是FocusableIsTabStop的,并且获得了焦点。可以在窗口加载后调用YourTextBox.Focus()
    2. 如果使用全局钩子,确保在窗口失去焦点或关闭时正确卸载钩子。
    3. 处理特殊按键,如Backspace,Enter,Escape。在CurrentInputsetter或按键事件中过滤这些按键,执行特殊逻辑(如删除字符、提交单词、暂停游戏)。

6.4 配置或成绩文件无法读写

  • 症状:设置无法保存,或读取时返回默认值。
  • 排查
    1. 检查文件路径的目录是否存在,程序是否有写入权限。使用Environment.SpecialFolder来获取合适的应用数据目录。
    2. 在序列化/反序列化时使用try-catch块,并记录异常信息。
    3. 检查JSON文件的格式是否正确。可以使用在线JSON验证工具。

6.5 MVVM中命令无法触发

  • 症状:按钮点击没反应。
  • 排查
    1. 检查命令的CanExecute方法返回值。按钮在CanExecute返回false时会自动禁用。
    2. 确保在CanExecute条件变化时,调用了ICommandCanExecuteChanged事件(RelayCommand提供了NotifyCanExecuteChanged方法)。
    3. 检查绑定语法:Command="{Binding StartGameCommand}"

开发这样一个项目,最大的收获往往不是最终的游戏本身,而是在解决上述一个个具体问题过程中,对C#语言特性、.NET框架、WPF技术栈和软件设计原则的深刻理解。从事件处理到异步编程,从数据绑定到依赖注入,从基础算法到性能调优,每一个环节都值得深入挖掘。当你把这个项目完整地实现一遍,并且尝试去优化、扩展它时,你所积累的经验将远超完成一个简单的教程作业。

返回列表