如何验证viewmodel中数据的视图控件

时间:2014-01-31 17:31:10

标签: c# wpf mvvm

我是WPF的新手,我对验证器感兴趣,似乎你需要继承ValidationRule并覆盖Validate函数,这完全与视图模型分离,但如果我想验证怎么办?针对viewmodel中的某些list / collection / set / dictionary,要检查这个新输入是否已经在列表中,一个很好的例子是创建验证以查看是否尚未使用用户名。

2 个答案:

答案 0 :(得分:0)

在WPF中进行验证有几种不同的方法。我能想到两个主要的方法

  1. 创建验证规则,然后将其应用于XAML
  2. 在ViewModel中实现IDataErrorInfo
  3. 验证规则在XAML(GUI)中指定,而实现IDataErrorInfo则将验证逻辑移动到ViewModel(业务逻辑)中。虽然ValidationRules很好,因为您可以创建自己的并重用它们,但它们也无法在您的业务逻辑中提供最有可能需要的验证。

    客户端与服务器端验证的概念很有意思,可能与Silverlight有关,但由于您将其标记为WPF,我假设唯一的区别是验证是在Views还是ViewModels(UI或商业逻辑)。在我看来,即使您的UI验证输入,您的ViewModel仍然需要进行适当的验证。

    因此,我建议实现IDataErrorInfo。顺便说一下,IDataErrorInfo工作的原因是因为存在检查IDataErrorInfo接口的ValidationRule!以下是我将如何在ViewModelBase类中执行此操作的示例:

    注意:以下示例忽略了您可能需要通知INotifyPropertyChanged以更新绑定的事实,而只关注验证。

    public class ViewModelBase : IDataErrorInfo
    {
    
        private Dictionary<string, string> errors = new Dictionary<string, string>();
    
        // required for IDataErrorInfo
        public virtual string Error
        {
            get { return String.Join(Environment.NewLine, errors); }
        }
    
        // required for IDataErrorInfo
        public string this[string propertyName]
        {
            get
            {
                string result;
                errors.TryGetValue(propertyName, out result);
                return result;
            }
        }
    
        // Useful property to check if you have errors
        public bool HasErrors
        {
            get
            {
                return errors.Count > 0;
            }
        }
    
        protected void SetError<T>(string propertyName, String error)
        {
            if (error == null)
                errors.Remove(propertyName);
            else
                errors[propertyName] = error;
    
            OnHasErrorsChanged();
        }
    
        protected string GetError<T>(string propertyName, String error)
        {
            String s;
            errors.TryGetValue(propertyName, out s);
            return s;
        }
    
        protected virtual void OnHasErrorsChanged()
        {
    
        }
    }
    

    然后您的ViewModel可以像这样实现它:

    public class MyViewModel : ViewModelBase
    {
        private string someProperty;
    
        public string SomeProperty
        {
            get
            {
                return someProperty;
            }
            set
            {
                if(someProperty != null)
                {
                    someProperty = value;
                    SetError("SomeProperty", ValidateSomeProperty());
                }
            }
        }
    
        private string ValidateSomeProperty()
        {
            if(String.IsNullOrEmpty(SomeProperty))
                return "Value is required";
            return null;
        }
    }
    

    在你的用户界面中,你需要像这样添加ValidatesOnDataErrors和NotifyOnValidationError:

    Text="{Binding SomeProperty, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
    

    注意:传入字符串来表示属性有点难看(如果重命名属性但忘记重命名字符串,则重构不安全)。当您想要通知DataBindings的属性更改时,INotifyPropertyChanged的方式相同。 Prism的NotificationObject有一个重构安全的解决方案,它看起来像这样:

    使用以下代码替换上一个示例中的GetError / SetError:

        protected void SetError<T>(Expression<Func<T>> prop, String error)
        {
            String propertyName = PropertySupport.ExtractPropertyName(prop);
    
            if (error == null)
                errors.Remove(propertyName);
            else
                errors[propertyName] = error;
    
            OnHasErrorsChanged();
        }
    
        protected string GetError<T>(Expression<Func<T>> prop, String error)
        {
            String propertyName = PropertySupport.ExtractPropertyName(prop);
    
            String s;
            errors.TryGetValue(propertyName, out s);
            return s;
        }
    

    然后我的ViewModelBase是这样的:

    public class ViewModelBase : NotificationObject, IDataErrorInfo
    

    然后实现属性:

    public class MyViewModel : ViewModelBase
    {
        private string someProperty;
    
        public string SomeProperty
        {
            get
            {
                return someProperty;
            }
            set
            {
                if(someProperty != null)
                {
                    someProperty = value;
                    SetError( () => SomeProperty, ValidateSomeProperty()); // update validation for property
                    RaisePropertyChanged( () => SomeProperty); // notify data bindings
                }
            }
        }
    

    我没有展示RaisePropertyChanged的实现,但它是在Prism的NotificationObject中,它是开源的,可以免费下载。你可以自己实现INotifyPropertyChanged并用字符串引发事件(不是重构安全)或者自己实现它类似于上面的SetError实现(提取属性名并用它激活事件)。

    你需要这个辅助方法:

    public static class PropertySupport
    {
        public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpresssion)
        {
            if (propertyExpresssion == null)
            {
                throw new ArgumentNullException("propertyExpression");
            }
    
            var memberExpression = propertyExpresssion.Body as MemberExpression;
            if (memberExpression == null)
            {
                throw new ArgumentException("The expression is not a member access expression.", "propertyExpression");
            }
    
            var property = memberExpression.Member as PropertyInfo;
            if (property == null)
            {
                throw new ArgumentException("The member access expression does not access a property.", "propertyExpression");
            }
    
            var getMethod = property.GetGetMethod(true);
            if (getMethod.IsStatic)
            {
                throw new ArgumentException("The referenced property is a static property.", "propertyExpression");
            }
    
            return memberExpression.Member.Name;
        }
    }
    

    编辑:简化的重构安全替代

    如果您使用的是.NET 4.5或更高版本,则可以使用CallerMemberAttribute之类的this example shows来实现INotifyPropertyChanged和我的SetError实现。那么你不需要通过上面的反射自己提取属性名称(它简化了很多)。

    很抱歉,如果您希望DataBindings和Validation能够正常运行,那么它们可以与我们讨论物业变更通知,但它们是相辅相成的。

答案 1 :(得分:-1)

我终于做到了,这就是:

的Xaml:

<Window.Resources>     
    <l:DataResource x:Key="ValidateFieldMethod" BindingTarget="{Binding IsFieldValid}"/>
</Window.Resources>
            <xctk:IntegerUpDown Width="50" Maximum="300" Minimum="0">
                <xctk:IntegerUpDown.Value>
                    <Binding Path="SelectedItem.TargetPosition" Mode="TwoWay">
                        <Binding.ValidationRules>
                            <l:CustomValidationRule Validator="{l:DataResourceBinding DataResource={StaticResource ValidateFieldMethod}}"  />
                        </Binding.ValidationRules>
                    </Binding>
                </xctk:IntegerUpDown.Value>
            </xctk:IntegerUpDown>

ValidatorRule:

public delegate bool CheckAgainstDataDelegate(object newValue, string fieldName);

public class CustomValidationRule : ValidationRule
{

    public CheckAgainstDataDelegate Validator { get; set; }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo, BindingGroup owner)
    {
        return Validate((object)owner, cultureInfo);
    }
    public override ValidationResult Validate(object value, CultureInfo cultureInfo, BindingExpressionBase owner)
    {
        switch (ValidationStep)
        {
            case ValidationStep.UpdatedValue:
            case ValidationStep.CommittedValue:
                value = (object)owner;
                break;
        }
        return new ValidationResult(Validator(value, ((BindingExpression) owner).ResolvedSourcePropertyName), null);
    }

    [Obsolete("Use Validator property of type delegate instead to validate the data",true)]
    public override ValidationResult Validate(object value, CultureInfo cultureInfo) { return null; } //not used anymore
}

在viewModel中:

    private CheckAgainstDataDelegate _isFieldValid;
    public CheckAgainstDataDelegate IsFieldValid
    {
        get
        {
            return _isFieldValid
                ?? (_isFieldValid = delegate (object newValue,string propertyName)
                {
                    switch (propertyName)
                    {
                        case "TargetPosition":
                            var newV = (int) newValue;
                            return Items.All(e => e.TargetPosition != newV);
                        default:
                            throw  new Exception("Property Assigned to unknown field");
                    }
                });
        }
    }

我使用http://www.wpfmentor.com/2009/01/how-to-add-binding-to-property-on.html的帮助在ValidationrRule中绑定。

你怎么想?