C# MVVM实战:从零开始构建一个WPF登录应用(含完整代码)
C# MVVM实战从零开始构建一个WPF登录应用含完整代码如果你是一位C#开发者正在从WinForms或简单的WPF事件驱动模式转向更结构化的开发方式那么MVVM架构很可能已经出现在你的雷达上。它听起来很美好——清晰的职责分离、易于测试、UI与逻辑解耦——但当你真正打开Visual Studio面对一个空白的WPF项目时那种“从何下手”的茫然感可能会瞬间袭来。理论文章读了不少可一到实战绑定不生效、命令没反应、数据不更新一堆问题接踵而至。这篇文章就是为你准备的。我们不谈空洞的概念直接动手用大约三百行代码构建一个功能完整、结构清晰的WPF登录应用。你会看到INotifyPropertyChanged如何让界面“活”起来ICommand如何优雅地处理按钮点击以及ViewModel如何成为连接数据和界面的桥梁。整个过程就像搭积木我们会从最基础的Model开始一步步组装直到看到一个可以交互、有基础验证的登录窗口。准备好了吗让我们打开Visual Studio开始这次实战之旅。1. 项目初始化与核心Model构建在开始敲代码之前我们先明确这个登录应用需要什么。最核心的是一个代表“用户”的数据模型。在MVVM的世界里Model是业务的基石它不关心界面长什么样只关心数据本身和与数据相关的核心规则。1.1 创建项目与解决方案结构首先启动Visual Studio2022或2019均可创建一个新的WPF应用项目命名为WpfLoginDemo。为了保持代码的整洁和可维护性我强烈建议在项目中建立清晰的文件夹结构。这不仅仅是美观更是为后续可能的功能扩展打下基础。在“解决方案资源管理器”中右键点击项目依次创建以下几个文件夹Models: 用于存放所有数据模型类。ViewModels: 用于存放所有视图模型类。Views: 用于存放所有窗口和用户控件XAML文件。Common或Utilities: 用于存放一些通用的辅助类比如我们后面会用到的命令中继器。提示这种文件夹结构是中型WPF项目的常见实践。对于非常小的demo你可以不这么做但养成习惯对未来大有裨益。1.2 实现User模型类现在让我们在Models文件夹下创建第一个类。右键点击Models文件夹选择“添加” - “类”命名为User.cs。这个User类非常简单它就是一个纯数据对象POCO。它的职责是定义用户有哪些属性以及封装一些最基础、不依赖于任何UI框架的业务规则。注意它没有任何对WPF命名空间的引用。namespace WpfLoginDemo.Models { public class User { public string Username { get; set; } public string Password { get; set; } // 一个简单的模型层验证示例 public bool ValidateCredentials(string inputUsername, string inputPassword) { // 在实际项目中这里可能是查询数据库或调用API // 此处仅为演示进行简单的非空和相等判断 return !string.IsNullOrEmpty(inputUsername) !string.IsNullOrEmpty(inputPassword) inputUsername Username inputPassword Password; } } }这个模型类有几个关键点属性简单明了只有用户名和密码。在实际场景中你可能会加入邮箱、ID、创建时间等更多属性。包含业务逻辑ValidateCredentials方法封装了“验证凭据”这个业务规则。虽然这里逻辑很简单但它体现了Model的职责——处理数据逻辑。未来这个方法内部可以轻松替换为调用Web API或查询数据库而不会影响上层的ViewModel和View。独立于UI整个类没有引用System.Windows或任何XAML相关的东西这意味着你可以把它用在控制台应用、单元测试或者其他任何类型的.NET项目中。Model构建完成后我们的数据基石就准备好了。接下来我们需要一个“翻译官”和“调度员”将Model的数据转换成View能显示的样子并处理View发来的用户操作指令这就是ViewModel的舞台。2. ViewModel数据与命令的中枢如果说Model是后台的仓库管理员那么ViewModel就是前台的客户经理。它从Model那里取货数据进行适当的包装和整理格式转换、状态管理然后展示给客户View。同时它也接收客户的订单用户操作并转交给仓库去处理。这个过程中最关键的两个机制是属性变更通知和命令绑定。2.1 实现属性变更通知 (INotifyPropertyChanged)WPF的数据绑定之所以强大是因为它支持双向通信。当界面上的文本框内容改变时后台的数据要能自动更新反之当后台数据改变时界面也要能自动刷新。这一切的魔法都源于INotifyPropertyChanged接口。我们先在ViewModels文件夹下创建LoginViewModel.cs。为了让其属性支持通知我们需要实现这个接口。using System.ComponentModel; using System.Runtime.CompilerServices; using WpfLoginDemo.Models; namespace WpfLoginDemo.ViewModels { public class LoginViewModel : INotifyPropertyChanged { private string _username; private string _password; private string _statusMessage; public event PropertyChangedEventHandler? PropertyChanged; // 这是触发属性变更通知的核心方法 protected virtual void OnPropertyChanged([CallerMemberName] string propertyName null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public string Username { get _username; set { if (_username ! value) { _username value; OnPropertyChanged(); // 通知UIUsername属性已变更 } } } public string Password { get _password; set { if (_password ! value) { _password value; OnPropertyChanged(); // 通知UIPassword属性已变更 } } } public string StatusMessage { get _statusMessage; set { if (_statusMessage ! value) { _statusMessage value; OnPropertyChanged(); } } } } }代码解析INotifyPropertyChanged接口要求我们实现一个PropertyChanged事件。OnPropertyChanged方法是一个辅助方法用于安全地触发这个事件。[CallerMemberName]这个特性是C#的语法糖它能让编译器自动将调用此方法处的属性名如Username作为参数传入这样我们写OnPropertyChanged()就行了无需手动写OnPropertyChanged(nameof(Username))既简洁又不易出错。在每个属性的setter中我们判断新值是否与旧值不同。如果不同则更新字段并调用OnPropertyChanged()。这个判断很重要避免了不必要的UI刷新。2.2 实现命令与中继器 (ICommand)在MVVM中我们不用传统的Click事件处理程序而是使用命令Command。命令将用户操作如点击按钮抽象成一个可绑定的对象它自带一个“是否可以执行”的状态。WPF的按钮绑定命令后会自动根据这个状态来启用或禁用自身。.NET提供了ICommand接口但我们需要一个具体的实现。通常我们会创建一个通用的RelayCommand或DelegateCommand。在Common文件夹下创建RelayCommand.cs。using System; using System.Windows.Input; namespace WpfLoginDemo.Common { public class RelayCommand : ICommand { private readonly Actionobject _execute; private readonly Predicateobject _canExecute; public RelayCommand(Actionobject execute, Predicateobject canExecute null) { _execute execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute canExecute; } public bool CanExecute(object parameter) { return _canExecute null || _canExecute(parameter); } public void Execute(object parameter) { _execute(parameter); } // 这个事件是ICommand接口要求的用于在命令的可执行状态可能改变时通知UI public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested value; } remove { CommandManager.RequerySuggested - value; } } } }这个RelayCommand类封装了执行逻辑(_execute)和判断逻辑(_canExecute)。CommandManager.RequerySuggested是一个WPF内置的机制它会在UI操作如输入文本、焦点变化时自动触发询问所有命令的CanExecute状态是否改变从而自动更新按钮的启用状态。现在我们回到LoginViewModel添加登录命令和相关的业务逻辑。using System.Windows.Input; using WpfLoginDemo.Common; using WpfLoginDemo.Models; namespace WpfLoginDemo.ViewModels { public class LoginViewModel : INotifyPropertyChanged { private User _user; // ... 之前的属性Username, Password, StatusMessage和事件 ... public ICommand LoginCommand { get; } public ICommand ResetCommand { get; } public LoginViewModel() { // 初始化一个模拟用户实际项目中应从服务加载 _user new User { Username admin, Password 123456 }; StatusMessage 请输入用户名和密码。; // 初始化命令 LoginCommand new RelayCommand(ExecuteLogin, CanExecuteLogin); ResetCommand new RelayCommand(ExecuteReset); } private bool CanExecuteLogin(object parameter) { // 只有当用户名和密码都不为空时登录按钮才可用 return !string.IsNullOrWhiteSpace(Username) !string.IsNullOrWhiteSpace(Password); } private void ExecuteLogin(object parameter) { // 调用Model的业务逻辑进行验证 bool isValid _user.ValidateCredentials(Username, Password); if (isValid) { StatusMessage $登录成功欢迎您{Username}。; // 在实际应用中这里通常会导航到主窗口或关闭登录窗口 } else { StatusMessage 登录失败用户名或密码错误。; } } private void ExecuteReset(object parameter) { Username string.Empty; Password string.Empty; StatusMessage 信息已重置。; } } }至此ViewModel的核心部分已经完成。它拥有数据属性(Username,Password,StatusMessage): 与View双向绑定。命令(LoginCommand,ResetCommand): 响应View的用户交互。业务协调在ExecuteLogin中它协调了Model(_user.ValidateCredentials) 和自身状态 (StatusMessage) 的更新。ViewModel已经准备就绪它正等待着与一个漂亮的界面连接起来。3. View使用XAML构建用户界面View是用户看到并与之交互的部分。在WPF中View主要由XAML文件定义。我们的目标是创建一个清晰、可绑定的登录界面将UI控件与ViewModel中的属性和命令连接起来。这里的关键在于理解并正确使用数据绑定语法。3.1 设计登录窗口布局首先我们需要设置窗口的DataContext。DataContext是整个数据绑定的源头窗口内的所有控件默认都会沿着视觉树向上查找直到找到它。最直接的方法是在窗口的后台代码.xaml.cs中设置。不过更符合MVVM“松耦合”思想的做法是在XAML中通过DataContext属性进行绑定或者使用像ViewModelLocator这样的模式。为了简单直观我们这次在后台代码中设置。打开MainWindow.xaml.cs。using System.Windows; using WpfLoginDemo.ViewModels; namespace WpfLoginDemo { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); // 将ViewModel实例设置为窗口的数据上下文 this.DataContext new LoginViewModel(); } } }现在MainWindow的所有子控件都能访问到LoginViewModel的公共属性和命令了。接下来我们设计MainWindow.xaml。3.2 实现数据绑定与命令绑定打开MainWindow.xaml我们将使用Grid和StackPanel进行基础布局并应用绑定。Window x:ClassWpfLoginDemo.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml TitleWPF MVVM 登录演示 Height300 Width400 WindowStartupLocationCenterScreen Grid Margin20 Grid.RowDefinitions RowDefinition HeightAuto/ RowDefinition HeightAuto/ RowDefinition HeightAuto/ RowDefinition HeightAuto/ RowDefinition Height*/ RowDefinition HeightAuto/ /Grid.RowDefinitions Grid.ColumnDefinitions ColumnDefinition WidthAuto/ ColumnDefinition Width*/ /Grid.ColumnDefinitions !-- 标题 -- TextBlock Text用户登录 FontSize20 FontWeightBold Grid.ColumnSpan2 HorizontalAlignmentCenter Margin0,0,0,20/ !-- 用户名行 -- TextBlock Text用户名 VerticalAlignmentCenter Grid.Row1/ TextBox x:NameUsernameBox Grid.Row1 Grid.Column1 Margin5 Text{Binding Username, UpdateSourceTriggerPropertyChanged}/ !-- 密码行 -- TextBlock Text密 码 VerticalAlignmentCenter Grid.Row2/ PasswordBox x:NamePasswordBox Grid.Row2 Grid.Column1 Margin5 Password{Binding Password, UpdateSourceTriggerPropertyChanged}/ !-- 状态信息行 -- TextBlock Text状态 VerticalAlignmentCenter Grid.Row3/ Border Grid.Row3 Grid.Column1 Background#FFF0F0F0 CornerRadius3 Padding8 Margin5 TextBlock x:NameStatusBlock Text{Binding StatusMessage} TextWrappingWrap/ /Border !-- 按钮行 -- StackPanel OrientationHorizontal HorizontalAlignmentRight Grid.Row5 Grid.ColumnSpan2 Button Content登录 Command{Binding LoginCommand} Margin5 Padding15,5 IsDefaultTrue/ Button Content重置 Command{Binding ResetCommand} Margin5 Padding15,5/ /StackPanel /Grid /Window绑定关键点解析Text{Binding Username, UpdateSourceTriggerPropertyChanged}:{Binding Username}表示将此文本框的Text属性绑定到DataContext即我们的LoginViewModel的Username属性。UpdateSourceTriggerPropertyChanged是这里的一个重要技巧。默认情况下TextBox的绑定在控件失去焦点时才会更新源ViewModel。加上这个设置后每次用户按键ViewModel中的属性都会立即更新。这对于我们实时判断登录按钮是否可用CanExecuteLogin至关重要。Password{Binding Password, ...}:注意我们使用的是PasswordBox控件它绑定的是Password属性而不是Text。绑定原理与用户名框相同。Command{Binding LoginCommand}:将按钮的Command属性绑定到ViewModel的LoginCommand属性。当按钮被点击会自动调用命令的Execute方法。IsDefaultTrue属性将这个按钮设置为窗口的默认按钮用户按回车键即可触发登录。Text{Binding StatusMessage}:状态文本绑定到StatusMessage属性。当ViewModel中登录成功或失败时修改StatusMessage属性由于我们实现了INotifyPropertyChanged这个文本块的内容会自动更新。现在按下F5运行程序。你会看到一个登录窗口。尝试输入用户名和密码你会发现当两个输入框都为空时“登录”按钮是禁用状态。当你开始输入“登录”按钮会自动变为启用状态这得益于CanExecuteLogin逻辑和UpdateSourceTriggerPropertyChanged。输入正确的凭据admin/123456并点击登录下方会显示成功信息。点击“重置”按钮会清空输入框和状态信息。一个具备基本交互功能的MVVM登录应用已经完成了但这只是开始。一个健壮的应用还需要输入验证、更好的用户体验和可测试性。4. 进阶输入验证、依赖注入与单元测试基础功能跑通后我们可以从“能用”向“好用”和“健壮”迈进。这部分我们将探讨三个进阶主题如何为输入添加验证反馈如何用依赖注入管理ViewModel依赖以及如何为ViewModel编写单元测试。4.1 实现数据验证与UI反馈目前我们的验证逻辑只在点击“登录”时在ViewModel中进行。但更好的用户体验是在用户输入时就给予反馈。WPF提供了强大的数据验证框架通常通过IDataErrorInfo接口或INotifyDataErrorInfo接口实现。这里我们使用更现代的INotifyDataErrorInfo。首先我们创建一个支持验证的ViewModel基类ValidatableViewModel。using System.Collections.Generic; using System.ComponentModel; using System.Linq; namespace WpfLoginDemo.ViewModels { public abstract class ValidatableViewModel : INotifyPropertyChanged, INotifyDataErrorInfo { private readonly Dictionarystring, Liststring _errors new Dictionarystring, Liststring(); public event PropertyChangedEventHandler? PropertyChanged; public event EventHandlerDataErrorsChangedEventArgs? ErrorsChanged; public bool HasErrors _errors.Any(); public System.Collections.IEnumerable GetErrors(string? propertyName) { if (string.IsNullOrEmpty(propertyName) || !_errors.ContainsKey(propertyName)) return Enumerable.Emptystring(); return _errors[propertyName]; } protected void SetError(string propertyName, string errorMessage) { if (!_errors.ContainsKey(propertyName)) _errors[propertyName] new Liststring(); if (!_errors[propertyName].Contains(errorMessage)) { _errors[propertyName].Add(errorMessage); OnErrorsChanged(propertyName); } } protected void ClearError(string propertyName, string errorMessage null) { if (_errors.ContainsKey(propertyName)) { if (string.IsNullOrEmpty(errorMessage)) { _errors.Remove(propertyName); } else { _errors[propertyName].Remove(errorMessage); if (_errors[propertyName].Count 0) _errors.Remove(propertyName); } OnErrorsChanged(propertyName); } } protected virtual void OnErrorsChanged(string propertyName) { ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); } protected virtual void OnPropertyChanged([CallerMemberName] string propertyName null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }然后让LoginViewModel继承这个基类并在属性设置器中添加验证逻辑。public class LoginViewModel : ValidatableViewModel { // ... 其他字段和属性 ... public string Username { get _username; set { if (_username ! value) { _username value; OnPropertyChanged(); // 验证逻辑 ClearError(nameof(Username)); if (string.IsNullOrWhiteSpace(value)) { SetError(nameof(Username), 用户名不能为空。); } else if (value.Length 3) { SetError(nameof(Username), 用户名至少需要3个字符。); } } } } // Password属性也可以添加类似的验证如最小长度、复杂度要求等。 }最后在XAML中我们需要让控件知道如何显示这些验证错误。WPF通常使用Validation.ErrorTemplate。我们可以修改TextBox的样式或者使用一个简单的ToolTip来显示错误。这里我们修改用户名和密码的XAML添加一个TextBlock来显示错误信息。!-- 用户名行修改后 -- TextBlock Text用户名 VerticalAlignmentCenter Grid.Row1/ StackPanel Grid.Row1 Grid.Column1 TextBox x:NameUsernameBox Margin5,0,5,0 Text{Binding Username, UpdateSourceTriggerPropertyChanged, ValidatesOnNotifyDataErrorsTrue}/ TextBlock Text{Binding (Validation.Errors)[0].ErrorContent, ElementNameUsernameBox} ForegroundRed FontSize10 Margin5,2 Visibility{Binding (Validation.HasError), ElementNameUsernameBox, Converter{StaticResource BooleanToVisibilityConverter}}/ /StackPanel注意ValidatesOnNotifyDataErrorsTrue这个绑定属性它告诉绑定引擎使用我们实现的INotifyDataErrorInfo进行验证。下面的TextBlock则通过Validation附加属性获取并显示第一条错误信息。你需要为窗口或应用资源字典添加一个BooleanToVisibilityConverter。4.2 引入依赖注入在MainWindow.xaml.cs中我们直接new了一个LoginViewModel。在更复杂的应用中ViewModel可能依赖其他服务如用户认证服务、数据服务。直接new会导致紧耦合难以测试和替换。引入一个简单的依赖注入容器可以解决这个问题。我们可以使用微软的Microsoft.Extensions.DependencyInjection包。通过NuGet安装后在App.xaml.cs中配置服务。using Microsoft.Extensions.DependencyInjection; using System.Windows; using WpfLoginDemo.ViewModels; namespace WpfLoginDemo { 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.GetRequiredServiceMainWindow(); mainWindow.Show(); } private void ConfigureServices(IServiceCollection services) { // 注册ViewModels services.AddTransientLoginViewModel(); // 注册Views (Windows/UserControls) services.AddSingletonMainWindow(); // 注册其他服务如 IUserService, IDataRepository 等 // services.AddScopedIUserService, UserService(); } } }然后修改MainWindow的构造函数通过构造函数注入ViewModel。public partial class MainWindow : Window { public MainWindow(LoginViewModel viewModel) { InitializeComponent(); this.DataContext viewModel; // 从外部注入而非内部创建 } }同时需要修改App.xaml移除StartupUri。Application x:ClassWpfLoginDemo.App xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml Application.Resources /Application.Resources /Application这样做的好处是MainWindow不再负责创建LoginViewModel它只负责使用。所有依赖关系的组装都在App类中完成代码更清晰也更容易进行单元测试。4.3 为ViewModel编写单元测试MVVM的一大优势就是可测试性。ViewModel不依赖UI我们可以用单元测试框架如xUnit、NUnit或MSTest轻松测试其逻辑。假设我们使用MSTest创建一个新的单元测试项目并引用主项目。然后为LoginViewModel编写测试。using Microsoft.VisualStudio.TestTools.UnitTesting; using WpfLoginDemo.ViewModels; using WpfLoginDemo.Models; namespace WpfLoginDemo.Tests { [TestClass] public class LoginViewModelTests { [TestMethod] public void LoginCommand_CanExecute_ReturnsFalse_WhenCredentialsEmpty() { // Arrange var vm new LoginViewModel(); vm.Username ; vm.Password ; var command vm.LoginCommand as Common.RelayCommand; // Act bool canExecute command.CanExecute(null); // Assert Assert.IsFalse(canExecute); } [TestMethod] public void LoginCommand_CanExecute_ReturnsTrue_WhenCredentialsProvided() { // Arrange var vm new LoginViewModel(); vm.Username test; vm.Password pass; var command vm.LoginCommand as Common.RelayCommand; // Act bool canExecute command.CanExecute(null); // Assert Assert.IsTrue(canExecute); } [TestMethod] public void ExecuteLogin_UpdatesStatusMessage_OnSuccessfulLogin() { // Arrange var vm new LoginViewModel(); // ViewModel构造函数中创建了默认用户 admin/123456 vm.Username admin; vm.Password 123456; string initialStatus vm.StatusMessage; var command vm.LoginCommand as Common.RelayCommand; // Act command.Execute(null); // Assert Assert.AreNotEqual(initialStatus, vm.StatusMessage); StringAssert.Contains(vm.StatusMessage, 成功); } } }这些测试验证了当用户名和密码为空时登录命令不可执行。当提供了凭据时登录命令可以执行。使用正确的凭据执行登录命令后状态信息会更新为包含“成功”字样。通过编写这样的测试你可以确保业务逻辑的准确性并在未来修改代码时快速发现回归错误。这才是MVVM架构在长期项目维护中体现出的巨大价值。从创建一个简单的User模型到实现双向绑定的ViewModel再到设计交互式的View最后为应用加上验证、改进架构并使其可测试我们完成了一个小型但完整的MVVM应用闭环。这个过程可能一开始会觉得有些繁琐但一旦习惯你会发现它带来的代码组织性、可维护性和可测试性是传统事件驱动代码难以比拟的。下次当你需要处理更复杂的表单、列表或导航时这套模式将成为你得心应手的工具。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2409752.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!