Связывание DataContext с ValidationRule

У меня есть пользовательский ValidationRule, который требует доступа к ViewModel для проверки поставляемого значения в сочетании с другими свойствами ViewModel. Я ранее пытался добиться этого, используя ValidationGroup, но отказался от этой идеи, поскольку код, который я изменяю, нуждался бы в большом количестве рефакторинга, чтобы включить этот маршрут.

Я нашел поток в группе новостей, который показал способ привязки DataContext элемента управления, в котором ValidationRule выполняется для этого ValidationRule через промежуточный класс, унаследованный от DependencyObject, но я не могу заставить его привязываться.

Помогает ли кто-нибудь?

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

В DataContext UserControl устанавливается экземпляр MyViewModel в коде. Я знаю, что это связывание работает как стандартные управляющие привязки, работают как ожидалось.

Метод TotalQuantityValidator.Validate вызывается правильно, но всякий раз, когда я смотрю на свойство ViewModel для Context, он всегда равен нулю (свойство Context объекта TotalQuantityValidator устанавливается в экземпляр TotalQuantityValidatorContext правильно). Однако я вижу из отладчика, что установщик в свойстве ViewModel TotalQuantityValidatorContext никогда не вызывается.

Может ли кто-нибудь посоветовать, как я могу заставить эту привязку работать?

Спасибо заранее.

Ответ 1

Я бы не использовал правила проверки. Если вам нужен доступ к информации в модели просмотра для выполнения проверки, то лучше поставить логику проверки в самой модели view.Model.

Вы можете сделать свой конструктор viewmodel IDataErrorInfo и просто включить проверку данных на основе данных на привязке.

Даже если вы не сталкиваетесь с этой (очень распространенной) проблемой, связанной с необходимостью контекстной информации, правила проверки достоверности не являются отличным способом выражения достоверности: правила проверки обычно связаны с бизнес-логикой или, по крайней мере, с семантическими аспектами вашей информации. Xaml кажется неправильным местом для размещения таких вещей - зачем мне помещать бизнес-правило в исходный файл, основной задачей которого является определение макета и визуального дизайна моего приложения?

Логика проверки правильности еще ниже в вашем приложении. Даже viewmodel может быть неправильным слоем, но в этом случае вы можете просто сделать его обязанностью viewmodel, чтобы решить, где найти логику проверки.

Ответ 2

Я только что нашел отличный ответ!

Если вы установите свойство ValidationStep для ValidationRule в ValidationStep.UpdatedValue, значение, переданное методу Validate, на самом деле является BindingExpression. Затем вы можете запросить свойство DataItem объекта BindingExpression, чтобы получить модель, с которой привязывается привязка.

Это означает, что теперь я могу проверить значение, которое было назначено вместе с существующими значениями других свойств, как я хочу.

Ответ 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:

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

Теперь вы можете удалить класс Context и просто иметь свойство непосредственно в ValidationRule, содержащем ViewModel.

ИЗМЕНИТЬ

Основываясь на вашем комментарии, я предлагаю внести небольшое изменение в код (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 };
}

Это обновит ваш контекст всякий раз, когда изменяется ваша модель просмотра.

Ответ 4

После некоторых исследований я придумал следующий код, который работает точно так же, как работает 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;
    }

Ответ 5

Я знаю, что это старые вопросы, но я был в той же ситуации, что и первоначальный плакат, поддерживающий существующее приложение, и не хотел полностью переписывать его, и я нашел способ обойти это, что работает, по крайней мере, в моей ситуации.

Я пытался проверить значение, помещенное в текстовое поле пользователем, но не хотел, чтобы значение возвращалось к модели, если значение недействительно. Однако для проверки я должен был получить доступ к другим свойствам объекта DataContext, чтобы знать, был ли вход действительным или нет.

В результате я создал свойство класса validator, который я создал, который содержит объект типа, которым должен обладать 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, но он работает для моей ситуации и не требует полной перезаписи системы проверки.

Ответ 6

Я использую другой подход. Используйте объекты Freezable для создания привязок

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

Ответ 7

Я использую другой подход. Используйте объекты 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, вы здесь: публичный класс 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();
    }
}

}