MVVM 模式 #
MVVM 概述 #
什么是 MVVM? #
MVVM (Model-View-ViewModel) 是一种软件架构模式,专门用于分离用户界面 (UI) 和业务逻辑。
text
┌─────────────────────────────────────────────────────────────┐
│ MVVM 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ View │ 用户界面层 │
│ │ (XAML 页面) │ - 显示数据 │
│ └────────┬────────┘ - 用户交互 │
│ │ │
│ │ 数据绑定 │
│ │ │
│ ┌────────▼────────┐ │
│ │ ViewModel │ 视图模型层 │
│ │ (C# 类) │ - 业务逻辑 │
│ └────────┬────────┘ - 状态管理 │
│ │ - 命令处理 │
│ │ │
│ │ 数据访问 │
│ │ │
│ ┌────────▼────────┐ │
│ │ Model │ 数据模型层 │
│ │ (数据实体) │ - 数据结构 │
│ └─────────────────┘ - 数据访问 │
│ │
└─────────────────────────────────────────────────────────────┘
MVVM 的优势 #
text
┌─────────────────────────────────────────────────────────────┐
│ MVVM 优势 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 关注点分离 │
│ - UI 与业务逻辑分离 │
│ - 代码职责清晰 │
│ - 易于理解和维护 │
│ │
│ ✅ 可测试性 │
│ - ViewModel 可单元测试 │
│ - 不依赖 UI 框架 │
│ - 测试驱动开发友好 │
│ │
│ ✅ 可复用性 │
│ - ViewModel 可跨平台复用 │
│ - 多个 View 可共享 ViewModel │
│ - 组件化开发 │
│ │
│ ✅ 设计师与开发者协作 │
│ - 设计师专注 XAML │
│ - 开发者专注 ViewModel │
│ - 并行开发 │
│ │
└─────────────────────────────────────────────────────────────┘
MVVM 各层职责 #
text
┌─────────────────────────────────────────────────────────────┐
│ 各层职责 │
├─────────────────────────────────────────────────────────────┤
│ │
│ View (视图): │
│ ├── XAML 文件定义 UI 结构 │
│ ├── .xaml.cs 代码隐藏文件 │
│ ├── 数据绑定到 ViewModel │
│ ├── 处理 UI 特定逻辑(动画、视觉效果) │
│ └── 不应包含业务逻辑 │
│ │
│ ViewModel (视图模型): │
│ ├── 实现 INotifyPropertyChanged │
│ ├── 暴露属性供 View 绑定 │
│ ├── 实现 ICommand 处理用户操作 │
│ ├── 调用 Model 层获取/保存数据 │
│ └── 不应引用 UI 控件 │
│ │
│ Model (模型): │
│ ├── 数据实体类 │
│ ├── 数据访问逻辑 │
│ ├── 业务规则验证 │
│ └── 服务接口 │
│ │
└─────────────────────────────────────────────────────────────┘
ViewModel 基础 #
INotifyPropertyChanged 接口 #
csharp
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
ViewModelBase 实现 #
csharp
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
创建 ViewModel #
csharp
public class MainViewModel : ViewModelBase
{
private string _title;
private string _username;
private bool _isLoading;
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
public string Username
{
get => _username;
set => SetProperty(ref _username, value);
}
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
public MainViewModel()
{
Title = "主页";
Username = string.Empty;
}
}
关联 View 和 ViewModel #
csharp
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = new MainViewModel();
}
}
xml
<ContentPage.BindingContext>
<local:MainViewModel />
</ContentPage.BindingContext>
Command 模式 #
ICommand 接口 #
csharp
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
Command 实现 #
csharp
public class MainViewModel : ViewModelBase
{
public ICommand SubmitCommand { get; }
public ICommand NavigateCommand { get; }
public MainViewModel()
{
SubmitCommand = new Command(OnSubmit, CanSubmit);
NavigateCommand = new Command<string>(OnNavigate);
}
private void OnSubmit()
{
}
private bool CanSubmit()
{
return !string.IsNullOrEmpty(Username);
}
private void OnNavigate(string page)
{
}
}
带参数的 Command #
csharp
public class ListViewModel : ViewModelBase
{
public ICommand ItemSelectedCommand { get; }
public ICommand DeleteCommand { get; }
public ListViewModel()
{
ItemSelectedCommand = new Command<Item>(OnItemSelected);
DeleteCommand = new Command<Item>(OnDelete, CanDelete);
}
private void OnItemSelected(Item item)
{
if (item != null)
{
}
}
private void OnDelete(Item item)
{
Items.Remove(item);
}
private bool CanDelete(Item item)
{
return item != null && item.CanDelete;
}
}
Command 绑定 #
xml
<StackLayout>
<Entry Text="{Binding Username}" />
<Button Text="提交"
Command="{Binding SubmitCommand}" />
<Button Text="导航"
Command="{Binding NavigateCommand}"
CommandParameter="DetailPage" />
</StackLayout>
刷新 Command 可执行状态 #
csharp
public class MainViewModel : ViewModelBase
{
private string _username;
public string Username
{
get => _username;
set
{
if (SetProperty(ref _username, value))
{
(SubmitCommand as Command)?.ChangeCanExecute();
}
}
}
public ICommand SubmitCommand { get; }
public MainViewModel()
{
SubmitCommand = new Command(OnSubmit, CanSubmit);
}
private bool CanSubmit()
{
return !string.IsNullOrWhiteSpace(Username);
}
}
AsyncCommand #
csharp
public class AsyncCommand : ICommand
{
private readonly Func<Task> _execute;
private readonly Func<bool> _canExecute;
private bool _isExecuting;
public event EventHandler CanExecuteChanged;
public AsyncCommand(Func<Task> execute, Func<bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return !_isExecuting && (_canExecute?.Invoke() ?? true);
}
public async void Execute(object parameter)
{
if (!CanExecute(parameter))
return;
try
{
_isExecuting = true;
await _execute();
}
finally
{
_isExecuting = false;
RaiseCanExecuteChanged();
}
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
csharp
public class MainViewModel : ViewModelBase
{
public ICommand LoadDataCommand { get; }
public MainViewModel()
{
LoadDataCommand = new AsyncCommand(LoadDataAsync);
}
private async Task LoadDataAsync()
{
IsLoading = true;
try
{
var data = await _dataService.GetDataAsync();
Items = new ObservableCollection<Item>(data);
}
finally
{
IsLoading = false;
}
}
}
数据绑定进阶 #
属性依赖 #
csharp
public class OrderViewModel : ViewModelBase
{
private decimal _subtotal;
private decimal _taxRate;
public decimal Subtotal
{
get => _subtotal;
set
{
if (SetProperty(ref _subtotal, value))
{
OnPropertyChanged(nameof(Tax));
OnPropertyChanged(nameof(Total));
}
}
}
public decimal TaxRate
{
get => _taxRate;
set
{
if (SetProperty(ref _taxRate, value))
{
OnPropertyChanged(nameof(Tax));
OnPropertyChanged(nameof(Total));
}
}
}
public decimal Tax => Subtotal * TaxRate;
public decimal Total => Subtotal + Tax;
}
集合属性 #
csharp
public class ListViewModel : ViewModelBase
{
private ObservableCollection<Item> _items;
private Item _selectedItem;
public ObservableCollection<Item> Items
{
get => _items;
set => SetProperty(ref _items, value);
}
public Item SelectedItem
{
get => _selectedItem;
set => SetProperty(ref _selectedItem, value);
}
public ICommand AddCommand { get; }
public ICommand RemoveCommand { get; }
public ListViewModel()
{
Items = new ObservableCollection<Item>();
AddCommand = new Command(OnAdd);
RemoveCommand = new Command<Item>(OnRemove);
}
private void OnAdd()
{
Items.Add(new Item { Title = $"Item {Items.Count + 1}" });
}
private void OnRemove(Item item)
{
if (item != null)
{
Items.Remove(item);
}
}
}
服务注入 #
依赖注入概述 #
text
┌─────────────────────────────────────────────────────────────┐
│ 依赖注入 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 传统方式: │
│ ViewModel 直接创建 Service 实例 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ViewModel │ │
│ │ ├── new DataService() ❌ 紧耦合 │ │
│ │ └── new NavigationService() ❌ 难以测试 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 依赖注入: │
│ 通过构造函数注入依赖 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ViewModel │ │
│ │ ├── IDataService _dataService ✅ 接口依赖 │ │
│ │ └── INavigationService _navService ✅ 易于模拟 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
定义服务接口 #
csharp
public interface IDataService
{
Task<IEnumerable<Item>> GetItemsAsync();
Task<Item> GetItemAsync(int id);
Task<bool> SaveItemAsync(Item item);
Task<bool> DeleteItemAsync(int id);
}
public interface INavigationService
{
Task NavigateToAsync(string route);
Task GoBackAsync();
}
public interface IDialogService
{
Task ShowAlertAsync(string title, string message, string cancel);
Task<bool> ShowConfirmAsync(string title, string message, string accept, string cancel);
}
实现服务 #
csharp
public class DataService : IDataService
{
private readonly HttpClient _httpClient;
public DataService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<IEnumerable<Item>> GetItemsAsync()
{
var response = await _httpClient.GetAsync("api/items");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<IEnumerable<Item>>(json);
}
public async Task<Item> GetItemAsync(int id)
{
var response = await _httpClient.GetAsync($"api/items/{id}");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Item>(json);
}
public async Task<bool> SaveItemAsync(Item item)
{
var json = JsonConvert.SerializeObject(item);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("api/items", content);
return response.IsSuccessStatusCode;
}
public async Task<bool> DeleteItemAsync(int id)
{
var response = await _httpClient.DeleteAsync($"api/items/{id}");
return response.IsSuccessStatusCode;
}
}
配置依赖注入 #
csharp
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddServices(this IServiceCollection services)
{
services.AddSingleton<IDataService, DataService>();
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IDialogService, DialogService>();
services.AddTransient<MainViewModel>();
services.AddTransient<DetailViewModel>();
services.AddTransient<SettingsViewModel>();
return services;
}
}
csharp
public partial class App : Application
{
public static IServiceProvider ServiceProvider { get; private set; }
public App()
{
InitializeComponent();
var services = new ServiceCollection();
services.AddServices();
ServiceProvider = services.BuildServiceProvider();
MainPage = new AppShell();
}
}
在 ViewModel 中使用依赖注入 #
csharp
public class MainViewModel : ViewModelBase
{
private readonly IDataService _dataService;
private readonly INavigationService _navigationService;
private readonly IDialogService _dialogService;
public MainViewModel(
IDataService dataService,
INavigationService navigationService,
IDialogService dialogService)
{
_dataService = dataService;
_navigationService = navigationService;
_dialogService = dialogService;
LoadCommand = new AsyncCommand(LoadDataAsync);
}
public ICommand LoadCommand { get; }
private async Task LoadDataAsync()
{
IsLoading = true;
try
{
var items = await _dataService.GetItemsAsync();
Items = new ObservableCollection<Item>(items);
}
catch (Exception ex)
{
await _dialogService.ShowAlertAsync("错误", ex.Message, "确定");
}
finally
{
IsLoading = false;
}
}
}
项目结构 #
推荐的项目结构 #
text
MyApp/
├── Models/ # 数据模型
│ ├── Item.cs
│ └── User.cs
│
├── ViewModels/ # 视图模型
│ ├── ViewModelBase.cs
│ ├── MainViewModel.cs
│ ├── DetailViewModel.cs
│ └── SettingsViewModel.cs
│
├── Views/ # 视图
│ ├── MainPage.xaml
│ ├── MainPage.xaml.cs
│ ├── DetailPage.xaml
│ └── DetailPage.xaml.cs
│
├── Services/ # 服务
│ ├── IDataService.cs
│ ├── DataService.cs
│ ├── INavigationService.cs
│ └── NavigationService.cs
│
├── Converters/ # 值转换器
│ ├── BoolToColorConverter.cs
│ └── DateFormatConverter.cs
│
├── Helpers/ # 辅助类
│ └── AsyncCommand.cs
│
├── Resources/ # 资源
│ ├── Styles/
│ └── Images/
│
├── App.xaml
├── App.xaml.cs
└── AppShell.xaml
MVVM 最佳实践 #
1. 保持 ViewModel 纯净 #
csharp
public class BadViewModel : ViewModelBase
{
private async void OnSubmit()
{
await Application.Current.MainPage.DisplayAlert("提示", "成功", "确定");
}
}
public class GoodViewModel : ViewModelBase
{
private readonly IDialogService _dialogService;
public GoodViewModel(IDialogService dialogService)
{
_dialogService = dialogService;
}
private async Task OnSubmitAsync()
{
await _dialogService.ShowAlertAsync("提示", "成功", "确定");
}
}
2. 使用异步方法 #
csharp
public class MainViewModel : ViewModelBase
{
public ICommand LoadCommand { get; }
public MainViewModel()
{
LoadCommand = new AsyncCommand(LoadAsync);
}
private async Task LoadAsync()
{
IsLoading = true;
try
{
var data = await _dataService.GetDataAsync();
Items = new ObservableCollection<Item>(data);
}
finally
{
IsLoading = false;
}
}
}
3. 避免在属性 setter 中执行耗时操作 #
csharp
public class BadViewModel : ViewModelBase
{
private string _searchText;
public string SearchText
{
get => _searchText;
set
{
SetProperty(ref _searchText, value);
Search(value);
}
}
private void Search(string text)
{
}
}
public class GoodViewModel : ViewModelBase
{
private string _searchText;
public string SearchText
{
get => _searchText;
set => SetProperty(ref _searchText, value);
}
public ICommand SearchCommand { get; }
public GoodViewModel()
{
SearchCommand = new Command<string>(Search);
}
private void Search(string text)
{
}
}
4. 使用消息中心进行跨组件通信 #
csharp
public class Messages
{
public const string UserLoggedIn = "UserLoggedIn";
public const string UserLoggedOut = "UserLoggedOut";
public const string DataUpdated = "DataUpdated";
}
public class LoginViewModel : ViewModelBase
{
public ICommand LoginCommand { get; }
public LoginViewModel()
{
LoginCommand = new AsyncCommand(LoginAsync);
}
private async Task LoginAsync()
{
var result = await _authService.LoginAsync(Username, Password);
if (result.Success)
{
MessagingCenter.Send(this, Messages.UserLoggedIn, result.User);
}
}
}
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
MessagingCenter.Subscribe<LoginViewModel, User>(this, Messages.UserLoggedIn,
(sender, user) =>
{
CurrentUser = user;
});
}
}
下一步 #
现在你已经掌握了 MVVM 模式,接下来学习 样式与资源,了解如何美化你的应用界面!
最后更新:2026-03-29