WPF MVVM:如何根据事件更新 UI 控制器

Posted

技术标签:

【中文标题】WPF MVVM:如何根据事件更新 UI 控制器【英文标题】:WPF MVVM : how to Update UI controllers based on Event 【发布时间】:2018-08-08 19:29:21 【问题描述】:

我的 UserControl 中有 2 个 TextBox 控制器,我们称它们为 TextBox1 和 TextBox2。

在我的旧代码中,当触发 TextBox2 TextChanged 事件时,我会更新 TextBox1 背景。在 xaml.cs 中使用事件处理程序,这很简单直接。

    private void textBox_TextChanged(object sender, TextChangedEventArgs e) 
     // use controllers Names.
    

但是我读到这违反了 MVVM 标准。基本上就是不要在xaml.cs中添加额外的代码!

在寻找答案的过程中,我发现了 2 种我有点理解的方法:

1- 有人建议我使用 PropertyChanged 来触发另一个事件。我注意到在 TextBox 失去焦点之前不会触发 PropertyChanged 事件。这不是我要找的。我希望 TextBox1 在用户向 TextBox2 输入内容后立即更新。但是,我仍然不确定在哪里告诉代码 “如果 TextBox TextChanged,则更改 TextBox1 背景”。

2- 另一种方法是使用对我来说全新的行为,我能够立即在 TextBox2 上触发 TextChanged 事件,但我不知道如何访问 TextBox1 属性!

我的问题:处理我在 MVVM 方法中寻找的要求的正确方法是什么?

【问题讨论】:

我会在你的视图模型上为每个Background 公开属性Textbox,并使用绑定连接它们。然后在 Textbox.Text 绑定到的任何属性的设置器中,您可以更新该属性。绑定会将​​更新后的值推送到控件。 这种方法是否需要文本框失去焦点才能调用 setter? 关于第 1 点,您的 xaml 中可能缺少 UpdateSourceTrigger=PropertyChanged。这将在属性更改后立即更新。 IE。如果你绑定到Text 属性,它会在每次有新输入时触发。 @user3382285 默认情况下会,但如果你在绑定中添加UpdateSourceTrigger="PropertyChanged",它将在文本的每次更改时触发。 后面的代码不违反 MVVM 标准。在 .xaml.cs 文件中做 UI 相关人员是完全可以的。在您的情况下,您正在更改仅与 UI 相关且不包含任何业务逻辑的文本框的颜色。我更愿意在后面的代码中使用事件处理程序更改背景。 【参考方案1】:

您可以在 View-Model 中执行所有这些逻辑。此特定示例使用AgentOctal.WpfLib NuGet 包(免责声明:我是此包的作者)作为引发PropertyChanged 通知的基本ViewModel 类,但您可以使用任何您想要的系统,只要它的属性实现INotifyPropertyChanged.


在此示例中,您在第一个 TextBox 中输入的字母越多,第二个 TextBox 的背景就越蓝。

第一个TextBoxText 属性绑定到视图模型上的Text 属性。绑定将UpdateSourceTrigger 设置为PropertyChanged,以便绑定在每次属性更改时更新视图模型,而不仅仅是在控件失去焦点时。

第二个TextBoxBackground 属性绑定到视图模型上名为BackgroundColorSolidColorBrush 属性。

在视图模型上,TextBox 的设置器包含确定第二个TextBox 的颜色的逻辑。

通过使用Color 而不是SolidColorBrushIValueConverter 可以将Color 更改为Brush,这可能会更好地实现,但它应该作为一个体面的服务器起点。

所有代码都存在于视图模型中,代码隐藏是空的。


XAML:

<Window
    x:Class="VmBindingExample.MainWindow"
    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:local="clr-namespace:VmBindingExample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="525"
    Height="350"
    mc:Ignorable="d">
    <Window.DataContext>
        <local:MainWindowVm />
    </Window.DataContext>
    <StackPanel Margin="20" Orientation="Vertical">
        <TextBox
            Margin="4"
            MaxLength="10"
            Text="Binding Path=Text, UpdateSourceTrigger=PropertyChanged" />
        <TextBox Margin="4" Background="Binding BackgroundColor">The color of this will reflect the length of the first textbox.</TextBox>
    </StackPanel>
</Window>

视图模型:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using AgentOctal.WpfLib;

namespace VmBindingExample

    using System.Windows.Media;

    public class MainWindowVm : ViewModel
    
        private string _text;

        public string Text
        
            get
            
                return _text;
            

            set
            
                SetValue(ref _text, value);
                byte red = (byte)(255 / 10 * (10 - _text.Length));
                BackgroundColor = new SolidColorBrush(Color.FromArgb(255, red, 255, 255));
            
        

        private Brush _backgroundColor;

        public Brush BackgroundColor
        
            get
            
                return _backgroundColor;
            

            set
            
                SetValue(ref _backgroundColor, value);
            
        
    

【讨论】:

【参考方案2】:

我会尽量在 xaml 中保留 UI 特定的内容,并在这种情况下使用触发器。 查看以下关于触发器和转换器的文章以获取空值。 DataTrigger where value is NOT null?

正如 Bradly Uffner 之前提到的,您应该修改绑定并添加 UpdateSourceTrigger="PropertyChanged",以便立即触发更改。

【讨论】:

对我来说,“UI 特定的东西”是一种代码味道。几乎总是有一个业务需求产生可以在 ViewModel 中建模的需求。【参考方案3】:

第二种方法是要走的路。在您的 viewmodel 中,添加 ICommand DoOnTextChanged 和依赖属性 BackgroundColor

使用行为将DoOnTextChanged 命令与TextBox1 的TextChanged 事件绑定 使用转换器将BackgroundColor属性绑定到TextBox2的背景。 在DoOnTextChangedExecute函数中,修改BackgroundColor的属性即可。

如果您使用的是 MVVMLight,绑定到 ICommand 很容易。首先添加这两个命名空间xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Platform" 并执行以下操作:

<TextBox>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="TextChanged" >
            <cmd:EventToCommand Command="Binding DoOnTextChanged" PassEventArgsToCommand="False" >
            </cmd:EventToCommand>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>

更新

由于 OP 使用的是普通 wpf/Xaml,我正在使用普通 wpf 的实现来更新我的答案。

在你的项目中添加以下两个帮助类:

public class ExecuteCommand : TriggerAction<DependencyObject>

    public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(ExecuteCommand));
    public ICommand Command
    
        get
        
            return GetValue(CommandProperty) as ICommand;
        
        set
        
            SetValue(CommandProperty, value);
        
    

    protected override void Invoke(object parameter)
    
        if (Command != null)
        
            if (Command.CanExecute(parameter))
            
                Command.Execute(parameter);
            
        
    


public class EventCommand : ICommand

    private Action<object> func;

    public EventCommand(Action<object> func)
    
        this.func = func;
    

    public bool CanExecute(object parameter)
    
        //Use your logic here when required
        return true;
    

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    
        if (func != null)
        
            func(parameter);
        
    

在您的 ViewModel 中,实现 INotifyPropertyChanged 并添加以下 ICommand 和 Background 属性。

public class MainViewModel : INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    public MainViewModel(IDataService dataService)
    
        BackColor = Brushes.Aqua;
        DoOnTextChanged = new EventCommand((obj => BackColor = BackColor == Brushes.BurlyWood ? Brushes.Chartreuse : Brushes.BurlyWood));
    

    public ICommand DoOnTextChanged  get; set; 

    private Brush backColor;
    public Brush BackColor
    
        get
        
            return backColor;
        
        set
        
            backColor = value;
            if (PropertyChanged != null)
            
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs("BackColor"));
            
        

    

最后,在您的 ViewName.xaml 文件中,添加此命名空间 xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"。您可能需要添加对 System.Windows.Interactivity 的引用。然后添加以下内容以将按钮事件绑定到命令:

<TextBox>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="TextChanged" >
            <local:ExecuteCommand Command="Binding DoOnTextChanged"></local:ExecuteCommand>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>

<TextBox Background="Binding BackColor"></TextBox>

虽然完成一些简单的事情需要很多代码,但在某些情况下它确实很有帮助。最好学习所有方法并使用最适合您需要的方法。

【讨论】:

第二个选项是“行为”。我没有看到您在回答中完全描述行为。不是说你的答案是错误的,但似乎没有讨论行为。 @BradleyUffner 请注意第一个要点。我只是给出基本方向。 我不知道我可以将命令绑定到 TextChanged!这是在 xaml 中吗?我可以做类似 的事情吗? @user3382285 你不能。我用一个简短的例子更新了我的答案。 虽然您的 EventToCommand 示例应该没问题,但它似乎比将逻辑放入视图模型的 getter 更多工作。它将具有相同的最终结果,并且与 MVVM 兼容。【参考方案4】:

好吧,我总是喜欢在可能的情况下关注MVVM,在这种情况下完全有可能:

您正在考虑View(当 TextBox2 TextChanged 时更新 TextBox1 背景)而不是从业务逻辑方面考虑(当业务层 [例如模型] 发生某些事情时更新 TextBox1 背景 [a属性改变了它的值?

您应该有一个带有 TextBox1 和一个 TextBox2 的 View 以及一个带有一些属性的 ViewModel,例如:

/* These properties should implement PropertyChanged, I'm too lazy */
public string WhateverInputsTextBox1  get; set; 
public string WhateverInputsTextBox2  get; set; 
public bool WhateverMeansTextBox1HasChanged  get; set; 

那么你应该在whatevernInputsTextBox1的内容发生变化时(在属性的set中)设置WhateverMeansTextBox1HasChanged为true。

最后,您必须使用转换器将 true 转换为一种颜色,将 false 转换为不同的颜色,将 TextBox1 文本绑定到WhateverInputsTextBox1 属性,将TextBox2 文本绑定到WhateverInputsTextBox2 属性,并将TextBox1 背景绑定到WhateverMeansTextBox1HasChanged 属性(检查@ 987654321@)。请记住在需要的地方将绑定设置为 UpdateSourceTrigger="PropertyChanged"(这会在输入数据时将数据移动到 ViewModel)。

这样您就可以将View 的所有业务逻辑都放入ViewModel 中,并且您可以根据需要对它进行测试,因为正确分配了所有职责。

另一个好处是(至少对我而言)当我看到TextBox 的背景在“AccountNumberChanged”而不是在编辑TextBox2 时发生变化时,更容易理解开发人员的意图。

【讨论】:

感谢您的回答。对我来说很奇怪,大多数 MVVM 解决方案最终都使用 PropertyChanged,然后在设置器中执行逻辑。虽然有一个事件可以直接使用TextChanged。 MVVM 是否有标准的事件处理方式!我希望有像 这样简单的东西,但似乎并非如此! @user3382285 PropertyChanged 是数据绑定的核心。这对于 MVVM 模式来说是绝对必要的。 您应该理解的主要思想是,当有人将某些内容写入 TextBox2 时,您不想更改 TextBox1 背景,而当有人更改您的业务的某些值时,您希望这样做。跨度>

以上是关于WPF MVVM:如何根据事件更新 UI 控制器的主要内容,如果未能解决你的问题,请参考以下文章

WPF 在 MVVM 模式下实现窗口后台代码与ViewModel交互

WPF 根据 UI 线程控制状态更新 NON UI 线程数据

什么时候使用事件和命令为WPF / MVVM

WPF MVVM

WPF Prism框架下基于MVVM模式的命令绑定事件

WPF 将 UI 事件绑定到 ViewModel 中的命令