Связывание 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();
}
}
}