待办事项小程序(1)
这是一个wpf的mvvm模式的练手小程序。
需求:见动图演示
如果勾选,则代表事情完成,文字会加上下划线。
现在没有存盘等额外的功能。

解决方案:

TodoMainWindow.Xaml
<Window x:Class="WpfApp1.TodoMainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="待办事项列表" Height="450" Width="800">
<!-- 设置ViewModel为界面数据源 -->
<Window.DataContext>
<local:TodoViewModel />
</Window.DataContext>
<!-- 布局代码(Grid) -->
<Grid Margin="20">
<!-- 输入区:第0行 -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,10,0">
<TextBox x:Name="txtNewTodo" Width="300"
Text="{Binding NewTodoText, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="添加" Command="{Binding AddTodoCommand}"/>
</StackPanel>
<!-- 待办列表区:第1行 -->
<ListView Grid.Row="1" ItemsSource="{Binding TodoItems}" Margin="0,10,0,10">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<!-- CheckBox只负责勾选状态,文字用独立TextBlock控制样式 -->
<CheckBox IsChecked="{Binding IsCompleted}" Width="20" VerticalAlignment="Center"/>
<!-- 待办文字:通过DataTrigger控制删除线 -->
<TextBlock Text="{Binding Content}"
Width="250"
VerticalAlignment="Center"
Margin="5,0,5,0"> <!-- 左右留5px间距 -->
<TextBlock.Style>
<Style TargetType="TextBlock">
<!-- 默认样式:无文字装饰 -->
<Setter Property="TextDecorations" Value="None"/>
<!-- 数据触发器:IsCompleted=true时显示删除线 -->
<Style.Triggers>
<DataTrigger Binding="{Binding IsCompleted}" Value="True">
<Setter Property="TextDecorations" Value="Strikethrough"/>
<!-- 可选:完成后文字变灰色,视觉更明显 -->
<Setter Property="Foreground" Value="Gray"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<!-- 删除按钮 -->
<Button Content="×"
Command="{Binding DataContext.DeleteTodoCommand, RelativeSource={RelativeSource AncestorType=ListView}}"
CommandParameter="{Binding}"
VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- 操作区:第2行 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,10,0,10">
<TextBlock Text="{Binding UnfinishedCount, StringFormat='待办数量:{0}'}"/>
<Button Content="清空已完成" Command="{Binding ClearCompletedCommand}"/>
</StackPanel>
</Grid>
</Window>TodoViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace WpfApp1
{
public class TodoViewModel : ViewModelBase // 复用之前的ViewModelBase
{
// 绑定输入框的待办内容
private string _newTodoText;
public string NewTodoText
{
get => _newTodoText;
set { _newTodoText = value; OnPropertyChanged(); }
}
// 待办列表(核心:ObservableCollection)
public ObservableCollection<TodoItem> TodoItems { get; } = new();
// 未完成待办数量(计算属性,实时更新)
public int UnfinishedCount
{
get => TodoItems.Count(t => !t.IsCompleted);
}
// 命令:添加待办
public ICommand AddTodoCommand { get; }
// 命令:删除待办
public ICommand DeleteTodoCommand { get; }
// 命令:清空已完成
public ICommand ClearCompletedCommand { get; }
public TodoViewModel()
{
// 初始化命令
AddTodoCommand = new RelayCommand(AddTodo, CanAddTodo);
DeleteTodoCommand = new RelayCommand<TodoItem>(DeleteTodo);
ClearCompletedCommand = new RelayCommand(ClearCompleted);
// 监听TodoItems集合变更,更新UnfinishedCount
TodoItems.CollectionChanged += (s, e) => OnPropertyChanged(nameof(UnfinishedCount));
// 监听每个TodoItem的IsCompleted变更,更新UnfinishedCount
// (需额外处理:新增TodoItem时订阅其PropertyChanged事件)
}
// AddTodoCommand的执行逻辑
private void AddTodo(object? parameter)
{
var newTodo = new TodoItem { Content = NewTodoText.Trim(), IsCompleted = false };
TodoItems.Add(newTodo);
NewTodoText = string.Empty; // 清空输入框
}
// AddTodoCommand的CanExecute逻辑(输入为空时禁用)
private bool CanAddTodo(object? parameter)
{
return !string.IsNullOrWhiteSpace(NewTodoText);
}
// DeleteTodoCommand的执行逻辑
private void DeleteTodo(TodoItem? todo)
{
if (todo != null && TodoItems.Contains(todo))
{
TodoItems.Remove(todo);
}
}
// ClearCompletedCommand的执行逻辑
private void ClearCompleted(object? parameter)
{
var completedItems = TodoItems.Where(t => t.IsCompleted).ToList();
foreach (var item in completedItems)
{
TodoItems.Remove(item);
}
}
}
// 待办项实体,实现INotifyPropertyChanged(状态变更通知界面)
public class TodoItem : INotifyPropertyChanged
{
private string _content;
private bool _isCompleted;
public string Content
{
get => _content;
set { _content = value; OnPropertyChanged(); }
}
public bool IsCompleted
{
get => _isCompleted;
set { _isCompleted = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// 非泛型版本(兼容原有用法)
public class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Func<object?, bool>? _canExecute;
public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => _execute(parameter);
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
// 泛型版本(支持指定参数类型,避免手动强转)
public class RelayCommand<T> : ICommand
{
private readonly Action<T?> _execute;
private readonly Func<T?, bool>? _canExecute;
public RelayCommand(Action<T?> execute, Func<T?, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
// 适配ICommand的非泛型CanExecute(自动转换参数类型)
public bool CanExecute(object? parameter)
{
// 处理参数类型不匹配的情况(比如传了null或错误类型)
if (parameter is not T && parameter is not null)
return false;
return _canExecute?.Invoke((T?)parameter) ?? true;
}
// 适配ICommand的非泛型Execute(自动转换参数类型)
public void Execute(object? parameter)
{
if (parameter is T typedParameter)
_execute(typedParameter);
else if (parameter is null && default(T) is null)
_execute(default); // 处理null参数(比如T是引用类型)
}
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
public class ViewModelBase : INotifyPropertyChanged
{
// 实现接口的事件
public event PropertyChangedEventHandler? PropertyChanged;
// 封装触发事件的方法,供子类调用
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// 示例属性:修改时触发通知
private string _value;
public string Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
OnPropertyChanged(); // [CallerMemberName]会自动传入"Value"
}
}
}
}
}(一)下面是勇哥对wpf的mvvm的理解: -------------
Model:
纯数据载体(所有核心数据实体),无业务逻辑、无界面关联,是程序的 “数据地基”;
ViewModel: 核心职责:封装业务逻辑 + 数据适配 + 作为 View 和 Model 的中间人; 与 View 的关系:基于 “绑定契约” 解耦,双向交互(View→ViewModel:用户操作触发命令 / 属性变更;
ViewModel→View:属性变更通知界面更新); 与 Model 的关系:直接引用 Model,操作 / 加工 Model 的数据;
View:
纯展示层,仅通过绑定契约对接 ViewModel,无业务逻辑、无 Model 引用,只负责 “看” 和 “收用户操作”。
关键点回顾Model 的核心是 “数据载体”,和 “业务逻辑是否存在” 无关; ViewModel 的核心是 “逻辑 + 适配 + 中间层”,不只是 “联络人”,还是业务逻辑的唯一执行载体; View 和 ViewModel 的解耦是 “绑定契约解耦”,双向交互是核心特征。
-------------
(二)我们把上面代码代入MVVM的三元素:
Model:
TodoItem类
View:
TodoMainWindow.xaml,以及对应的cs文件
ViewModel:
TodoViewModel类