For reasons unrelated to this question, I am using ValidationRule
to validate user input, instead of IDataErrorInfo
or other view model-side validation. But I still want the view model to receive the outcome of validation, to allow accurate control of command CanExecute()
results.
To this end, I found and am using the solution here: Passing state of WPF ValidationRule to View Model in MVVM. It seems a bit hacky to me, in that it is overloading the coerce-value callback to also perform initialization tasks. But it does work reasonably well, with two exceptions:
- If a view model field's default value is invalid, this is not detected until the user changes the view's input value.
- When the coerce-value callback in the attached property is called for an item that is in a
DataTemplate
, this apparently happens before the binding for the element has been set up, and theIsDataBound()
method returnsfalse
, bypassing the block of code that would hook up the proxying of theValidation.HasError
property through theValidationEx.HasError
property.
The first issue is annoying, but has a reasonably understandable and legitimate cause, as far as I know. That is, since validation occurs only when data flows back from the target of the binding to the source, and since that hasn't happened until there's user input, it makes sense that invalid data isn't detected until later, when the user provides some input. The work-around is similarly reasonable and not awful: i.e. just call BindingExpression.UpdateSource()
after the element has been fully loaded (e.g. in an event handler for the element's Loaded
event).
It's annoying, because I do feel like WPF ought to be running validation any time the target of the binding has a value that has not yet been validated. But in the vein of "you just set the value programmatically, you ought to know whether it's valid or not", I can see how the design is intended as it is.
The second issue is more problematic. First, it's not clear to me why this should even happen. There doesn't seem to be any good reason for WPF to treat an element declared in a template differently than one declared directly in the main XAML of the view. Indeed, it would make more sense for the two to behave as close to identically as possible.
Second, and the main point of this question: the work-around I've come up with seems like a hack on top of a hack, and I can't help but feel like there ought to be a better way to accomplish all this.
Here is my version of the attached property suggested by the other answer:
static class ValidationEx
{
public static readonly DependencyProperty HasErrorProperty = DependencyProperty.RegisterAttached(
"HasError", typeof(bool), typeof(ValidationEx),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, null, _OnCoerceHasError));
private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached(
"HasErrorDescriptor", typeof(DependencyPropertyDescriptor), typeof(ValidationEx));
public static void SetHasError(DependencyObject target, bool value)
{
target.SetValue(HasErrorProperty, value);
}
public static bool GetHasError(DependencyObject target)
{
return (bool)target.GetValue(HasErrorProperty);
}
private static void SetHasErrorDescriptor(DependencyObject target, DependencyPropertyDescriptor value)
{
target.SetValue(HasErrorDescriptorProperty, value);
}
private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject target)
{
return (DependencyPropertyDescriptor)target.GetValue(HasErrorDescriptorProperty);
}
private static object _OnCoerceHasError(DependencyObject d, object baseValue)
{
bool value = (bool)baseValue;
DependencyPropertyDescriptor descriptor = GetHasErrorDescriptor(d);
if (BindingOperations.IsDataBound(d, HasErrorProperty))
{
if (descriptor == null)
{
descriptor = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType());
descriptor.AddValueChanged(d, _OnSourceHasErrorChanged);
SetHasErrorDescriptor(d, descriptor);
}
}
else if (descriptor != null)
{
descriptor.RemoveValueChanged(d, _OnSourceHasErrorChanged);
SetHasErrorDescriptor(d, null);
}
else
{
// If this statement is commented out, then the in-template element
// will never get hooked up and thus HasError value changes will not
// be reported back to the view model.
System.Windows.Threading.Dispatcher.CurrentDispatcher
.InvokeAsync(() => SetHasError(d, Validation.GetHasError(d)), System.Windows.Threading.DispatcherPriority.Loaded);
}
return value;
}
private static void _OnSourceHasErrorChanged(object sender, EventArgs e)
{
DependencyObject source = (DependencyObject)sender;
SetHasError(source, Validation.GetHasError(source));
}
}
Note that unlike in the other answer, I don't actually ever change the value being coerced. It doesn't actually appear to be needed for the in-main-XAML element, and it doesn't have any use at all in the in-template element.
What I do do is defer a call (via Dispatcher.InvokeAsync()
) to ValidationEx.SetHasError()
if I wind up in the coerce-value callback for an element that doesn't have binding set up yet. This handles the in-template case, as the SetHasError()
call results in another invocation of the coerce-value callback, at a time when the binding for that element has finally been set up, allowing the attached property work correctly for that element as well.
Here is a simple demonstration XAML:
<Window x:Class="TestAttachedHasError.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:TestAttachedHasError"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type l:ViewModel}">
<TextBox Name="textBox1" Margin="5" Loaded="textBox_Loaded"
l:ValidationEx.HasError="{Binding HasErrorValue, Mode=OneWayToSource}">
<TextBox.Text>
<Binding Path="Text" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<l:StringValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<l:ViewModel Text=""/>
</Window.DataContext>
<StackPanel>
<ContentControl Content="{Binding}"/>
<TextBox Name="textBox2" Loaded="textBox_Loaded" Margin="5"
l:ValidationEx.HasError="{Binding HasErrorValue2, Mode=OneWayToSource}">
<TextBox.Text>
<Binding Path="Text" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<l:StringValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Content="Button 1" IsEnabled="{Binding HasErrorValue}" HorizontalAlignment="Left"/>
<Button Content="Button 2" IsEnabled="{Binding HasErrorValue2}" HorizontalAlignment="Left"/>
</StackPanel>
</Window>
There are two TextBox
elements, one declared in a template and the other in the main XAML. They are both bound to the same text value property in the view model, but they each have their own ValidationEx.HasError
source property, to illustrate the difference in behaviors. I also added buttons as a convenient element that has an obvious visual feedback for the IsEnabled
property, just to monitor the respective properties on the view model.
The validation rule here is very simple, just checking to make sure the string is non-empty:
class StringValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
string text = (string)value;
return string.IsNullOrWhiteSpace(text) ?
new ValidationResult(false, "invalid text value") :
new ValidationResult(true, null);
}
}
Here is the view model for the example:
class ViewModel : INotifyPropertyChanged
{
private string _text;
private bool _hasError;
private bool _hasError2;
public string Text
{
get { return _text; }
set { _UpdateValue(ref _text, value); }
}
public bool HasErrorValue
{
get { return _hasError; }
set { _UpdateValue(ref _hasError, value); }
}
public bool HasErrorValue2
{
get { return _hasError2; }
set { _UpdateValue(ref _hasError2, value); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void _UpdateValue<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
{
if (!object.Equals(field, newValue))
{
field = newValue;
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
And for completeness, the MainWindow
class with the event handler for the Loaded
event:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void textBox_Loaded(object sender, RoutedEventArgs e)
{
DependencyObject textBox = (DependencyObject)sender;
BindingOperations.GetBindingExpression(textBox, TextBox.TextProperty).UpdateSource();
}
}
How can I implement this attached property in such a way that a) it is initialized in a less "hacky" way, and/or b) can be initialized identically regardless of whether the target element is in a template or not?
Aucun commentaire:
Enregistrer un commentaire