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?
只需将 TextBox
的 Text
属性 绑定到视图模型的 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>
描述
在 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?
只需将 TextBox
的 Text
属性 绑定到视图模型的 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>