XAML 中 TextBox.TextChanged 的 WPF 事件处理程序或代码隐藏?

WPF EventHandler for TextBox.TextChanged in XAML or code behind?

描述

在 WPF 中,使用 MvvmLight,我有一个带有整数 属性 SelectedIndex 的 viewModel。更改此 属性 的值是一项昂贵的操作,因此我只想在操作员相当确定他已完成输入时更新 属性。

我有一个文本框和一个按钮。操作员键入一个数字,然后按下按钮。这应该导致更新 属性.

的命令

标准 WPF MvvmLight 解决方案

class MyViewModel
{
    private int selectedIndex;

    public MyViewModel()
    {
        this.CommandSelectIndex = new RelayCommand(ExecuteSelectIndex, CanSelectIndex);
    }

    public public RelayCommand<int> CommandSelectIndex { get; }

    public int SelectedIndex
    {
        get => this.selectedIndex;
        set => base.Set(nameof(SelectedIndex), ref this.selectedIndex, value);
    }

    private bool CanSelectIndex(int proposedIndex)
    {
         return proposedIndex > 0 && proposedIndex < MyData.Count;
    }

    private void ExecuteSelectIndex(int proposedIndex)
    {
        this.SelectedIndex = proposedIndex;
        ProcessSelectedIndex(proposedIndex);  // Expensive!
    }
}

对于了解 MvvmLight 的人来说,这相当简单。

所以当接线员输入数字时,我只想更新按钮。我不想对中间值做任何事情:

1 --> 12 --> 123 --> (typing error, backspace) --> 124 [press button]

XAML

<StackPanel Name="Test1" Orientation="Horizontal">
    <TextBox Name="ProposedValue1" Text="1234" Width="300" Height="20"/>
    <Button x:Name="ButtonChangeText1" Content="Change"
                    Height="30" Width="74" Padding="5,2"
                    Command="{Binding Path=CommandSelectedIndex}"
                    CommandParameter="{Binding ElementName=ProposedValue1, Path=Text}"/>
</StackPanel>

这部分起作用:在启动时调用 CanSelectIndex(1234);如果按下按钮 ExecuteSelectedIndex(1234) 被调用。

问题

但是,如果TextBox的文本发生变化,则不会调用CanSelectIndex

原因是当文本框改变时事件ICommand.CanExecuteChanged没有被引发。

解法:

添加事件处理程序:

XAML:

<TextBox Name="ProposedValue1" Text="1234" Width="300" Height="20"
         TextChanged="textChangedEventHandler"/>

后面的代码:

private void textChangedEventHandler(object sender, TextChangedEventArgs args)
{
    ((MyViewModel)this.DataContext).CommandSelectedIndex.RaiseCanExecuteChanged();
}

每当要在后面写代码的时候,总觉得有点不自在。在代码隐藏中编写事件处理程序是标准的,还是我只在教程中看到的简化。

有什么方法可以在 XAML 中做到这一点?绑定的东西?

TextChanged="TextChanged="{Binding Path=CommandSelectIndex ??? RaiseCanExecuteChanged() }

Is there a method that I can do this in XAML? Something with Binding?

只需将 TextBoxText 属性 绑定到视图模型的 string 源 属性 并调用 RaiseCanExecuteChanged命令的方法来自这个setter

如果您出于某种原因真的想处理实际事件,您应该查看 interaction triggers

RelayCommandclass在MvvmLight中有两个实现。 在 GalaSoft.MvvmLight.Command 命名空间和 GalaSoft.MvvmLight.CommandWpf 命名空间中。

您可能使用过命名空间 GalaSoft.MvvmLight.Command。 而且这种类型实际上并没有更新命令的状态。

如果从GalaSoft.MvvmLight.CommandWpf命名空间使用,则命令的状态根据预定逻辑更新。

@Harald Coppulse,你完全正确!

这是我的 MvvmLight 测试代码。

ViewModel:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.CommandWpf;

namespace InvalidateCommandMvvmLight.ViewModel
{
    public class MyViewModel : ViewModelBase
    {
        private string _text;
        private int _number;

        public string Text { get => _text; private set => Set(ref _text, value); }

        public int Number { get => _number; set => Set(ref _number, value); }

        public RelayCommand<string> CommandTest { get; }
        public RelayCommand<int> CommandNumber { get; }

        public MyViewModel()
        {
            CommandTest = new RelayCommand<string>(Test, CanTest);
            CommandNumber = new RelayCommand<int>(IntTest, CanIntTest);
        }

        private bool CanTest(string text)
        {
            // the text must have a minimum length of 4 
            // and be different from the current one
            return text != null && text.Length >= 4 && text != Text;
        }
        private void Test(string text)
        {
            Text = text;

        }

        private bool CanIntTest(int num)
        {
            // The "num" parameter must be positive, less than 100
            // and is not equal to the Number property
            return num > 0 && num <100 && num != Number;
        }
        private void IntTest(int num)
        {
            Number = num;
        }
    }
}

XAML:

<Window x:Class="InvalidateCommandMvvmLight.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:InvalidateCommandMvvmLight"
        xmlns:vm="clr-namespace:InvalidateCommandMvvmLight.ViewModel"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MyViewModel/>
    </Window.DataContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TextBox x:Name="tbText"
                Text="Alle eendjes zwemmen in het water" VerticalAlignment="Center"
                />

        <Button Content="Change Text"
                Grid.Column="1"
                Margin="5"
                Padding="5,2"
                Command="{Binding Path=CommandTest}"
                CommandParameter="{Binding ElementName=tbText, Path=Text}"/>
        <TextBox Text="{Binding Text, Mode=OneWay}" Grid.Column="2" IsReadOnly="True" VerticalAlignment="Center"/>
        <TextBox x:Name="tbNumber"
                Grid.Row="1"
                Text="55" VerticalAlignment="Center"/>

        <Button Content="Change Number"
                Grid.Row="1" Grid.Column="1"
                Margin="5"
                Padding="5,2"
                Command="{Binding Path=CommandNumber}"
                CommandParameter="{Binding ElementName=tbNumber, Path=Text}"/>
        <TextBox Text="{Binding Number, Mode=OneWay}" IsReadOnly="True"
                 Grid.Row="1" Grid.Column="2" VerticalAlignment="Center"/>
    </Grid>
</Window>

不幸的是,MvvmLight 中的 CommandsWpf.RelayCommand class 没有正确实现。
它没有考虑到在 WPF 中处理不同类型的值的特殊性。

要以典型的 WPF 方式工作,实现应如下所示:

using System.ComponentModel;

namespace Common
{
    #region Delegates for WPF Command Methods
    /// <summary>Delegate of the executive team method.</summary>
    /// <param name="parameter">Command parameter.</param>
    public delegate void ExecuteHandler<T>(T parameter);
    /// <summary>Command сan execute method delegate.</summary>
    /// <param name="parameter">Command parameter.</param>
    /// <returns><see langword="true"/> if command execution is allowed.</returns>
    public delegate bool CanExecuteHandler<T>(T parameter);
    #endregion

    /// <summary>Class for typed parameter commands.</summary>
    public class RelayCommand<T> : RelayCommand
    {

        /// <summary>Command constructor.</summary>
        /// <param name="execute">Executable command method.</param>
        /// <param name="canExecute">Method allowing command execution.</param>
        public RelayCommand(ExecuteHandler<T> execute, CanExecuteHandler<T> canExecute = null)
            : base
        (
                  p => execute(TypeDescriptor.GetConverter(typeof(T)).IsValid(p) ? (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(p) : default),
                  p => (canExecute == null) || (TypeDescriptor.GetConverter(typeof(T)).IsValid(p) && canExecute((T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(p)))
        )
        {}

    }
}

除非您有能力更改 RelayCommand 实现,否则您需要以某种方式使用 Binding 的能力来 auto-convert 值。

一个变体。
在 ViewModel 中创建所需类型的 属性,并将其用作自动转换的代理。
但是如果输入了 non-numeric 值,那么命令将无法定义它。
您还需要检查 Validation.HasError.

ViewModel:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.CommandWpf;

namespace InvalidateCommandMvvmLight.ViewModel
{
    public class MyViewModel : ViewModelBase
    {
        private string _text;
        private int _number;
        private int _numberView;

        public string Text { get => _text; private set => Set(ref _text, value); }

        public int Number { get => _number; set => Set(ref _number, value); }
        public int NumberView { get => _numberView; set => Set(ref _numberView, value); }

        public RelayCommand<string> CommandTest { get; }
        public RelayCommand<int> CommandNumber { get; }

        public MyViewModel()
        {
            CommandTest = new RelayCommand<string>(Test, CanTest);
            CommandNumber = new RelayCommand<int>(IntTest, CanIntTest);
        }

        private bool CanTest(string text)
        {
            // the text must have a minimum length of 4 
            // and be different from the current one
            return text != null && text.Length >= 4 && text != Text;
        }
        private void Test(string text)
        {
            Text = text;

        }

        private bool CanIntTest(int num)
        {
            // The "num" parameter must be positive, less than 100
            // and is not equal to the Number property
            return num > 0 && num <100 && num != Number;
        }
        private void IntTest(int num)
        {
            Number = num;
        }
    }
}

XAML:

<Window x:Class="InvalidateCommandMvvmLight.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:InvalidateCommandMvvmLight"
        xmlns:vm="clr-namespace:InvalidateCommandMvvmLight.ViewModel"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MyViewModel NumberView="55"/>
    </Window.DataContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TextBox x:Name="tbText"
                Text="Alle eendjes zwemmen in het water" VerticalAlignment="Center"
                />

        <Button Content="Change Text"
                Grid.Column="1"
                Margin="5"
                Padding="5,2"
                Command="{Binding Path=CommandTest}"
                CommandParameter="{Binding ElementName=tbText, Path=Text}"/>
        <TextBox Text="{Binding Text, Mode=OneWay}" Grid.Column="2" IsReadOnly="True" VerticalAlignment="Center"/>
        <TextBox x:Name="tbNumber"
                Grid.Row="1"
                Text="{Binding NumberView, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center"/>

    <Button Content="Change Number"
                Grid.Row="1" Grid.Column="1"
                Margin="5"
                Padding="5,2"
                Command="{Binding Path=CommandNumber}"
                CommandParameter="{Binding NumberView}">
        <Button.Style>
            <Style TargetType="Button">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=(Validation.HasError), ElementName=tbNumber}"
                                 Value="True">
                        <Setter Property="IsEnabled" Value="False"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Button.Style>
    </Button>
        <TextBox Text="{Binding Number, Mode=OneWay}" IsReadOnly="True"
                 Grid.Row="1" Grid.Column="2" VerticalAlignment="Center"/>
    </Grid>
</Window>

第二种变体。
创建显式代理转换器。

转换器:

using System;
using System.ComponentModel;
using System.Windows;

namespace InvalidateCommandMvvmLight
{
    public class ProxyBinding : Freezable
    {
        public Type Type
        {
            get { return (Type)GetValue(TypeProperty); }
            set { SetValue(TypeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Type.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TypeProperty =
            DependencyProperty.Register(nameof(Type), typeof(Type), typeof(ProxyBinding), new PropertyMetadata(typeof(object), ChangedValueOrType));

        private static void ChangedValueOrType(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ProxyBinding proxy = (ProxyBinding)d;
            if (proxy.Type == null)
            {
                proxy.Value = null;
                return;
            }
            if (proxy.Source == null)
                return;

            if (proxy.Type == proxy.Source.GetType())
                return;

            if (TypeDescriptor.GetConverter(proxy.Type).IsValid(proxy.Source))
                proxy.Value = TypeDescriptor.GetConverter(proxy.Type).ConvertFrom(proxy.Source);
            else
                proxy.Value = null;
        }

        public object Source
        {
            get { return GetValue(SourceProperty); }
            set { SetValue(SourceProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Value.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register(nameof(Source), typeof(object), typeof(ProxyBinding), new PropertyMetadata(null, ChangedValueOrType));

        public object Value
        {
            get { return GetValue(ValueProperty); }
            protected  set { SetValue(ValuePropertyKey, value); }
        }

        // Using a DependencyProperty as the backing store for readonly Value.  This enables animation, styling, binding, etc...
        protected static readonly DependencyPropertyKey ValuePropertyKey =
            DependencyProperty.RegisterReadOnly(nameof(Value), typeof(object), typeof(ProxyBinding), new PropertyMetadata(null));
        public static readonly DependencyProperty ValueProperty = ValuePropertyKey.DependencyProperty;

        protected override Freezable CreateInstanceCore()
        {
            return new ProxyBinding();
        }
    }
}

XAML:

<Window x:Class="InvalidateCommandMvvmLight.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:InvalidateCommandMvvmLight"
            xmlns:vm="clr-namespace:InvalidateCommandMvvmLight.ViewModel"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MyViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <local:ProxyBinding x:Key="ProxyInt"
                Type="{x:Type sys:Int32}"
                Source="{Binding ElementName=tbNumber, Path=Text, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TextBox x:Name="tbText"
                    Text="Alle eendjes zwemmen in het water" VerticalAlignment="Center"
                    />

        <Button Content="Change Text"
                    Grid.Column="1"
                    Margin="5"
                    Padding="5,2"
                    Command="{Binding Path=CommandTest}"
                    CommandParameter="{Binding ElementName=tbText, Path=Text}"/>
        <TextBox Text="{Binding Text, Mode=OneWay}" Grid.Column="2" IsReadOnly="True" VerticalAlignment="Center"/>
        <TextBox x:Name="tbNumber"
                    Grid.Row="1"
                    Text="55" VerticalAlignment="Center"/>

        <Button Content="Change Number"
                    Grid.Row="1" Grid.Column="1"
                    Margin="5"
                    Padding="5,2"
                    Command="{Binding Path=CommandNumber}"
                    CommandParameter="{Binding Value, Source={StaticResource ProxyInt}}">
        </Button>
        <TextBox Text="{Binding Number, Mode=OneWay}" IsReadOnly="True"
                     Grid.Row="1" Grid.Column="2" VerticalAlignment="Center"/>
        <TextBlock Grid.Row="2" Text="{Binding Value,Source={StaticResource proxy}}"/>
    </Grid>
</Window>

另一种变体。
为绑定创建转换器:

using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows.Data;

namespace InvalidateCommandMvvmLight
{
    public class ValueTypeConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (parameter is Type type && TypeDescriptor.GetConverter(type).IsValid(value))
                return TypeDescriptor.GetConverter(type).ConvertFrom(value);
            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

XAML:

<Window x:Class="InvalidateCommandMvvmLight.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:InvalidateCommandMvvmLight"
            xmlns:vm="clr-namespace:InvalidateCommandMvvmLight.ViewModel"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MyViewModel/>
    </Window.DataContext>
    <Window.Resources>
    <local:ValueTypeConverter x:Key="ValueTypeConverter"/>
</Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TextBox x:Name="tbText"
                    Text="Alle eendjes zwemmen in het water" VerticalAlignment="Center"
                    />

        <Button Content="Change Text"
                    Grid.Column="1"
                    Margin="5"
                    Padding="5,2"
                    Command="{Binding Path=CommandTest}"
                    CommandParameter="{Binding ElementName=tbText, Path=Text}"/>
        <TextBox Text="{Binding Text, Mode=OneWay}" Grid.Column="2" IsReadOnly="True" VerticalAlignment="Center"/>
        <TextBox x:Name="tbNumber"
                    Grid.Row="1"
                    Text="55" VerticalAlignment="Center"/>

        <Button Content="Change Number"
                    Grid.Row="1" Grid.Column="1"
                    Margin="5"
                    Padding="5,2"
                    Command="{Binding Path=CommandNumber}"
                    CommandParameter="{Binding Text, Converter={StaticResource ValueTypeConverter}, ConverterParameter={x:Type sys:Int32}, ElementName=tbNumber}">
        </Button>
        <TextBox Text="{Binding Number, Mode=OneWay}" IsReadOnly="True"
                     Grid.Row="1" Grid.Column="2" VerticalAlignment="Center"/>
    </Grid>
</Window>