Validation order for an entire form of controls
I'm learning this wpf stuff and trying to get my head around validation of controls. Specifically what I'm looking for is this...
A form can have 100 controls on it (exaggerating, but possible). The form's layout and flow are a specific order (via tabbed sequence for user). A user may never get to some "required" fields and click on a "Save" 开发者_运维百科button. How can I trigger it so all the controls force triggering their own respective "Validation" events.
Based on above, does the WPF framework process the validation rules in the tab order the user is looking at. If not, how can that be controlled to match the data entry flow instead of bouncing around in the sequential order the application happens to create objects and their respective validation rules.
Is there a way to have ALL failed controls triggered for the default behavior of putting a red border box around the failed control instead of only one at a time.
Thanks
Typically, to accomplish what you are looking for you use an MVVM type pattern. This means that you bind each control that collects data in your WPF form to a backing field or property. You add validation to the binding, with a style that will cause the red border box. For controls with required data, part of the validation is that they are filled in. You could define a single validation rule for this called "ValidWhenHasData" or some such.
To cause the validations to trigger only when you press "save" or the like, there are a number of ways you can do this. I typically make a property in each validation rule called "IsEnabled" and set it to false by default; if set to false, the validation rule always returns valid. I then add a list in the code-behind of the controls that I want to validate. When "save" is clicked, I go through the list and set all the validation rules' IsEnabled to true, clear all errors on the controls in the list, and then refresh the binding on each. This will display the red rectangles on any that are not filled in or whatever else you have defined as an error condition. You can also use this list to set focus to the first control that failed validation, in the order you choose.
Example validation control template, which includes placeholder for validation error tooltip:
<ControlTemplate x:Key="errorTemplate">
<Canvas Width="{Binding Path=AdornedElement.ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Adorner}}}" Height="{Binding Path=AdornedElement.ActualHeight, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Adorner}}}">
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder/>
</Border>
<Border Canvas.Top="-5" Canvas.Right="-5" BorderBrush="Gray" BorderThickness="1" >
<TextBlock x:Name="errorBlock" TextAlignment="Center" Background="Red" Foreground="White" Width="10" Height="10" FontSize="9" ctl:valTooltip.MessageBody="{Binding Path=AdornedElement.(Validation.Errors)[0].ErrorContent,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Adorner}}}">*</TextBlock>
</Border>
</Canvas>
</ControlTemplate>
Example validation binding:
<TextBox x:Name="TBNumItems" Margin="2,2,2,2" MinWidth="40" HorizontalAlignment="Left" Validation.ErrorTemplate="{StaticResource errorTemplate}">
<TextBox.Text>
<Binding x:Name="NumItemsBinding" Path="NumItems" NotifyOnSourceUpdated="True" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<cal:UIntValidationRule x:Name="NumItemsValidationRule" MinValue="1" MaxValue="99999" IsEnabled="False"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
Example code behind for validation:
/// <summary>
/// Clears all validation errors
/// </summary>
void ClearAllValidationErrors()
{
Validation.ClearInvalid(TBNumItems.GetBindingExpression(TextBox.TextProperty));
}
/// <summary>
/// Revalidates everything
/// </summary>
void RevalidateAll()
{
ClearAllValidationErrors();
TBNumItems.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
Make your data object implement IDataErrorInfo
, which will perform a validation check on a property when the user changes it, then use the following style to apply the red border to controls that have a validation error:
<!-- ValidatingControl Style -->
<Style TargetType="{x:Type FrameworkElement}" x:Key="ValidatingControl">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding
Path=(Validation.Errors)[0].ErrorContent,
RelativeSource={x:Static RelativeSource.Self}}" />
</Trigger>
</Style.Triggers>
</Style>
This will
- Only perform a validation check for a single property when that property gets changed
- Only (and always) show the red validation error border on controls that are bound to an invalid property
Edit
Here's a sample of a how I would implement validation on an object:
public class MyObject : ValidatingObject
{
public MyObject()
{
// Add Properties to Validate here
this.ValidatedProperties.Add("SomeNumber");
}
// Implement validation rules here
public override string GetValidationError(string propertyName)
{
if (ValidatedProperties.IndexOf(propertyName) < 0)
{
return null;
}
string s = null;
switch (propertyName)
{
case "SomeNumber":
if (SomeNumber <= 0)
s = "SomeNumber must be greater than 0";
break;
}
return s;
}
}
And my ValidatingObject
base class which implements IDataErrorInfo
usually contains the following:
#region IDataErrorInfo & Validation Members
/// <summary>
/// List of Property Names that should be validated
/// </summary>
protected List<string> ValidatedProperties = new List<string>();
public abstract string GetValidationError(string propertyName);
string IDataErrorInfo.Error { get { return null; } }
string IDataErrorInfo.this[string propertyName]
{
get { return this.GetValidationError(propertyName); }
}
public bool IsValid
{
get
{
return (GetValidationError() == null);
}
}
public string GetValidationError()
{
string error = null;
if (ValidatedProperties != null)
{
foreach (string s in ValidatedProperties)
{
error = GetValidationError(s);
if (error != null)
{
return error;
}
}
}
return error;
}
#endregion // IDataErrorInfo & Validation Members
I faced the same problem. I wanted controls who know if they are required and report automatically any change to the hosting Window. I didn't want to have to write complicated XAML or other code, just placing the control and setting a property to indicate if user input is required. The control searches then automatically the host window and informs it when the user keys in required data or deletes it, so the window can change the state of the Save button. The final solution looks like this:
<wwl:CheckedWindow x:Class="Samples.SampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wwl="clr-namespace:WpfWindowsLib;assembly=WpfWindowsLib">
<StackPanel>
<wwl:CheckedTextBox x:Name="TestCheckedTextBox" MinWidth="100" MaxLength="20" IsRequired="True"/>
<Button x:Name="SaveButton" Content="_Save"/>
</StackPanel>
</wwl:CheckedWindow>
For this to work added to the most common WPF controls an IChecker, which does the following:
- know when the data has changed
- know when the data has been unchanged (user undid his change)
- know when a "required" control is lacking data
- know when a "required" control has data
- find automatically the window the control is in and inform the Window about each state change
If the window gets informed by any control that the control's state has changed, the window then queries automatically all other controls about their state. If all required controls have data, the Save Button gets enabled.
Knowing that the user has changed some data it is useful, when the user tries to close the window without saving . He gets then automatically a warning and gives him the choice if he wants to save or discard the data.
I had to write too much code to be posted here, but now life is very easy. Just place the control into the XAML and add few lines of code to the Window for the Save Button, that's all. Here is an detailed explanation: https://www.codeproject.com/Articles/5257393/Base-WPF-Window-functionality-for-data-entry The code is on GitHub: https://github.com/PeterHuberSg/WpfWindowsLib
精彩评论