Binding DataContext to ValidationRule

前端 未结 7 1917
粉色の甜心
粉色の甜心 2021-02-04 11:33

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 tr

相关标签:
7条回答
  • 2021-02-04 11:59

    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 讨论(0)
  • 2021-02-04 12:02

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

    0 讨论(0)
  • 2021-02-04 12:12

    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.

    0 讨论(0)
  • 2021-02-04 12:13

    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.

    0 讨论(0)
  • 2021-02-04 12:13

    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.

    0 讨论(0)
  • 2021-02-04 12:15

    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;
        }
    
    0 讨论(0)
提交回复
热议问题