问题
I'm still struggling with validation in WPF.
I've got a custom validation rule which requires text to appear in a textbox i.e. it enforces a mandatory field constraint.
<TextBox local:Masking.Mask="^[a-zA-Z0-9]*$" x:Name="CameraIdCodeTextBox" Grid.Row="1" Grid.Column="1">
<Binding Path="CameraIdCode" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True" ValidatesOnExceptions="True">
<Binding.ValidationRules>
<localValidation:RequiredFieldRule />
</Binding.ValidationRules>
</Binding>
</TextBox>
The problem is, that when the Window first loads, there is no text in the TextBox (as you would expect). But the Text property is being bound to a property on the ViewModel, and as such, the validation Rule is firing, indicating that there is a problem with the Window - before the user has even had an opportunity to violate a business rule.
Is this a problem which has been solved before? I can't have been the 1st to experience this. I'm sure it is a trap for young players.
回答1:
There are a couple of patterns for this. I usually implement on the class/Model the ISupportInitialize
interface, which will require you creating BeginInit()
and EndInit()
in those methods I simply set a private bool _isInitializing
to true or false.
In the view model or where/when you create/populate your model/class wrap it with begin and end init:
var o = new SampleObject();
o.BeginInit()
o.StartDate = DateTime.Now; //just some sample property...
o.EndInit();
So then depending how your ValidationRule is invoked, you could check the state of your _isInitializing
to see if you need to validate.
Lately I've been using attribute validators which fire on PropertyChanged
so you could do something like:
[CustomValidator("ValidateStartDate")]
public DateTime StartDate
{ get ...
{
set
{
if(_startDate == value) return;
_startDate = value;
if(_isInitializing) return;
RaisePropertyChange(() => StartDate);
}..
If you don't want to bother with ISupportInitialize
, then pass all the values that you need in your properties during construction not the property. Binding will query the getters on you properties first time and will get their values, and after anything will go through property setter and get validated:
//c-tor
public MyObject(DateTime start)
{
_startDate = start;
}
回答2:
It's been a while and I should have updated this question. I resolved it using a class which I found in a WPF book by Ian Griffths (an O'Reilly book):
public static class Validator
{
/// <summary>
/// This method forces WPF to validate the child controls of the control passed in as a parameter.
/// </summary>
/// <param name="parent">Type: DependencyObject. The control which is the descendent root control to validate.</param>
/// <returns>Type: bool. The validation result</returns>
public static bool IsValid(DependencyObject parent)
{
// Validate all the bindings on the parent
bool valid = true;
LocalValueEnumerator localValues = parent.GetLocalValueEnumerator();
while (localValues.MoveNext())
{
LocalValueEntry entry = localValues.Current;
if (BindingOperations.IsDataBound(parent, entry.Property))
{
Binding binding = BindingOperations.GetBinding(parent, entry.Property);
foreach (ValidationRule rule in binding.ValidationRules)
{
ValidationResult result = rule.Validate(parent.GetValue(entry.Property), null);
if (!result.IsValid)
{
BindingExpression expression = BindingOperations.GetBindingExpression(parent, entry.Property);
Validation.MarkInvalid(expression, new ValidationError(rule, expression, result.ErrorContent, null));
valid = false;
}
}
}
}
// Validate all the bindings on the children
for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); ++i)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (!IsValid(child))
{
valid = false;
}
}
return valid;
}
}
Then, on the view, I had the following configuration:
<TextBox local:Masking.Mask="^[0-9]*$" IsEnabled="{Binding Path=LocationNumberEnabled}" Grid.Row="1" Grid.Column="3">
<Binding Path="LocationNumber" Mode="TwoWay" UpdateSourceTrigger="LostFocus" NotifyOnValidationError="True" ValidatesOnExceptions="True">
<Binding.ValidationRules>
<localValidation:PositiveNumberRule />
<localValidation:RequiredFieldRule />
</Binding.ValidationRules>
</Binding>
</TextBox>
Worked like a charm! I just called the IsValid method every time I wanted to manually validate e.g. on a button press.
来源:https://stackoverflow.com/questions/9802290/validation-binding-on-first-load