【问题标题】:Passing state of WPF ValidationRule to View Model in MVVM将 WPF ValidationRule 的状态传递给 MVVM 中的视图模型
【发布时间】:2012-05-22 17:05:13
【问题描述】:

我陷入了一个看似常见的要求。我有一个 WPF Prism(用于 MVVM)应用程序。我的模型实现了 IDataErrorInfo 进行验证。 IDataErrorInfo 适用于非数字属性。但是,对于数字属性,如果用户输入了无效字符(不是数字),那么数据甚至不会到达模型,因为 wpf 无法将其转换为数字类型。

因此,我不得不使用 WPF ValidationRule 为用户提供一些有意义的消息来处理无效的数字条目。视图中的所有按钮都绑定到 prism 的 DelegateCommand(在视图模型中),并且按钮的启用/禁用在视图模型本身中完成。

现在,如果某些 TextBox 的 wpf ValidationRule 失败,我如何将此信息传递给 View Model,以便它可以适当地禁用视图中的按钮?

【问题讨论】:

  • 经常用WPF,看来解决简单的常见问题需要50行代码:-(

标签: wpf mvvm prism


【解决方案1】:

如果您提供自定义的ValidationRule 实现,您可以存储它收到的值,以及存储最后一个结果。伪代码:

public class IsInteger : ValidationRule
{
  private int parsedValue;

  public IsInteger() { }

  public string LastValue{ get; private set; }

  public bool LastParseSuccesfull{ get; private set; }

  public int ParsedValue{ get{ return parsedValue; } }

  public override ValidationResult Validate( object value, CultureInfo cultureInfo )
  {
    LastValue = (string) value;
    LastParseSuccesfull = Int32.TryParse( LastValue, cultureInfo, ref parsedValue );
    return new ValidationResult( LastParseSuccesfull, LastParseSuccesfull ? "not a valid number" : null );
  }
}

【讨论】:

  • 我不确定我是否完全理解您要解释的内容。我的问题是无效值(对于属性)永远不会到达模型,所以我不能使用 IDataErrorInfo 来验证属性。例如,假设有一个年龄属性的文本框。第一个用户输入有效年龄(比如 20 岁)。然后稍后他将年龄文本框更改为非数字(比如 abc)。现在这个不正确的值永远不会到达模型,尽管 ValidationRule 能够检测到相同的值。模型的年龄仍然是旧的有效值 (20)。所有 by 按钮都绑定到 ViewModel,我只能从那里禁用它们。
【解决方案2】:

有人在这里解决了这个问题(不幸的是它在 VB 中),方法是在 VM 中创建一个似乎绑定到 Validation.HasError 的依赖属性 HasError。我还没有完全理解它,但它可能会对你有所帮助:

http://wpfglue.wordpress.com/2009/12/03/forwarding-the-result-of-wpf-validation-in-mvvm/

【讨论】:

    【解决方案3】:

    涅槃

    解决此特定问题的最简单方法是使用 数字文本框,它可以防止用户输入无效值(您可以通过第三方供应商执行此操作,或者找到一个开放的源解决方案,例如从禁止非数字输入的 Textbox 派生的类)。

    在不执行上述操作的情况下在 MVVM 中处理此问题的第二种方法是在 ViewModel 中定义另一个字段,它是一个字符串,并将该字段绑定到您的文本框。然后,在您的字符串字段的设置器中,您可以设置整数,并为您的数字字段分配一个值:

    这是一个粗略的例子:(注意我没有测试它,但它应该给你的想法)

    // original field
    private int _age;
    int Age 
    {
       get { return _age; }
       set { 
         _age = value; 
         RaisePropertyChanged("Age");
       }
    }
    
    
    private string _ageStr;
    string AgeStr
    {
       get { return _ageStr; }
       set { 
         _ageStr = value; 
         RaisePropertyChanged("AgeStr");
         if (!String.IsNullOrEmpty(AgeStr) && IsNumeric(AgeStr) )
             Age = intVal;
        }
    } 
    
    private bool IsNumeric(string numStr)
    {
       int intVal;
       return int.TryParse(AgeStr, out intVal);
    }
    
    #region IDataErrorInfo Members
    
        public string this[string columnName]
        {
            get
            {
    
                if (columnName == "AgeStr" && !IsNumeric(AgeStr)
                   return "Age must be numeric";
            }
        }
    
        #endregion
    

    【讨论】:

    • 亚历克斯,感谢您的回答。我正在使用您建议的第二种方式。尽管对于当前的问题,这种方法就足够了,但我一直在寻找一个通用的解决方案来结合使用 ValidationRule 和 IDateErrorInfo。我在解决这个问题时遇到了一个问题,并发布在这里 (stackoverflow.com/questions/10629278/…)。我将非常感谢您在这个问题上的建议。由于我当前的问题已解决,因此我会将您的此答案标记为正确答案。干杯。涅槃
    • 我强烈建议不要使用验证规则。它们处于绑定级别,旨在用于代码后面,不适合 MVVM。您也许可以让它们一起工作,但这需要将您的 ViewModel 连接到 View 并浏览逻辑树检查绑定并将其绑定回 ViewModel。当我想到这个时,我不寒而栗。看看这个:stackoverflow.com/questions/63646/…
    • 就我自己而言,我总是会尝试遵循阻力最小的路径,并简单地使用不涉及无法进入 ViewModel 的无效输入的控件。我希望所有验证代码都在 ViewModel 中,因此在一个地方,并在我的控制之下。
    • Alex,确实验证规则不适合 MVVM。我想我会听从你的建议并完全避免它。非常感谢您的建议。
    【解决方案4】:

    对于 MVVM,我更喜欢将 Attached Properties 用于此类事物,因为它们是可重用的,并且可以保持视图模型的清洁。

    为了将 Validation.HasError 属性绑定到您的视图模型,您必须创建一个附加属性,该属性具有 CoerceValueCallback,该属性将您的附加属性的值与您正在验证用户输入的控件上的 Validation.HasError 属性同步开。

    This 文章解释了如何使用这种技术来解决通知视图模型 WPF ValidationRule 错误的问题。代码在 VB 中,所以如果您不是 VB 人员,我将其移植到 C#。

    附加属性

    public static class ValidationBehavior
    {
        #region Attached Properties
    
        public static readonly DependencyProperty HasErrorProperty = DependencyProperty.RegisterAttached(
            "HasError",
            typeof(bool),
            typeof(ValidationBehavior),
            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, null, CoerceHasError));
    
        private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached(
            "HasErrorDescriptor",
            typeof(DependencyPropertyDescriptor),
            typeof(ValidationBehavior));
    
        #endregion
    
        private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
        {
            return (DependencyPropertyDescriptor)d.GetValue(HasErrorDescriptorProperty);
        }
    
        private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
        {
            d.SetValue(HasErrorDescriptorProperty, value);
        }
    
        #region Attached Property Getters and setters
    
        public static bool GetHasError(DependencyObject d)
        {
            return (bool)d.GetValue(HasErrorProperty);
        }
    
        public static void SetHasError(DependencyObject d, bool value)
        {
            d.SetValue(HasErrorProperty, value);
        }
    
        #endregion
    
        #region CallBacks
    
        private static object CoerceHasError(DependencyObject d, object baseValue)
        {
            var result = (bool)baseValue;
            if (BindingOperations.IsDataBound(d, HasErrorProperty))
            {
                if (GetHasErrorDescriptor(d) == null)
                {
                    var desc = DependencyPropertyDescriptor.FromProperty(System.Windows.Controls.Validation.HasErrorProperty, d.GetType());
                    desc.AddValueChanged(d, OnHasErrorChanged);
                    SetHasErrorDescriptor(d, desc);
                    result = System.Windows.Controls.Validation.GetHasError(d);
                }
            }
            else
            {
                if (GetHasErrorDescriptor(d) != null)
                {
                    var desc = GetHasErrorDescriptor(d);
                    desc.RemoveValueChanged(d, OnHasErrorChanged);
                    SetHasErrorDescriptor(d, null);
                }
            }
            return result;
        }
        private static void OnHasErrorChanged(object sender, EventArgs e)
        {
            var d = sender as DependencyObject;
            if (d != null)
            {
                d.SetValue(HasErrorProperty, d.GetValue(System.Windows.Controls.Validation.HasErrorProperty));
            }
        }
    
        #endregion
    }
    

    在 XAML 中使用附加属性

    <Window
      x:Class="MySolution.MyProject.MainWindow"
      xmlns:v="clr-namespace:MyNamespace;assembly=MyAssembly">  
        <TextBox
          v:ValidationBehavior.HasError="{Binding MyPropertyOnMyViewModel}">
          <TextBox.Text>
            <Binding
              Path="ValidationText"
              UpdateSourceTrigger="PropertyChanged">
              <Binding.ValidationRules>
                <v:SomeValidationRuleInMyNamespace/>
              </Binding.ValidationRules>
            </Binding>
         </TextBox.Text>
      </TextBox>
    </ Window >
    

    现在视图模型上的属性将与文本框上的 Validation.HasError 同步。

    【讨论】:

    • 适用于基本的内联 XAML,但似乎不适用于数据模板元素。最初调用强制值回调,但似乎在设置数据绑定之前(即IsDataBound() 返回false)。我可以删除对IsDataBound() 的检查并且它可以工作,但是我冒着内存泄漏的风险。知道如何让这种方法在数据模板场景中工作吗?
    【解决方案5】:

    您必须根据绑定类型属性指定自定义用户控件。 例如,如果您的属性是 int 类型,则必须放置除整数类型外不允许其他值的控件。

    你可以放入 PreviewTextInput="NumberValidationTextBox" 的逻辑。

    private void NumberValidationTextBox(object sender, TextCompositionEventArgs e)
        { 
            Regex regex = new Regex("[^0-9]+");
            e.Handled = regex.IsMatch(e.Text);
        }
    

    只需插入您的逻辑或放置自定义控件即可完成。

    当然也必须实现 mvvm 验证。

    【讨论】:

    • 好的,这很简单,但不是很好。现在是解决方案。更好。
    【解决方案6】:
    1. 在您的模型或视图模型中实施 IDataErrorInfo,具体取决于绑定属性的逻辑。您可以在这两个类中实现。

    2. 在您的基础验证类中也实现这一点。这里验证会在绑定IDataErrorInfo 不起作用时触发。

      public virtual bool HasError
      {
          get { return _hasError; } 
          set
          {
              // if (value.Equals(_hasError)) return;
              _hasError = value;
              RaisePropertyChanged(() => HasError);
          }
      }
      
    3. 接下来,添加全局类

      public class ProtocolSettingsLayout
      {
          public static readonly DependencyProperty MVVMHasErrorProperty = DependencyProperty.RegisterAttached("MVVMHasError", typeof(bool), typeof(ProtocolSettingsLayout), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, null, CoerceMVVMHasError));
      
          public static bool GetMVVMHasError(DependencyObject d)
          {
              return (bool)d.GetValue(MVVMHasErrorProperty);
          }
      
          public static void SetMVVMHasError(DependencyObject d, bool value)
          {
              d.SetValue(MVVMHasErrorProperty, value);
          }
      
          private static object CoerceMVVMHasError(DependencyObject d, Object baseValue)
          {
              bool ret = (bool)baseValue;
      
              if (BindingOperations.IsDataBound(d, MVVMHasErrorProperty))
              {
                  if (GetHasErrorDescriptor(d) == null)
                  {
                      DependencyPropertyDescriptor desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType());
                      desc.AddValueChanged(d, OnHasErrorChanged);
                      SetHasErrorDescriptor(d, desc);
                      ret = System.Windows.Controls.Validation.GetHasError(d);
                  }
              }
              else
              {
                  if (GetHasErrorDescriptor(d) != null)
                  {
                      DependencyPropertyDescriptor desc = GetHasErrorDescriptor(d);
                      desc.RemoveValueChanged(d, OnHasErrorChanged);
                      SetHasErrorDescriptor(d, null);
                  }
              }
              return ret;
          }
      
          private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached("HasErrorDescriptor",
                                                                                  typeof(DependencyPropertyDescriptor),
                                                                                  typeof(ProtocolSettingsLayout));
      
          private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
          {
              var ret = d.GetValue(HasErrorDescriptorProperty);
              return ret as DependencyPropertyDescriptor;
          }
      
          private static void OnHasErrorChanged(object sender, EventArgs e)
          {
              DependencyObject d = sender as DependencyObject;
      
              if (d != null)
              {
                  d.SetValue(MVVMHasErrorProperty, d.GetValue(Validation.HasErrorProperty));
              }
          }
      
          private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
          {
              var ret = d.GetValue(HasErrorDescriptorProperty);
              d.SetValue(HasErrorDescriptorProperty, value);
          }
      }
      
    4. xml

      <TextBox  PreviewTextInput="NumValidationTextBox" Text="{Binding ESec, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, NotifyOnValidationError=true, ValidatesOnExceptions=True, NotifyOnSourceUpdated=True, NotifyOnTargetUpdated=True, TargetNullValue='0', FallbackValue='0' }" Validation.ErrorTemplate="{StaticResource validationTemplate}" viewmodels:ProtocolSettingsLayout.MVVMHasError="{Binding Path=HasError}" />    
      

    【讨论】:

    • 100% 有效。我已经测试过了。不需要实现验证规则或自定义控件。
    【解决方案7】:

    从 .NET 4.5 开始,ValidationRule 重载了 Validate 方法:

    public ValidationResult Validate(object value, CultureInfo cultureInfo,
        BindingExpressionBase owner)
    

    您可以通过这种方式覆盖它并获取视图模型:

    public override ValidationResult Validate(object value, 
        CultureInfo cultureInfo, BindingExpressionBase owner)
    {
        ValidationResult result = base.Validate(value, cultureInfo, owner);
        var vm = (YourViewModel)((BindingExpression)owner).DataItem;
        // ...
        return result;
    }
    

    【讨论】:

    • 也许我遗漏了一些东西,但是如何从视图中调用它?据我所知,为绑定设置验证规则默认使用抽象的 Validate 方法。
    • 抽象的Validate方法被虚调用。源代码在这里:referencesource.microsoft.com/#PresentationFramework/Framework/…
    • @Maxence 此链接不再可用。我和克里斯有同样的问题。潜力巨大,因为 ValidationRule 可以让 VM 使用 INotifyDataErrorInfo 执行验证。请更详细地解释如何强制重载 Validate() 运行?使用 ValidationRule 而不是仅绑定的好处是可以保留 VM 属性类型(例如 Integer),并将 ValidationRule 应用于视图控件类型(示例字符串)。可以在绑定引擎吞没异常之前执行验证,以便更新 VM 上的 HasError 属性。
    【解决方案8】:

    我遇到了同样的问题并用一个技巧解决了它。 请参阅下面的转换器:

    public class IntValidationConverter : IValueConverter
    {
        static string[] AllValuse = new string[100000];
        static int index = 1;
        public static int StartOfErrorCodeIndex = -2000000000;
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null) return null;
            if (value.ToString() == "") return null;
    
            int iValue = (int)(value);
    
            if (iValue == int.MinValue) return null;
    
            if (iValue >= StartOfErrorCodeIndex) return value;
            if ((iValue < IntValidationConverter.StartOfErrorCodeIndex) && (iValue > int.MinValue)) return AllValuse[StartOfErrorCodeIndex - iValue];
    
            return null;
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null) return int.MinValue;
            if (value.ToString() == "") return int.MinValue;
    
            int result;
            bool success = int.TryParse(value.ToString(), out result);
            if (success) return result;
    
            index++;
            AllValuse[index] = value.ToString();
            return StartOfErrorCodeIndex - index;
        }
    }
    

    【讨论】:

      【解决方案9】:

      我和你有同样的问题,但是我用另一种方式解决了,当输入无效时,我使用触发器来禁用按钮。同时,文本框绑定应使用ValidatesOnExceptions=true

      <Style TargetType="{x:Type Button}">
      <Style.Triggers>
          <DataTrigger Binding="{Binding ElementName=tbInput1, Path=(Validation.HasError)}" Value="True">
              <Setter Property="IsEnabled" Value="False"></Setter>
          </DataTrigger>
      
          <DataTrigger Binding="{Binding ElementName=tbInput2, Path=(Validation.HasError)}" Value="True">
              <Setter Property="IsEnabled" Value="False"></Setter>
          </DataTrigger>
      </Style.Triggers>
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-10-21
        • 1970-01-01
        • 2012-04-18
        • 2011-01-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多