开发者

Binding DataContext to ValidationRule

I have a custom ValidationRule that requires access to the ViewModel in order to validate a supplied value in conjunction with other properties of the ViewModel. I previously tried to acheive this by using a ValidationGroup, but abandoned this idea as the code I am modifying would need a lot of refactoring in order to enable this route.

I found a thread on a newsgroup that showed a way of binding the DataContext of a control in which the ValidationRule is being run to that ValidationRule by way of an intermediate class inherited from DependencyObject, but I cannot get it to bind.

Can anybody help?

My ValidationRule is as follows...

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 is defined as follows...

public abstract class CustomValidationRule : ValidationRule {

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

    public string ErrorMessage { get; set; }

}

TotalQuantityValidatorContext is defined as follows...

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) {
    }

}

And the whole thing is used thus...

<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>
             开发者_Go百科       <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>

The DataContext of the UserControl is being set to an instance of MyViewModel in code-behind. I know that this binding works as the standard control bindings are operating as expected.

The TotalQuantityValidator.Validate method is called correctly, but whenever I look at the ViewModel property of the Context, it is always null (the Context property of the TotalQuantityValidator is being set to an instance of TotalQuantityValidatorContext correctly). I can see from the debugger however that the setter on the ViewModel property of the TotalQuantityValidatorContext is never called.

Can anybody advise as to how I can get this binding to work?

Thanks in advance.


I would avoid using validation rules. If you need access to the information in the viewmodel to perform validation, then it's better to put the validation logic in the viewmodel itself.

You can make your viewmodel implement IDataErrorInfo, and simply turn on data error info-based validation on the binding.

Even if you don't run into this (very common) problem of needing contextual information, validation rules aren't really a great way to express validation: validation rules are usually related to business logic, or at least to semantic aspects of your information. Xaml seems like the wrong place to put such things - why would I put a business rule in the source file whose main job is to determine the layout and visual design of my application?

Validation logic belongs further down in your app. Even the viewmodel might be the wrong layer, but in that case, you can simply make it the viewmodel's responsibility to work out where to find the validation logic.


I have just found a perfect answer!

If you set the ValidationStep property of the ValidationRule to ValidationStep.UpdatedValue, the value passed to the Validate method is actually a BindingExpression. You can then interrogate the DataItem property of the BindingExpression object to get the model to which the Binding is bound.

This means that I can now validate the value that has been assigned along with the existing values of other properties as I want.


The problem you are having is that your DataContext is being set after you have created the validation rule and there is no notification that it has changed. The simplest way to solve the problem is to change the xaml to the following:

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

And then set up the Context directly after setting the DataContext:

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

You could actually remove the Context class now and just have a property directly on the ValidationRule containing the ViewModel.

EDIT

Based on your comment I now suggest a slight change to the above code (the XAML is fine) to the following:

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 };
}

This will update your context whenever your viewmodel changes.


After some research, I have come up with the following code which works exactly as the DataErrorValidationRule works.

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;
    }


I know this is an old questions but I was in the same situation as the initial poster maintaining an existing application and didn't want to rewrite it totally and I ended up finding a way around this that works at least in my situation.

I was trying to validate a value placed into a text box by the user, but didn't want to commit the value back to the model if the value was not valid. However in order to validate I needed to access other properties of the DataContext object to know if the input was valid or not.

What I ended up doing was creating a property on the validator class that I had created that holds an object of the type that the datacontext should be. In that handler I added this code:

        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 { }
        }

This code basically uses the textbox that got the focus, gets it's binding and finds the validator class that is it's first (and only) ValidationRule. Then I have a handle on the class and can just set it's property to the DataContext of the textbox. Since this is done when the textbox first gets focus it is setting the value before any user input can be done. When the user inputs some value, then, the property is already set and can be used in the validator class.

I did put in the following in my validator class just in case it ever gets there without the property being correctly set:

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

However that validation error has never come up.

Kind of hack-ish but it works for my situation and doesn't require a full re-write of the validation system.


I use a different approch. Use Freezable objects to make your bindings

  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 -->


I use a different approch. Use Freezable objects to make your bindings

<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>

As for the Binding proxy, here you go: 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();
    }
}

}

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜