对于文本框中的 DataObject.Pasting 事件,我想分配位于视图模型(MVVM 模式)中的 TextBoxPasting 函数。不幸的是,该代码不起作用。我使用图书馆:
xmlns:behaviours =“http://schemas.microsoft.com/xaml/behaviors”。
查看 - 代码:
<TextBox Text="{Binding StartNumber, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" FontSize="16" Height="27" Margin="10"/>
<behaviours:Interaction.Triggers >
<behaviours:EventTrigger EventName="DataObject.Pasting">
<behaviours:InvokeCommandAction x:Name="DataObjectPastingCommand" Command="{Binding DataObjectPastingCommand}" PassEventArgsToCommand="True"/>
</behaviours:EventTrigger>
</behaviours:Interaction.Triggers>
ViewModel - 代码:
public class MechanicViewModel : ViewModelBase, IMechanicViewModel
{
private static readonly Regex _regex = new Regex("[^0-9.-]+");
public MechanicViewModel()
{
DataObjectPastingCommand = new DelegateCommand<DataObjectPastingEventArgs>(TextBoxPasting);
}
public DelegateCommand<DataObjectPastingEventArgs> DataObjectPastingCommand { get; private set; }
private static bool IsTextAllowed(string text)
{
return !_regex.IsMatch(text);
}
private void TextBoxPasting(DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(typeof(string)))
{
string text = (string)e.DataObject.GetData(typeof(string));
if (!IsTextAllowed(text))
{
e.CancelCommand();
}
}
else
{
e.CancelCommand();
}
}
}
您的问题明确要求采用符合 MVVM 的方式来处理应用程序视图模型中附加的 UI 事件(或一般 UI 事件)。空无一人。根据定义,视图模型不得参与 UI 逻辑。视图模型没有理由处理 UI 事件。所有事件处理都必须在视图中进行。
下面的 UML 图显示视图模型必须是视图不可知的。这是 MVVM 设计模式的关键要求。
视图不可知论当然意味着视图模型不允许主动参与 UI 逻辑。
虽然在技术上可以在应用程序视图模型中处理 UI 事件,但 MVVM 禁止这样做,并要求此类事件在应用程序视图中处理。如果您想正确实现 MVVM,因为您不想让 UI 渗透到应用程序中,那么您必须在视图中处理此类事件(在 C# 中,又称为代码隐藏,又称为部分类)。
复制和粘贴是纯粹的查看操作:
RoutedEventArgs
对象。RoutedEventArgs
公开源UI元素(通过委托的发送者参数,通过事件参数的Source
和OriginalSource
属性)。RoutedEventArgs
使处理程序能够直接参与 UI 逻辑(例如,通过将事件标记为已处理或取消正在进行的 UI 操作等)。这些都是暗示事件不应该在视图模型中处理。路由事件是与 UI 交互或 UI 行为或渲染逻辑相关的always事件 - 它们由 UI 对象声明和引发。
视图模型不得关心任何 UI 元素,例如他们何时以及如何改变尺寸。
Button.Click
事件是唯一应该触发视图模型操作的事件。此类元素通常实现 ICommandSource
来消除事件订阅。但是,在代码隐藏中处理 Click
事件并将操作从那里委托给视图模型并没有什么问题。
将所有 UI 对象传递到视图模型(通过事件参数)也是明显的 MVVM 违规。
参与 UI 逻辑(复制和粘贴)是另一个 MVVM 违规行为。您必须在控件的代码隐藏中处理
Pasting
事件,或者通过让视图模型类实现 INotifyDataErrorInfo
来实现属性验证。
从用户体验的角度来看,吞没用户操作是“永远”的好主意。
您必须始终提供反馈,以便用户知道他的操作不受支持。如果您无法从一开始就阻止它,例如通过禁用通常由灰色元素可视化的交互,那么您必须提供错误信息来解释为什么不允许该操作以及如何修复它。数据验证是最好的解决方案。
例如,当您在网络应用程序中填写注册表并在输入字段中输入/粘贴无效数据时,您的操作不会被默默吞没。
INotifyDataErrorInfo
的作用完全相同。
并且它不违反 MVVM。 完成任务的推荐 MVVM 方法是实施
INotifyDataErrorInfo
。请参阅
如何添加验证以查看模型属性或如何实现 INotifyDataErrorInfo。 一个不太优雅但仍然有效的 MVVM 解决方案是在视图中实现
DataObject.Pasting
附加事件的事件处理并从代码隐藏中取消命令。请注意,此解决方案可能违反多项 UI 设计规则。您至少必须向用户提供适当的错误反馈。
对于您的特定情况,您应该考虑实施NumericTextBox
。这也是一个优雅的解决方案,其中输入验证完全由输入字段处理。该控件可以通过实施
Binding
验证来提供数据验证,例如定义 ValidationRule
。您将获得一种便捷的方式来向用户显示红色框(可自定义)和错误消息。还处理粘贴内容的示例 NumericTextBox
:
NumericValidationRule.cs
ValidationRule
用于验证输入的
NumericTextBox
。该规则还会传递到绑定引擎,以便启用框架中内置的可视化验证错误反馈。public class NumericValidationRule : ValidationRule
{
private readonly string nonNumericErrorMessage = "Only numeric input allowed.";
private readonly string malformedInputErrorMessage = "Input is malformed.";
private readonly string decimalSeperatorInputErrorMessage = "Only a single decimal seperator allowed.";
private TextBox Source { get; }
public NumericValidationRule(TextBox source) => this.Source = source;
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
ArgumentNullException.ThrowIfNull(cultureInfo, nameof(cultureInfo));
if (value is not string textValue
|| string.IsNullOrWhiteSpace(textValue))
{
return new ValidationResult(false, this.nonNumericErrorMessage);
}
if (IsInputNumeric(textValue, cultureInfo))
{
return ValidationResult.ValidResult;
}
// Input was can still be a valid special character
// like '-', '+' or the decimal seperator of the current culture
ValidationResult validationResult = HandleSpecialNonNumericCharacter(textValue, cultureInfo);
return validationResult;
}
private bool IsInputNumeric(string input, IFormatProvider culture) =>
double.TryParse(input, NumberStyles.Number, culture, out _);
private ValidationResult HandleSpecialNonNumericCharacter(string input, CultureInfo culture)
{
ValidationResult validationResult;
switch (input)
{
// Negative sign is not the first character
case var _ when input.LastIndexOf(culture.NumberFormat.NegativeSign, StringComparison.OrdinalIgnoreCase) != 0:
validationResult = new ValidationResult(false, this.malformedInputErrorMessage);
break;
// Positivre sign is not the first character
case var _ when input.LastIndexOf(culture.NumberFormat.PositiveSign, StringComparison.OrdinalIgnoreCase) != 0:
validationResult = new ValidationResult(false, this.malformedInputErrorMessage);
break;
// Allow single decimal separator
case var _ when input.Equals(culture.NumberFormat.NumberDecimalSeparator, StringComparison.OrdinalIgnoreCase):
{
bool isSingleSeperator = !this.Source.Text.Contains(culture.NumberFormat.NumberDecimalSeparator, StringComparison.CurrentCultureIgnoreCase);
validationResult = isSingleSeperator ? ValidationResult.ValidResult : new ValidationResult(false, this.decimalSeperatorInputErrorMessage);
break;
}
default:
validationResult = new ValidationResult(false, this.nonNumericErrorMessage);
break;
}
return validationResult;
}
}
NumericTextBox.cs
class NumericTextBox : TextBox
{
private ValidationRule NumericInputValidationRule { get; set; }
static NumericTextBox()
=> DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericTextBox), new FrameworkPropertyMetadata(typeof(NumericTextBox)));
public NumericTextBox()
{
this.NumericInputValidationRule = new NumericValidationRule(this);
DataObject.AddPastingHandler(this, OnContentPasting);
}
private void OnContentPasting(object sender, DataObjectPastingEventArgs e)
{
if (!e.DataObject.GetDataPresent(DataFormats.Text))
{
e.CancelCommand();
ShowErrorFeedback("Only numeric content supported.");
return;
}
string pastedtext = (string)e.DataObject.GetData(DataFormats.Text);
CultureInfo culture = CultureInfo.CurrentCulture;
ValidationResult validationResult = ValidateText(pastedtext, culture);
if (!validationResult.IsValid)
{
e.CancelCommand();
}
}
#region Overrides of TextBoxBase
/// <inheritdoc />
protected override void OnTextInput(TextCompositionEventArgs e)
{
CultureInfo culture = CultureInfo.CurrentCulture;
string currentTextInput = e.Text;
// Remove any negative sign if '+' was pressed
// or prepend a negative sign if '-' was pressed
if (TryHandleNumericSign(currentTextInput, culture))
{
e.Handled = true;
return;
}
ValidationResult validationResult = ValidateText(currentTextInput, culture);
e.Handled = !validationResult.IsValid;
if (validationResult.IsValid)
{
base.OnTextInput(e);
}
}
#endregion Overrides of TextBoxBase
private ValidationResult ValidateText(string currentTextInput, CultureInfo culture)
{
ValidationResult validationResult = this.NumericInputValidationRule.Validate(currentTextInput, culture);
if (validationResult.IsValid)
{
HideErrorFeedback();
}
else
{
ShowErrorFeedback(validationResult.ErrorContent);
}
return validationResult;
}
private bool TryHandleNumericSign(string input, CultureInfo culture)
{
int oldCaretPosition = this.CaretIndex;
// Remove any negative sign if '+' pressed
if (input.Equals(culture.NumberFormat.PositiveSign, StringComparison.OrdinalIgnoreCase))
{
if (this.Text.StartsWith(culture.NumberFormat.NegativeSign, StringComparison.OrdinalIgnoreCase))
{
this.Text = this.Text.Remove(0, 1);
// Move the caret to the original input position
this.CaretIndex = oldCaretPosition - 1;
}
return true;
}
// Prepend the negative sign if '-' pressed
else if (input.Equals(culture.NumberFormat.NegativeSign, StringComparison.OrdinalIgnoreCase))
{
if (!this.Text.StartsWith(culture.NumberFormat.NegativeSign, StringComparison.OrdinalIgnoreCase))
{
this.Text = this.Text.Insert(0, culture.NumberFormat.NegativeSign);
// Move the caret to the original input position
this.CaretIndex = oldCaretPosition + 1;
}
return true;
}
return false;
}
private void HideErrorFeedback()
{
BindingExpression textPropertyBindingExpression = GetBindingExpression(TextProperty);
bool hasTextPropertyBinding = textPropertyBindingExpression is not null;
if (hasTextPropertyBinding)
{
Validation.ClearInvalid(textPropertyBindingExpression);
}
}
private void ShowErrorFeedback(object errorContent)
{
BindingExpression textPropertyBindingExpression = GetBindingExpression(TextProperty);
bool hasTextPropertyBinding = textPropertyBindingExpression is not null;
if (hasTextPropertyBinding)
{
// Show the error feedbck by triggering the binding engine
// to show the Validation.ErrorTemplate
Validation.MarkInvalid(
textPropertyBindingExpression,
new ValidationError(
this.NumericInputValidationRule,
textPropertyBindingExpression,
errorContent, // The error message
null));
}
}
}
<Style TargetType="local:NumericTextBox">
<Style.Resources>
<!-- The visual error feedback -->
<ControlTemplate x:Key="ValidationErrorTemplate1">
<StackPanel>
<Border BorderBrush="Red"
BorderThickness="1"
HorizontalAlignment="Left">
<!-- Placeholder for the NumericTextBox itself -->
<AdornedElementPlaceholder x:Name="AdornedElement" />
</Border>
<Border Background="White"
BorderBrush="Red"
Padding="4"
BorderThickness="1"
HorizontalAlignment="Left">
<ItemsControl ItemsSource="{Binding}"
HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}"
Foreground="Red" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</StackPanel>
</ControlTemplate>
</Style.Resources>
<Setter Property="Validation.ErrorTemplate"
Value="{StaticResource ValidationErrorTemplate1}" />
<Setter Property="BorderBrush"
Value="{x:Static SystemColors.ActiveBorderBrush}" />
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:NumericTextBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ScrollViewer Margin="0"
x:Name="PART_ContentHost" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled"
Value="False">
<Setter Property="Background"
Value="{x:Static SystemColors.ControlLightBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>