少有人走的路

勇哥的工业自动化技术网站

待办事项小程序(1) 以这个例子讨论下mvvm的概念



待办事项小程序(1)

这是一个wpf的mvvm模式的练手小程序。


需求:见动图演示

如果勾选,则代表事情完成,文字会加上下划线。

现在没有存盘等额外的功能。


act101.gif


解决方案:

image.png



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类







发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

«    2026年1月    »
1234
567891011
12131415161718
19202122232425
262728293031
控制面板
您好,欢迎到访网站!
  查看权限
网站分类
搜索
最新留言
文章归档
网站收藏
友情链接

Powered By Z-BlogPHP 1.7.3

Copyright www.skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864