WPF EventHandler для TextBox.TextChanged в XAML или коде? - PullRequest
0 голосов
/ 04 августа 2020

Описание

В WPF с использованием MvvmLight у меня есть viewModel с целочисленным свойством SelectedIndex. Изменение значения этого свойства - дорогостоящая операция, поэтому я хочу обновить свойство только в том случае, если оператор достаточно уверен, что он закончил вводить текст.

У меня есть TextBox и кнопка. Оператор набирает номер и нажимает кнопку. Это должно привести к команде, которая обновляет свойство.

Стандартное решение 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() }

Ответы [ 3 ]

1 голос
/ 05 августа 2020

Класс RelayCommand в MvvmLight имеет две реализации. В пространстве имен GalaSoft.MvvmLight.Command и в пространстве имен GalaSoft.MvvmLight.CommandWpf .

Вы, вероятно, использовали из пространства имен GalaSoft.MvvmLight. Команда . И этот тип на самом деле не обновляет состояние команды.

Если используется из пространства имен GalaSoft.MvvmLight.CommandWpf , то состояние команды обновляется в соответствии с заранее определенным логом. c.

0 голосов
/ 11 августа 2020

@ 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>

К сожалению, класс CommandsWpf.RelayCommand в MvvmLight реализован некорректно. Он не учитывает особенности работы со значениями разных типов в 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 для автоматического преобразования значений.

Один вариант. Создайте свойство желаемого типа в ViewModel и используйте его в качестве прокси для автопреобразования. Но если введено нечисловое значение c, команда не сможет его определить. Вам также необходимо проверить 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>
0 голосов
/ 04 августа 2020

Есть ли способ сделать это в XAML? Что-то с привязкой?

Просто привяжите свойство Text объекта TextBox к исходному свойству string модели представления и вызовите метод RaiseCanExecuteChanged команды из установщика этого.

Если вы действительно хотите обработать реальное событие по какой-то причине, вам следует изучить триггеры взаимодействия .

...