为什么 TextBox 的值会重置为以前的值而不是显示错误?

Why does the Value of the TextBox get reset to the previous value instead of showing an error?

我有一个示例,其中我将视图模型的属性与一些 TextBox 控件(包括验证规则)绑定在一起。在大多数情况下,这工作正常。但是当我尝试包含绑定 TextBoxIsFocused 属性 时,如果在控件中输入无效数字,我会遇到麻烦。

当我在直接绑定到视图模型的 属性 的 TextBox 控件中输入错误的数字时,错误按预期显示(TextBox 周围的红色边框) .但是在与包含视图模型 属性 和 TextBoxIsFocused 属性 的 MultiBinding 绑定的 TextBox 中,未显示错误,并且该值被重置为先前的有效值。

例如,如果小于10的数字无效,我输入3,当TextBox失去焦点时,TextBox中通常会出现红色边框,表示错误。但是在包含 IsFocused 作为其绑定源的 TextBox 中,该值变回先前的有效值(如果在我输入 3 之前有 39,则 TextBox 变回到 39).

使用下面的代码可以重现问题:

TestViewModel.cs

public class TestViewModel
{
    public double? NullableValue { get; set; }
}

MainWindow.xaml

<Window x:Class="TestSO34204136TextBoxValidate.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:TestSO34204136TextBoxValidate"
        Title="MainWindow" Height="350" Width="525">

  <Window.DataContext>
    <l:TestViewModel/>
  </Window.DataContext>

  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <TextBlock Text="Nullable: "/>
    <TextBox VerticalAlignment="Top" Grid.Column="1">
      <TextBox.Text>
        <MultiBinding Mode="TwoWay">
          <Binding Path="NullableValue"/>
          <Binding Path="IsFocused"
                   RelativeSource="{RelativeSource Self}"
                   Mode="OneWay"/>
          <MultiBinding.ValidationRules>
            <l:ValidateIsBiggerThanTen/>
          </MultiBinding.ValidationRules>
          <MultiBinding.Converter>
            <l:TestMultiBindingConverter/>
          </MultiBinding.Converter>
        </MultiBinding>
      </TextBox.Text>
    </TextBox>
    <TextBox VerticalAlignment="Top" Grid.Column="2"/>
  </Grid>
</Window>

TestMultiBindingConverter.cs

public class TestMultiBindingConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values[0] != null)
            return values[0].ToString();
        return DependencyProperty.UnsetValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        if (value != null)
        {
            double doubleValue;
            var stringValue = value.ToString();
            if (Double.TryParse(stringValue, out doubleValue))
            {
                object[] values = { doubleValue };
                return values;
            }
        }

        object[] values2 = { DependencyProperty.UnsetValue };
        return values2;
    }
}

ValidateIsBiggerThanTen.cs

public class ValidateIsBiggerThanTen : ValidationRule
{
    private const string errorMessage = "The number must be bigger than 10";

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        var error = new ValidationResult(false, errorMessage);

        if (value == null)
            return new ValidationResult(true, null);

        var stringValue = value.ToString();
        double doubleValue;
        if (!Double.TryParse(stringValue, out doubleValue))
            return new ValidationResult(true, null);

        if (doubleValue <= 10)
            return error;
        return new ValidationResult(true, null);
    }
}

为什么上面示例中的 TextBox 没有显示错误?

您所看到的行为的具体原因是您在 MultiBinding 中绑定了 TextBoxIsFocused 属性。这直接具有在焦点更改时强制更新绑定的 target 的效果。

在验证失败的情况下,有一个非常短暂的时刻验证规则被触发,错误被设置,但焦点实际上还没有改变。但这一切发生得太快,用户看不到。由于验证失败,绑定的源未更新。

因此,当 IsFocused 属性 值更改时,在输入值的验证和拒绝发生后,接下来发生的事情是重新评估绑定(因为其中一个源属性已更改!)以更新目标。由于实际的源值从未改变,目标(TextBox)将从您键入的任何内容恢复为存储在源中的任何内容。


你应该如何解决这个问题?这取决于所需的确切行为。您有三个基本选项:

  • 继续绑定到 IsFocused,并添加 UpdateSourceTrigger="PropertyChanged"。这将保留失去焦点时复制旧值的基本当前行为,但至少会在编辑值时为用户提供即时验证反馈。
  • 完全删除对 IsFocused 的绑定。然后绑定的目标将不依赖于此,并且不会在焦点更改时重新评估。问题解决了。 :)
  • 继续绑定到 IsFocused,并添加逻辑,以便与验证的交互不会导致将过时值复制回 TextBox

根据我们的来回评论,上面的第三个选项似乎是您的方案的首选选项,因为您希望在控件具有焦点时与在控件具有焦点时以不同方式格式化值的文本表示形式没有。


我对用户界面的智慧表示怀疑 格式化 数据取决于控件是否聚焦。当然,焦点变化影响整体视觉呈现是完全合理的,但这通常会涉及下划线、突出显示等内容。根据控件是否聚焦显示完全不同的字符串似乎可能会干扰用户理解和可能也会惹恼他们。

但我同意这是一个主观观点,很明显,在您的情况下,您有这种特定行为,这对您的规范来说是可取的,需要得到支持。因此,考虑到这一点,让我们看看如何实现这种行为……


如果您希望能够绑定到 IsFocused 属性,但如果源实际上尚未更新(即如果验证错误防止这种情况发生),那么您还可以绑定到 Validation.HasError 属性,并使用它来控制转换器的行为。例如:

class TestMultiBindingConverter : IMultiValueConverter
{
    private bool _hadError;

    public object Convert(object[] values,
        Type targetType, object parameter, CultureInfo culture)
    {
        bool? isFocused = values[1] as bool?,
            hasError = values[2] as bool?;

        if ((hasError == true) || _hadError)
        {
            _hadError = true;
            return Binding.DoNothing;
        }

        if (values[0] != null)
        {
             return values[0].ToString() + (isFocused == true ? "" : " (+)");
        }

        return DependencyProperty.UnsetValue;
    }

    public object[] ConvertBack(object value,
        Type[] targetTypes, object parameter, CultureInfo culture)
    {
        if (value != null)
        {
            double doubleValue;
            var stringValue = value.ToString();
            if (Double.TryParse(stringValue, out doubleValue))
            {
                object[] values = { doubleValue };
                _hadError = false;
                return values;
            }

        }
        object[] values2 = { DependencyProperty.UnsetValue };
        return values2;
    }
}

上面添加了一个字段_hadError,表示"remembers"控件最近发生了什么。如果在验证检测到错误时调用转换器,则转换器 returns Binding.DoNothing (其效果如其名称所示:)),并设置标志。此后,无论发生什么,只要设置了该标志,转换器将始终不执行任何操作。

清除标志的唯一方法是用户最终输入有效的文本。然后将调用转换器的 ConvertBack() 方法来更新源,这样做可以清除 _hadError 标志。这确保控件内容永远不会因绑定更新而被覆盖,除非自上次更新源以来没有错误。

上面的 XAML 示例更新为使用附加绑定输入:

<Window x:Class="TestSO34204136TextBoxValidate.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:TestSO34204136TextBoxValidate"
        Title="MainWindow" Height="350" Width="525">

  <Window.DataContext>
    <l:TestViewModel/>
  </Window.DataContext>

  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <TextBlock Text="Nulleable: "/>
    <TextBox x:Name="textBoxWrapper" Grid.Column="1" VerticalAlignment="Top">
      <TextBox.Text>
        <MultiBinding x:Name="TextBoxBinding" Mode="TwoWay"
                      UpdateSourceTrigger="PropertyChanged">
          <Binding Path="NulleableValue"/>
          <Binding Path="IsFocused"
                   RelativeSource="{RelativeSource Self}"
                   Mode="OneWay"/>
          <Binding Path="(Validation.HasError)"
                   RelativeSource="{RelativeSource Self}"
                   Mode="OneWay"/>
          <MultiBinding.ValidationRules>
            <l:ValidateIsBiggerThanTen/>
          </MultiBinding.ValidationRules>
          <MultiBinding.Converter>
            <l:TestMultiBindingConverter/>
          </MultiBinding.Converter>
        </MultiBinding>
      </TextBox.Text>
    </TextBox>
    <TextBox VerticalAlignment="Top" Grid.Column="2"/>
  </Grid>
</Window>

我应该指出,以防它不明显:_hadError 字段是针对 converter 本身的。为了使上述内容正常工作,您需要为应用它的每个绑定创建一个单独的转换器实例。有其他方法可以为每个控件独特地跟踪这样的标志,但我觉得在这方面对这些选项的扩展讨论超出了这个问题的范围。请随意自行探索,如果您自己无法充分解决问题,post 关于该方面的新问题。