You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C# WPF MVVM(Caliburn Micro)实现Apply按钮启用校验的问题

基于Caliburn Micro的WPF MVVM应用Apply按钮校验逻辑实现问题

我正基于Caliburn Micro约定构建WPF MVVM应用,已完成以下功能:

  • 定义包含FirstName、LastName、Age三个属性的PersonModel类
  • 在ShellViewModel中使用BindableCollection存储3个PersonModel实例
  • 通过XAML的ItemsControl以每行三个文本框的形式展示所有Person数据
  • 新增、删除Person的功能已正常运行

当前需求是为"Apply"按钮实现启用校验逻辑(CanApply),要求仅当所有Person的所有文本框均填写完整时,按钮才启用。目前遇到的问题:修改文本框内容时,变更仅能通知到对应的PersonModel属性,但无法传递到定义了BindableCollection和CanApply()方法的ShellViewModel,导致按钮状态无法实时更新。


现有代码

PersonModel

public class PersonModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

ShellViewModel

public class ShellViewModel
{
    public BindableCollection<PersonModel> People { get; set; }

    public ShellViewModel()
    {
        DataAccess da = new DataAccess();
        People = new BindableCollection<PersonModel>() {
            new PersonModel { FirstName = "John", LastName= "Smith", Age = 30},
            new PersonModel { FirstName = "Bob", LastName= "Marry", Age = 21},
            new PersonModel { FirstName = "", LastName= "Milito", Age = 16}
            };
    }

    public void AddPerson()
    {
        DataAccess dataAccess = new DataAccess();

        if (People.Count > 0)
        {
            People.Add(new PersonModel());
        }

    }

    public void RemovePerson()
    {
        DataAccess dataAccess = new DataAccess();

        if (People.Count == 0)
        {
            return;
        }

        PersonModel randomPerson = dataAccess.GetRandomItem(People.ToArray());

        People.Remove(randomPerson);
    }

    
    public bool CanApply() {
        bool output = false;
        foreach (var item in People) {
            output = false;
            if (!string.IsNullOrEmpty(item.FirstName)) {
                output = true;
            }
        }
        return output;
    }
    
    public void Apply() {

    }
}

XAML

<Window x:Class="WPFDemoUI.Views.ShellView"
        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:WPFDemoUI.Views"
        mc:Ignorable="d" FontSize="20" WindowStartupLocation="CenterScreen"
        Title="ShellView" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal" Grid.Row="0">
            <Button x:Name="AddPerson">Add New Person</Button>
            <Button x:Name="RemovePerson" Margin="10 0 0 0">Remove Random Person</Button>
            <Button x:Name="Apply" Margin="10 0 0 0">Apply</Button>
        </StackPanel>
        
        <ItemsControl x:Name="People" Grid.Row="1">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="Black" BorderThickness="1" Margin="10" Padding="10">
                        <StackPanel Orientation="Horizontal">
                            <TextBox Text="{Binding FirstName, Mode=TwoWay}" MinWidth="50" />
                            <TextBox Text="{Binding LastName, Mode=TwoWay}" MinWidth="50" />
                            <TextBox Text="{Binding Age, Mode=TwoWay}" MinWidth="50"/>
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

解决方案

要实现需求,需要完成三个核心步骤:让PersonModel支持属性变更通知、在ShellViewModel中监听Person的属性变化、修正CanApply的校验逻辑。

1. 修改PersonModel,实现INotifyPropertyChanged接口

只有实现该接口,属性变更时才能触发通知,让绑定系统感知到变化。

using System.ComponentModel;
using System.Runtime.CompilerServices;

public class PersonModel : INotifyPropertyChanged
{
    private string _firstName;
    private string _lastName;
    private int _age;

    public string FirstName
    {
        get => _firstName;
        set
        {
            if (_firstName != value)
            {
                _firstName = value;
                OnPropertyChanged();
            }
        }
    }

    public string LastName
    {
        get => _lastName;
        set
        {
            if (_lastName != value)
            {
                _lastName = value;
                OnPropertyChanged();
            }
        }
    }

    public int Age
    {
        get => _age;
        set
        {
            if (_age != value)
            {
                _age = value;
                OnPropertyChanged();
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

2. 修改ShellViewModel,监听Person属性变化并通知校验状态

让ShellViewModel继承Caliburn Micro的Screen(内置INotifyPropertyChanged实现),然后订阅每个Person的PropertyChanged事件,当属性变化时通知CanApply状态更新。

using Caliburn.Micro;
using System.ComponentModel;

public class ShellViewModel : Screen
{
    public BindableCollection<PersonModel> People { get; set; }

    public ShellViewModel()
    {
        People = new BindableCollection<PersonModel>() {
            new PersonModel { FirstName = "John", LastName= "Smith", Age = 30},
            new PersonModel { FirstName = "Bob", LastName= "Marry", Age = 21},
            new PersonModel { FirstName = "", LastName= "Milito", Age = 16}
        };

        // 为已有Person订阅属性变更事件
        foreach (var person in People)
        {
            person.PropertyChanged += Person_PropertyChanged;
        }

        // 监听集合变更,新增/移除Person时自动订阅/取消订阅事件
        People.CollectionChanged += People_CollectionChanged;
    }

    private void People_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
        {
            foreach (PersonModel newPerson in e.NewItems)
            {
                newPerson.PropertyChanged += Person_PropertyChanged;
            }
        }

        if (e.OldItems != null)
        {
            foreach (PersonModel oldPerson in e.OldItems)
            {
                oldPerson.PropertyChanged -= Person_PropertyChanged;
            }
        }
    }

    private void Person_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        // 通知Caliburn重新计算CanApply状态,自动更新按钮启用状态
        NotifyOfPropertyChange(() => CanApply);
    }

    public void AddPerson()
    {
        People.Add(new PersonModel());
    }

    public void RemovePerson()
    {
        if (People.Count == 0) return;

        DataAccess dataAccess = new DataAccess();
        PersonModel randomPerson = dataAccess.GetRandomItem(People.ToArray());
        People.Remove(randomPerson);
    }

    public bool CanApply()
    {
        // 校验所有Person的必填字段:FirstName/LastName不为空,Age大于0
        foreach (var person in People)
        {
            if (string.IsNullOrEmpty(person.FirstName) || 
                string.IsNullOrEmpty(person.LastName) || 
                person.Age <= 0)
            {
                return false;
            }
        }
        // 没有Person时也禁用按钮
        return People.Count > 0;
    }

    public void Apply()
    {
        // 这里添加Apply的业务逻辑
    }
}

3. 优化XAML的Age绑定(可选但推荐)

由于Age是int类型,空输入会导致绑定报错,添加TargetNullValue和UpdateSourceTrigger优化用户体验:

<TextBox Text="{Binding Age, Mode=TwoWay, TargetNullValue='', UpdateSourceTrigger=PropertyChanged}" MinWidth="50"/>
  • UpdateSourceTrigger=PropertyChanged:输入时立即更新绑定,实时触发校验
  • TargetNullValue='':处理空输入时的绑定兼容问题

原理说明

  • PersonModel实现INotifyPropertyChanged后,属性变更时会触发PropertyChanged事件
  • ShellViewModel订阅每个Person的该事件,当任何属性变化时,通过NotifyOfPropertyChange(() => CanApply)通知Caliburn Micro重新计算CanApply()方法的返回值
  • Caliburn Micro会自动根据CanApply()的结果更新Apply按钮的IsEnabled状态

内容的提问来源于stack exchange,提问作者Needy.Coder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 21:45:29