将DataContext绑定到ValidationRule

时间:2010-12-03 12:39:31

标签: .net wpf binding

我有一个自定义ValidationRule,需要访问ViewModel才能与ViewModel的其他属性一起验证提供的值。我之前试图通过使用ValidationGroup来实现这一点,但放弃了这个想法,因为我修改的代码需要大量的重构才能启用此路由。

我找到了一个thread on a newsgroup,它显示了一种绑定控件的DataContext的方法,其中ValidationRule通过继承自DependencyObject的中间类运行到ValidationRule,但是我无法让它绑定。

有人可以帮忙吗?

我的ValidationRule如下......

class TotalQuantityValidator : CustomValidationRule {

    public TotalQuantityValidator()
        : base(@"The total number must be between 1 and 255.") {
    }

    public TotalQuantityValidatorContext Context { get; set; }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo) {

        ValidationResult validationResult = ValidationResult.ValidResult;

        if (this.Context != null && this.Context.ViewModel != null) {

            int total = ...
            if (total <= 0 || total > 255) {
                validationResult = new ValidationResult(false, this.ErrorMessage);
            }

        }

        return validationResult;

    }

}

CustomValidationRule定义如下......

public abstract class CustomValidationRule : ValidationRule {

    protected CustomValidationRule(string defaultErrorMessage) {
        this.ErrorMessage = defaultErrorMessage;
    }

    public string ErrorMessage { get; set; }

}

TotalQuantityValidatorContext定义如下......

public class TotalQuantityValidatorContext : DependencyObject {

    public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(@"ViewModel",
        typeof(MyViewModel), typeof(TotalQuantityValidatorContext),
        new PropertyMetadata {
            DefaultValue = null,
            PropertyChangedCallback = new PropertyChangedCallback(TotalQuantityValidatorContext.ViewModelPropertyChanged)
        });

    public MyViewModel ViewModel {
        get { return (MyViewModel)this.GetValue(TotalQuantityValidatorContext.ViewModelProperty); }
        set { this.SetValue(TotalQuantityValidatorContext.ViewModelProperty, value); }
    }

    private static void ViewModelPropertyChanged(DependencyObject element, DependencyPropertyChangedEventArgs args) {
    }

}

因此使用了整个事情......

<UserControl x:Class="..."
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:val="clr-namespace:Validators" x:Name="myUserControl">

    <TextBox Name="myTextBox">
        <TextBox.Text>
            <Binding NotifyOnValidationError="True" Path="myViewModelProperty" UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <val:TotalQuantityValidator>
                        <val:TotalQuantityValidator.Context>
                            <val:TotalQuantityValidatorContext ViewModel="{Binding ElementName=myUserControl, Path=DataContext}" />
                        </val:TotalQuantityValidator.Context>
                    </val:TotalQuantityValidator>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>

</UserControl>

UserControl的DataContext被设置为代码隐藏中的MyViewModel实例。我知道这个绑定有效,因为标准控件绑定按预期运行。

正确调用TotalQuantityValidator.Validate方法,但每当我查看ViewModel的{​​{1}}属性时,它始终为null(Context属性Context 1}}被正确设置为TotalQuantityValidator的实例。我可以从调试器看到,TotalQuantityValidatorContext的{​​{1}}属性上的setter永远不会被调用。

有人可以建议如何让这种绑定工作吗?

提前致谢。

7 个答案:

答案 0 :(得分:5)

我会避免使用验证规则。如果您需要访问viewmodel中的信息来执行验证,那么最好将验证逻辑放在viewmodel本身中。

您可以使您的viewmodel实现IDataErrorInfo,并简单地在绑定上启用基于数据错误信息的验证。

即使您没有遇到需要上下文信息的这个(非常常见的)问题,验证规则也不是表达验证的好方法:验证规则通常与业务逻辑相关,或者至少与语义方面相关你的信息Xaml似乎是放置这些东西的错误地方 - 为什么我会在源文件中放置一个业务规则,其主要工作是确定我的应用程序的布局和可视化设计?

验证逻辑属于你的应用程序。即使是viewmodel也可能是错误的图层,但在这种情况下,您可以简单地让viewmodel负责找出验证逻辑的位置。

答案 1 :(得分:5)

我刚刚找到了一个完美的答案!

如果将ValidationRule的ValidationStep属性设置为ValidationStep.UpdatedValue,则传递给Validate方法的值实际上是BindingExpression。然后,您可以询问BindingExpression对象的DataItem属性,以获取绑定绑定的模型。

这意味着我现在可以根据需要验证已分配的值以及其他属性的现有值。

答案 2 :(得分:3)

您遇到的问题是您在创建验证规则后设置了DataContext,并且没有通知它已更改。解决问题的最简单方法是将xaml更改为以下内容:

<TextBox.Text>
    <Binding NotifyOnValidationError="True" Path="myViewModelProperty" UpdateSourceTrigger="PropertyChanged">
        <Binding.ValidationRules>
            <local:TotalQuantityValidator x:Name="validator" />
        </Binding.ValidationRules>
    </Binding>
</TextBox.Text>

然后在设置DataContext后直接设置Context:

public MainWindow()
{
    InitializeComponent();
    this.DataContext = new MyViewModel();
    this.validator.Context = new TotalQuantityValidatorContext { ViewModel = (MyViewModel)this.DataContext };
}

您现在可以实际删除Context类,并且只在包含ViewModel的ValidationRule上直接拥有属性。

修改

根据您的评论,我现在建议对以上代码稍作修改(XAML很好),以下内容:

public MainWindow()
{
    this.DataContextChanged += new DependencyPropertyChangedEventHandler(MainWindow_DataContextChanged);
    InitializeComponent();
}

private void MainWindow_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    this.validator.Context = new TotalQuantityValidatorContext { ViewModel = (MyViewModel)this.DataContext };
}

只要您的视图模型发生更改,这将更新您的上下文。

答案 3 :(得分:1)

经过一些研究,我得出了以下代码,它与DataErrorValidationRule的工作方式完全相同。

class VJValidationRule : System.Windows.Controls.ValidationRule
{
    public VJValidationRule()
    {
        //we need this so that BindingExpression is sent to Validate method
        base.ValidationStep = System.Windows.Controls.ValidationStep.UpdatedValue;
    }

    public override System.Windows.Controls.ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        System.Windows.Controls.ValidationResult result = System.Windows.Controls.ValidationResult.ValidResult;

        System.Windows.Data.BindingExpression bindingExpression = value as System.Windows.Data.BindingExpression;

        System.ComponentModel.IDataErrorInfo source = bindingExpression.DataItem as System.ComponentModel.IDataErrorInfo;

        if (source != null)
        {
            string msg = source[bindingExpression.ParentBinding.Path.Path];

            result = new System.Windows.Controls.ValidationResult(msg == null, msg); 
        }

        return result;
    }

答案 4 :(得分:0)

我知道这是一个古老的问题,但我和维护现有应用程序的初始海报的情况相同,并且不想完全重写它,我最终找到了解决这个问题的方法至少在我的情况下有效

我试图验证用户放置在文本框中的值,但如果值无效,则不希望将值提交回模型。但是为了验证我需要访问DataContext对象的其他属性以了解输入是否有效。

我最终做的是在我创建的验证器类上创建一个属性,该属性包含datacontext应该是的类型的对象。在那个处理程序中,我添加了这段代码:

        TextBox tb = sender as TextBox;

        if (tb != null && tb.DataContext is FilterVM)
        {
            try
            {
                BindingExpression be = tb.GetBindingExpression(TextBox.TextProperty);
                Validator v = be.ParentBinding.ValidationRules[0] as Validator;
                v.myFilter = tb.DataContext as FilterVM;
            }
            catch { }
        }

此代码基本上使用获得焦点的文本框,获取它的绑定并找到它是第一个(也是唯一的)ValidationRule的验证器类。然后我有一个类的句柄,可以将它的属性设置为文本框的DataContext。由于这是在文本框首次获得焦点时完成的,因此在完成任何用户输入之前设置该值。当用户输入一些值时,该属性已经设置并可以在验证器类中使用。

我确实在我的验证器类中添加了以下内容,以防它在没有正确设置属性的情况下到达那里:

        if (myFilter == null)
        { return new ValidationResult(false, "Error getting filter for validation, please contact program creators."); }

然而,验证错误从未出现过。

有点hack-ish但它适用于我的情况,并且不需要完全重写验证系统。

答案 5 :(得分:0)

我使用不同的approch。使用Freezable对象进行绑定

&#13;
&#13;
  public class BindingProxy : Freezable
    {
            

        
            static BindingProxy()
            {
                var sourceMetadata = new FrameworkPropertyMetadata(
                delegate(DependencyObject p, DependencyPropertyChangedEventArgs args)
                {
                    if (null != BindingOperations.GetBinding(p, TargetProperty))
                    {
                        (p as BindingProxy).Target = args.NewValue;
                    }
                });

                sourceMetadata.BindsTwoWayByDefault = false;
                sourceMetadata.DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;

                SourceProperty = DependencyProperty.Register(
                    "Source",
                    typeof(object),
                    typeof(BindingProxy),
                    sourceMetadata);

                var targetMetadata = new FrameworkPropertyMetadata(
                    delegate(DependencyObject p, DependencyPropertyChangedEventArgs args)
                    {
                        ValueSource source = DependencyPropertyHelper.GetValueSource(p, args.Property);
                        if (source.BaseValueSource != BaseValueSource.Local)
                        {
                            var proxy = p as BindingProxy;
                            object expected = proxy.Source;
                            if (!object.ReferenceEquals(args.NewValue, expected))
                            {
                                Dispatcher.CurrentDispatcher.BeginInvoke(
                                    DispatcherPriority.DataBind, 
                                    new Action(() =>
                                    {
                                        proxy.Target = proxy.Source;
                                    }));
                            }
                        }
                    });

                targetMetadata.BindsTwoWayByDefault = true;
                targetMetadata.DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
                TargetProperty = DependencyProperty.Register(
                    "Target",
                    typeof(object),
                    typeof(BindingProxy),
                    targetMetadata);
            }
          
public static readonly DependencyProperty SourceProperty;   
            public static readonly DependencyProperty TargetProperty;
       
            public object Source
            {
                get
                {
                    return this.GetValue(SourceProperty);
                }

                set
                {
                    this.SetValue(SourceProperty, value);
                }
            }

           
            public object Target
            {
                get
                {
                    return this.GetValue(TargetProperty);
                }

                set
                {
                    this.SetValue(TargetProperty, value);
                }
            }

            protected override Freezable CreateInstanceCore()
            {
                return new BindingProxy();
            }
        }

sHould This have the problem of binding the value too late after the application started. I use Blend Interactions to resolve the problem after the window loads 

<!-- begin snippet: js hide: false -->
&#13;
&#13;
&#13;

答案 6 :(得分:0)

我使用不同的方法。使用Freezable对象进行绑定

<TextBox Name="myTextBox">
  <TextBox.Resources>
    <att:BindingProxy x:Key="Proxy" Source="{Binding}" Target="{Binding ViewModel, ElementName=TotalQuantityValidator}" />
  </TextBox.Resources>
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="Loaded">
      <ei:ChangePropertyAction PropertyName="Source" TargetObject="{Binding Source={StaticResource MetaDataProxy}}" Value="{Binding Meta}" />
    </i:EventTrigger>
  </i:Interaction.Triggers>
  <TextBox.Text>
    <Binding NotifyOnValidationError="True" Path="myViewModelProperty" UpdateSourceTrigger="PropertyChanged">
      <Binding.ValidationRules>
        <val:TotalQuantityValidator x:Name="TotalQuantityValidator" />
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

至于Binding代理,请转到: public class BindingProxy:Freezable {

    public static readonly DependencyProperty SourceProperty;

    /// <summary>
    /// The target property
    /// </summary>
    public static readonly DependencyProperty TargetProperty;


    /// <summary>
    /// Initializes static members of the <see cref="BindingProxy"/> class.
    /// </summary>
    static BindingProxy()
    {
        var sourceMetadata = new FrameworkPropertyMetadata(
        delegate(DependencyObject p, DependencyPropertyChangedEventArgs args)
        {
            if (null != BindingOperations.GetBinding(p, TargetProperty))
            {
                (p as BindingProxy).Target = args.NewValue;
            }
        });

        sourceMetadata.BindsTwoWayByDefault = false;
        sourceMetadata.DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;

        SourceProperty = DependencyProperty.Register(
            "Source",
            typeof(object),
            typeof(BindingProxy),
            sourceMetadata);

        var targetMetadata = new FrameworkPropertyMetadata(
            delegate(DependencyObject p, DependencyPropertyChangedEventArgs args)
            {
                ValueSource source = DependencyPropertyHelper.GetValueSource(p, args.Property);
                if (source.BaseValueSource != BaseValueSource.Local)
                {
                    var proxy = p as BindingProxy;
                    object expected = proxy.Source;
                    if (!object.ReferenceEquals(args.NewValue, expected))
                    {
                        Dispatcher.CurrentDispatcher.BeginInvoke(
                            DispatcherPriority.DataBind, 
                            new Action(() =>
                            {
                                proxy.Target = proxy.Source;
                            }));
                    }
                }
            });

        targetMetadata.BindsTwoWayByDefault = true;
        targetMetadata.DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
        TargetProperty = DependencyProperty.Register(
            "Target",
            typeof(object),
            typeof(BindingProxy),
            targetMetadata);
    }

    /// <summary>
    /// Gets or sets the source.
    /// </summary>
    /// <value>
    /// The source.
    /// </value>
    public object Source
    {
        get
        {
            return this.GetValue(SourceProperty);
        }

        set
        {
            this.SetValue(SourceProperty, value);
        }
    }

    /// <summary>
    /// Gets or sets the target.
    /// </summary>
    /// <value>
    /// The target.
    /// </value>
    public object Target
    {
        get
        {
            return this.GetValue(TargetProperty);
        }

        set
        {
            this.SetValue(TargetProperty, value);
        }
    }

    /// <summary>
    /// When implemented in a derived class, creates a new instance of the <see cref="T:System.Windows.Freezable" /> derived class.
    /// </summary>
    /// <returns>
    /// The new instance.
    /// </returns>
    protected override Freezable CreateInstanceCore()
    {
        return new BindingProxy();
    }
}

}