2010-12-03 143 views
10

我有一個自定義ValidationRule,它需要訪問ViewModel以驗證提供的值與ViewModel的其他屬性。我以前試圖通過使用ValidationGroup來實現這一點,但放棄了這個想法,因爲我修改的代碼需要大量的重構才能啓用此路由。將DataContext綁定到ValidationRule

我找到了一個thread on a newsgroup,它顯示了一種通過從DependencyObject繼承的中間類的方式將ValidationRule正在運行的控件的DataContext綁定到該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方法正確調用,但每當我看ContextViewModel財產,它總是空(在TotalQuantityValidatorContext屬性被設置爲TotalQuantityValidatorContext實例正確)。然而,我可以從調試器看到,TotalQuantityValidatorContextViewModel屬性上的setter從未被調用過。

有人可以告訴我如何讓這個綁定工作?

在此先感謝。

+0

我知道這個問題是相似http://stackoverflow.com/questions/3577899/wpf-property-in-validationrule-never-set,但我期待訪問DataContext,而不僅僅是它的另一個屬性。 – 2010-12-03 13:05:29

回答

4

我會避免使用驗證規則。如果您需要訪問viewmodel中的信息來執行驗證,那麼最好將驗證邏輯放入視圖模型本身中。

你可以讓你的viewmodel實現IDataErrorInfo,只需在綁定上打開基於數據錯誤信息的驗證。

即使您沒有遇到需要上下文信息的這種(非常常見的)問題,驗證規則也不是真正的表達驗證的好方法:驗證規則通常與業務邏輯有關,或者至少與語義您的信息的各個方面。 Xaml似乎是錯誤的地方放置這些東西 - 爲什麼我會在源文件中放置一個業務規則,其主要工作是確定應用程序的佈局和視覺設計?

驗證邏輯屬於應用程序的更深層次。即使視圖模型可能是錯誤的層次,但在這種情況下,您可以簡單地將視圖模型作爲視圖模型的責任來確定在哪裏找到驗證邏輯。

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類並且直接在包含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 }; 
} 

這將更新您的上下文時,您的視圖模型變化。

+0

我確實在XAML中設置了一個初始的DataContext,然後它可以被代碼隱藏替代(這是一個數據輸入表單 - 當它首次使用XAML設置的DataContext時,在保存之後會分配一個新的空模型到DataContext)。那麼爲什麼setter不會被調用爲原始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

我剛剛找到了一個完美的答案!

如果您將ValidationRule的ValidationStep屬性設置爲ValidationStep.UpdatedValue,則傳遞給Validate方法的值實際上是BindingExpression。然後,您可以詢問BindingExpression對象的DataItem屬性以獲取Binding綁定到的模型。

這意味着我現在可以根據需要驗證已經分配的值以及其他屬性的現有值。

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

此代碼基本上使用,進行了重點的文本,得到它的結合,並認爲該校驗器類,是它的第一個(也是唯一一個)有效性規則。然後我有一個類的句柄,可以將它的屬性設置爲文本框的DataContext。由於這是在文本框首先獲得焦點時完成的,因此在任何用戶輸入完成之前都要設置該值。當用戶輸入一些值時,那麼屬性已經設置好了,並且可以在驗證器類中使用。

我把在我的驗證類中的下列以防萬一它曾經到達那裏沒有屬性被設置正確:

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

但是,這種驗證錯誤從來沒有上來。

一種黑客行爲,但它適用於我的情況,並不需要完全重寫驗證系統。

0

我使用不同的接頭。使用可凍結對象,使您的綁定

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

我使用不同的計算策略。使用可凍結對象,使您的綁定

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

至於粘合代理,在這裏你去: 公共類BindingProxy:可凍結 {

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

}