2010-12-03 2 views
10

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

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

Может ли кто-нибудь помочь?

Мой 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 правильно). Однако я вижу из отладчика, что установщик в свойстве ViewModelTotalQuantityValidatorContext никогда не вызывается.

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

Заранее спасибо.

+0

Я знаю, что этот вопрос похож на http://stackoverflow.com/questions/3577899/wpf-property-in-validationrule-never-set, но я ищу доступ к DataContext, а не только к другому свойству. – 2010-12-03 13:05:29

ответ

4

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

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

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

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

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

Вы можете фактически удалить класс контекста прямо сейчас и просто имеют свойство непосредственно в ValidationRule, содержащем ViewModel.

EDIT

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

Это обновит контекст, когда ваш ViewModel изменения.

+0

У меня есть исходный DataContext, который устанавливается в XAML, который затем может быть заменен кодом-позади (это форма ввода данных - при первом использовании он использует DataContext, установленный XAML, после сохранения затем назначает новый пустой модели к DataContext). Почему тогда сеттер не вызывается для исходного значения DataContext (тем более, что изменение DataContext отражается остальными связями)? – 2010-12-03 13:20:36

+1

Ну, я, по крайней мере, понял, почему это не работает :). Объект, который вы связываете (TotalQuantityValidatorContext), не является частью визуального или логического дерева, и поэтому он не может видеть «myUserControl» в его привязке. Вы должны указать прямой источник, но поскольку ваш источник (сама ВМ) изменяется, вы не можете напрямую привязываться к нему. Мое предложение (если вы все еще делаете это в XAML) должно делать то, что я указал выше (просто отредактируйте его так, чтобы он работал лучше с измененной виртуальной машиной) – 2010-12-03 14:12:58

+0

Право - ответ обновился - это немного пошло на отслеживание - спасибо за удовольствие :) – 2010-12-03 14:18:08

5

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

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

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

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

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

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

0

Я использую другой подход. Используйте 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 -->

0

Я использую другой Approch. Используйте 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(); 
    } 
} 

}