![]() |
Platforms, Frameworks & Libraries »
Windows Presentation Foundation »
General
Intermediate
License: The Code Project Open License (CPOL)
Total View ValidationBy Sacha BarberCross business object validation a more centric view |
C# (C#3.0), .NET (.NET3.0, .NET3.5), WPF, CEO, Architect, Dev
|
||||||||||
|
Advanced Search Add to IE Search |
|
|
||||||||||||||||||
WPF is a fairly new technology (ok its been around a while) so its still growing and new techniques and idea are developing all the time. As developers we are expected to provide users with usable systems. As part of a usable system we must provide input validation of some sort. Now WPF does come with a standard mechanism for this, but as you shall see its not perfect, and that it is limited in certain areas.
What this article will attempt to outline is 1 possible solution to the inbuilt WPF frameworks shortcomings. It does assume you know a little bit about WPF and validation in general, but it should cover most ideas in enough detail that WPF newbies should be ok.
As I stated this article will attempt to show you an alternative validation concept that fills the gaps with the standard WPF validation mechanism.
But what's so wrong with the standard WPF validation mechanism. Well typically the inbuilt WPF validation mechanism provides means of validating a single object. So it provides bounds checking for that object if you will. So how does this work, well although that's now the main push for this article, it is worth a quick mention...so here goes.
The Standard WPF Validation Works Likes This :
In the .NET 3.0 days you would have typically done something like this:
<Binding Source="{StaticResource data}" Path="Age"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule/>
</Binding.ValidationRules>
</Binding>
Where you add in ValidationRules to your binding.
Which was ok, but if you wanted a specific type of Validation you had to either
use the standard ValidationRule or create your own, which may look
like this.
class FutureDateRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
DateTime date;
try
{
date = DateTime.Parse(value.ToString());
}
catch (FormatException)
{
return new ValidationResult(false, "Value is not a valid date.");
}
if (DateTime.Now.Date > date)
{
return new ValidationResult(false, "Please enter a date in the future.");
}
else
{
return ValidationResult.ValidResult;
}
}
}
Ouch, painful.
Luckily in .NET 3.5 things changed for the better. We were then able to use
the IDataErrorInfo interface directly on our business objects,
which works like this:
public class Person : IDataErrorInfo
{
private int age;
public int Age
{
get { return age; }
set { age = value; }
}
public string Error
{
get
{
return null;
}
}
public string this[string name]
{
get
{
string result = null;
if (name == "Age")
{
if (this.age < 0 || this.age > 150)
{
result =
"Age must not be less than 0 or greater than 150.";
}
}
return result;
}
}
}
And the XAML (binding code really) would look something like this:
<Binding Source="{StaticResource data}" Path="Age"
UpdateSourceTrigger="PropertyChanged"
ValidatesOnDataErrors="True" />
This is also a tad painful, as we have to write loads of rules in the business objects. I have seen several people try to ease the pain of this mechanism using various techniques such as
You can read more about the standard WPF validation mechanisms using these links:
The big problems that I personally have with all these standard methods are:
However, there is no denying that item 1 above just can not be solved by having each business object hold its own validation rules. I mean how would business object one validate if it needed to do so with some knowledge of another object. Even using some of the new WPFism design patterns such as MVVM we may end up with one ViewModel holding a number of business objects that drive a single View. There will be a high probability that there may actually be some cross business object validation required in these cases.
This led me to have a think about it a bit, and it led me to actually ditch
what I had thought of as a good thing, so I ditched the use of the IDataErrorInfo
interface all together. But why did I do that?
Well I personally think that validation logic doesn't actually belong in the objects themselves. So I decided to move those rules out of the business objects. So where could I do my validating, I have to do it somewhere right. Right indeed, so what I decided to do was stick to using a couple of WPF goodies such as:
And then I decided to do the following.
That is pretty much it. So how does this look in code.
Well lets see an example app (this is the demo app attached, but you can change it to suit your actual needs) :

Well quite simply it works as follows. The actual business objects (Person
classes) simply have properties that notify bindings of changes via the INotifyPropertyChanged
interface mechanism. There is a single ViewModel (Window1ViewModel)
object per window (you may decided to have a number of view models to govern
your page, that is up to you), which holds a number of business objects (Person
classes) that the view (Window1) needs to bind to. So basically
the View will bind directly to the ViewModel (this is the MVVM pattern). The
ViewModel also holds a validator (Window1Validator) which is responsible
for running all business logic for a given ViewModel. As such the validator
(Window1Validator) needs to know about the ViewModel (Window1ViewModel),
so that it can examine the ViewModels (Window1ViewModel) business
objects (Person classes) values, that are currently driving the
view (Window1).
So what happens next, is that the view (Window1) will at some
stage have to validate its contents. For the demo that moment occurs when a
vlidate button is clicked which will call a Validate() method inside
the associated ViewModel (Window1ViewModel). When the ViewModel
is asked to validate, it simply passed the call on to its internal validator
(Window1Validator). As the internal ViewModel held validator knows
about the ViewModel (Window1ViewModel), it is just a case of running
through what ever business validation logic you want.
When you do run through the business validation logic, a ViewModel (Window1ViewModel)
held ObservableCollection<ValidationFailure> is added to,
where the Key property of the ValidationFailure key will be some
unique name. This could typically be the name of the property you are validating,
such as "Age". The beaty of this approach is that you have access
to ALL the objects that drive the view, which allows cross object validation.
This is something that the standard WPF validation framework just doesn't cater
for (to the best of my knowledge).
So once we rattle through all the validation logic we end up with a bunch of
broken rules in the form of a ObservableCollection<ValidationFailure>
within the ViewModel (Window1ViewModel), that can be used to bind
to. As each ValidationFailure key will be some unique name, we
can strip out only the ones that match a particular field on the view, which
is done by using a ValueConverter, or choose to view all of the ObservableCollection<ValidationFailure>
for the entire view.
Roughly that is how it all works, so time for some code.
So I guess it is time to start examining the code. Well lets start with the business objects themselves. The demo uses the following business object.
/// <summary>
/// A simple Person domain object
/// </summary>
public class Person : DomainObject
{
#region Data
private String firstName = String.Empty;
private String lastName = String.Empty;
private Boolean isAbleToVote = false;
private Int32 age = 0;
#endregion
#region Public Properties
public String FirstName
{
get { return firstName; }
set
{
firstName = value;
NotifyChanged("FirstName");
}
}
public String LastName
{
get { return lastName; }
set
{
lastName = value;
NotifyChanged("LastName");
}
}
public Boolean IsAbleToVote
{
get { return isAbleToVote; }
set
{
isAbleToVote = value;
NotifyChanged("IsAbleToVote");
}
}
public Int32 Age
{
get { return age; }
set
{
age = value;
NotifyChanged("Age");
}
}
#endregion
}
Where this class inherits from a base class called DomainObject which looks like this
/// <summary>
/// The class all domain objects must inherit from.
///
/// INotifyPropertyChanged : Provides change notification to allow WPF bindings to work, without the
/// need to inherit from a WPF specifio class. So this will work even with WinForms/ASP .NET
/// </summary>
[Serializable()]
public abstract class DomainObject : INotifyPropertyChanged
{
#region Data
protected int id;
#endregion
#region Ctor
/// <summary>
/// Constructor.
/// </summary>
public DomainObject()
{
}
#endregion
#region Public Properties
/// <summary>
/// Gets or sets the Address primary key value.
/// </summary>
public int ID
{
get { return id; }
set
{
id = value;
NotifyChanged("ID");
}
}
#endregion
#region INotifyPropertyChanged Implementation
/// <summary>
/// Occurs when any properties are changed on this object.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// A helper method that raises the PropertyChanged event for a property.
/// </summary>
/// <param name="propertyNames">The names of the properties that changed.</param>
protected virtual void NotifyChanged(params string[] propertyNames)
{
foreach (string name in propertyNames)
{
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="e">Event arguments.</param>
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, e);
}
}
#endregion
}
As you can see the DomainObject class simply provides the INotifyPropertyChanged
interface implementation. Next let us examine the ViewModel code which looks
like this for the attached demo code.
/// <summary>
/// View model for Window1
/// </summary>
public class Window1ViewModel : ViewModelBase
{
#region Data
private Window1Validator window1Validator = null;
private Person currentPerson1 = new Person();
private Person currentPerson2 = new Person();
private ObservableCollection<ValidationFailure>
validationErrors = new ObservableCollection<ValidationFailure>();
private ICommand validateCommand;
#endregion
#region Ctor
public Window1ViewModel()
{
//initialise validator for this view model
window1Validator = new Window1Validator(this);
//wire up command
validateCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => Validate()
};
}
#endregion
#region Public Properties
/// <summary>
/// Return an ICommand that can execute the search
/// </summary>
public ICommand ValidateCommand
{
get { return validateCommand; }
}
public ObservableCollection<ValidationFailure> ValidationErrors
{
get { return validationErrors; }
set
{
validationErrors = value;
NotifyChanged("ValidationErrors");
}
}
public Person CurrentPerson1
{
get { return currentPerson1; }
set
{
currentPerson1 = value;
NotifyChanged("CurrentPerson1");
}
}
public Person CurrentPerson2
{
get { return currentPerson2; }
set
{
currentPerson2 = value;
NotifyChanged("CurrentPerson2");
}
}
#endregion
#region Public Methods
public void Validate()
{
window1Validator.Validate();
}
#endregion
}
Again this class inherits from a base class that offers several useful things
such as INotifyPropertyChanged interface implementation. It can
also be seen that this example ViewModel holds several business objects that
will be used by the View to bind to. This ViewModel is then used to bind against
in the View, where the DataContext of the View is set to be this ViewModel.
Let us have a look at the View next.
/// <summary>
/// Demonstrates an alternative way fo validation across all objects within a
/// View, rather than using IDataErrorInfo which must be done on your actual
/// DomainObject and can only validate internal properties.
/// </summary>
public partial class Window1 : Window
{
private Window1ViewModel window1ViewModel = new Window1ViewModel();
public Window1()
{
InitializeComponent();
this.DataContext = window1ViewModel;
}
}
As you can see the View is using the Window1ViewModel ViewModel
as its DataContext which allows controls on the View to bind to the ViewModel
directly. I am not going to bore you with all the XAML for the View, but will
just show you a typical binding for one of the ViewModel held business objects.
<!-- FirstName Property-->
<StackPanel Orientation="Horizontal" Margin="5">
<Label Content="FirstName" Width="100" />
<local:TextBoxEx x:Name="txtFirstName1" Width="150" Height="25" FontSize="12" FontWeight="Bold"
ValidationErrors="{Binding Path=ValidationErrors, Mode=OneWay,
Converter={StaticResource ValidationErrorsLookupConv},
ConverterParameter='FirstName1'}"
Text="{Binding Path=CurrentPerson1.FirstName, UpdateSourceTrigger=PropertyChanged}"
Foreground="Black" HorizontalAlignment="Center"
HorizontalContentAlignment="Center" />
</StackPanel>
So you can see that we have a specialised TextBox (TextBoxEx)
which is being bound to the ValidationErrors property of the current
DataContext of the View (which is really the Window1ViewModel ViewModel).
It can also be seen that we are using a ValueConverter (ValidationErrorsLookupConverter)
where we feed a parameter value into the ValueConverter.
To understand that mechanism we need to first understand how the ValidationErrors
property works within the Window1ViewModel ViewModel, so let us
examine that now.
Basically what happens is when a new ViewModel (Window1ViewModel)
is constructed it creates a new validator object (Window1Validator)
which is used to perform all validation on the ViewModel (Window1ViewModel).
The validator object (Window1Validator) simply runs through a chunk
of really boring rules code, and append a new ValidationFailure object to the
list of ObservableCollection<ValidationFailure> held within
the ViewModel (Window1ViewModel).
Let us examine the code for the validator object (Window1Validator),
it is pretty dull stuff, but that is the nature of validation code.
/// <summary>
/// Validation code for Window1
/// </summary>
public class Window1Validator
{
#region Data
private Window1ViewModel window1ViewModel { get; set; }
#endregion
#region Ctor
public Window1Validator(Window1ViewModel window1ViewModel)
{
this.window1ViewModel = window1ViewModel;
}
#endregion
#region Public Methods
public void Validate()
{
ObservableCollection<ValidationFailure> localValidationErrors=
new ObservableCollection<ValidationFailure>();
#region Validate CurrentPerson1
if (window1ViewModel.CurrentPerson1.Age < 0)
localValidationErrors.Add(
new ValidationFailure("Age1",
"Person 1 Age cant be < 0"));
if (window1ViewModel.CurrentPerson1.Age > 65
&& window1ViewModel.CurrentPerson1.IsAbleToVote)
localValidationErrors.Add(
new ValidationFailure("Age1",
"Person 1 Age, You can't vote > 65"));
if (window1ViewModel.CurrentPerson1.FirstName == String.Empty)
localValidationErrors.Add(
new ValidationFailure("FirstName1",
"Person 1 FirstName can't be empty"));
if (window1ViewModel.CurrentPerson1.LastName == String.Empty)
localValidationErrors.Add(
new ValidationFailure("LastName1",
"Person 1 LastName can't be empty"));
#endregion
#region Validate CurrentPerson2
if (window1ViewModel.CurrentPerson1.Age < 18
&& window1ViewModel.CurrentPerson2.Age == 0)
localValidationErrors.Add(
new ValidationFailure(
"Age2",
"Person 2 Age cant be < 0 if Person1 Age < 18"));
if (window1ViewModel.CurrentPerson2.Age > 65
&& window1ViewModel.CurrentPerson2.IsAbleToVote)
localValidationErrors.Add(
new ValidationFailure("Age2",
"Person 2, You can't vote > 65"));
if (window1ViewModel.CurrentPerson2.FirstName == String.Empty)
localValidationErrors.Add(
new ValidationFailure("FirstName2",
"Person 2 FirstName can't be empty"));
if (window1ViewModel.CurrentPerson2.LastName == String.Empty)
localValidationErrors.Add(
new ValidationFailure("LastName2",
"Person 2 LastName can't be empty"));
#endregion
window1ViewModel.ValidationErrors = localValidationErrors;
}
#endregion
}
The important part to note here is that the ViewModel (Window1ViewModel)
holds a complete list of all the ValidationFailures that occurred,
and that each ValidationFailure has a key that can be used to grab
only those ValidationFailures that are related to a specific TextBox.
Which is exactly what happens within the ValueConverter (ValidationErrorsLookupConverter)
where we fed a parameter value into the ValueConverter, which is used to obtain
only those ValidationFailures that pertain to the TextBox that
has the correct key (value converter parameter value), which will be used to
filter the list of all the ValidationFailures to only those that
match the key (value converter parameter value).
Recall this binding for a single TextBoxEx within the XAML:
am not going to bore you with all the XAML for the View, but will just show you a typical binding for one of the ViewModel held business objects.
<!-- FirstName Property-->
<StackPanel Orientation="Horizontal" Margin="5">
<Label Content="FirstName" Width="100" />
<local:TextBoxEx x:Name="txtFirstName1" Width="150" Height="25" FontSize="12" FontWeight="Bold"
ValidationErrors="{Binding Path=ValidationErrors, Mode=OneWay,
Converter={StaticResource ValidationErrorsLookupConv},
ConverterParameter='FirstName1'}"
Text="{Binding Path=CurrentPerson1.FirstName, UpdateSourceTrigger=PropertyChanged}"
Foreground="Black" HorizontalAlignment="Center"
HorizontalContentAlignment="Center" />
</StackPanel>
See the ValidationErrors part, and the ConverterParameter='FirstName1', that is the part that enables us to grab only those validation errors that we need for the current TextBox.
This may become clearer when you see the ValueConverter (ValidationErrorsLookupConverter).
/// <summary> /// Obtains a sub set of all validation errors from the /// Bound object (ViewModel) that matches a particular key /// for the actual bound control /// </summary> [ValueConversion(typeof(ObservableCollection<ValidationFailure>), typeof(ObservableCollection<ValidationFailure>))] public class ValidationErrorsLookupConverter : IValueConverter { #region IValueConverter implementation public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value != null) { ObservableCollection<ValidationFailure> validationLookup = (ObservableCollection<ValidationFailure>)value; List<ValidationFailure> failuresForKey = ( from vf in validationLookup where vf.Key.Equals(parameter.ToString()) select vf ).ToList(); return new ObservableCollection<ValidationFailure>(failuresForKey); } return null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException("Can't convert back"); } #endregion }
The last step was in having a specialised TextBox (TextBoxEx)
which knew how to show its own list of validation errors in a popup. Now this
is just what I chose to do, you may think up something different, but that is
what I chose to do.
I should also point out that generally I am against creating specialised controls that inherit from the System.Windows.Controls, as most extra behavior can be added via attached properties, but this just didn't seem to fit in this case, as I wanted the popup within the XAML to be triggered by mouse moves etc etc.
Josh Smith will more than likely say, oh you just do this, then that, and there you have it. I will just smile back at the man and go "That's nice to know, thanks Josh".
Since writing this article a reader "SE_GEEK" has proposed a new control which inherits from ContentControl instead of TextBox, you can read more about that approach using the forum link GlobalWPFValidation.aspx?msg=2895494#xx2895494xx
Anyway, here is the code for the specialised TextBox (TextBoxEx)
which knows how to show its own list of ValidationFailures within
a popup. I have only done a specialised TextBox (TextBoxEx) for
the attached demo code, but you could apply the same idea to any of the standard
controls. At work we have actually done that with CheckBox/ComboBox and other
controls and it works very well.
/// <summary>
/// A Simple extentended TextBox that supports our
/// custom validation collection & logic
/// </summary>
[TemplatePart(Name = "PART_PopupErrors", Type = typeof(Popup))]
[TemplatePart(Name = "PART_Close", Type = typeof(Button))]
public class TextBoxEx : TextBox
{
#region Data
private Popup errorPopup = null;
private Button cmdClose = null;
#endregion
#region Ctor
public TextBoxEx() : base()
{
this.MouseEnter += (s, e) =>
{
if (errorPopup != null && !IsValid)
{
errorPopup.IsOpen = false;
errorPopup.IsOpen = true;
}
};
}
#endregion
#region Private Methods
private void ErrorPopup_MouseUp(object sender,
System.Windows.Input.MouseButtonEventArgs e)
{
if (errorPopup.IsOpen)
errorPopup.IsOpen = false;
}
private void CmdClose_Click(object sender, RoutedEventArgs e)
{
Button button = e.OriginalSource as Button;
Popup pop = button.Tag as Popup;
if (pop != null)
pop.IsOpen = false;
}
#endregion
#region DPs
#region IsValid
/// <summary>
/// IsValid Dependency Property
/// </summary>
public static readonly DependencyProperty IsValidProperty =
DependencyProperty.Register("IsValid", typeof(bool), typeof(TextBoxEx),
new FrameworkPropertyMetadata((bool)true));
/// <summary>
/// Gets or sets the IsValid property. This dependency property
/// indicates ....
/// </summary>
public bool IsValid
{
get { return (bool)GetValue(IsValidProperty); }
set { SetValue(IsValidProperty, value); }
}
#endregion
#region NewStyle
/// <summary>
/// NewStyle Dependency Property
/// </summary>
public static readonly DependencyProperty NewStyleProperty =
DependencyProperty.Register("NewStyle", typeof(Style), typeof(TextBoxEx),
new FrameworkPropertyMetadata((Style)null,
new PropertyChangedCallback(OnNewStyleChanged)));
/// <summary>
/// Gets or sets the NewStyle property.
/// </summary>
public Style NewStyle
{
get { return (Style)GetValue(NewStyleProperty); }
set { SetValue(NewStyleProperty, value); }
}
/// <summary>
/// Handles changes to the NewStyle property.
/// </summary>
private static void OnNewStyleChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
((TextBoxEx)d).Style = e.NewValue as Style;
}
#endregion
#region ValidationErrors
/// <summary>
/// ValidationErrors Dependency Property
/// </summary>
public static readonly DependencyProperty ValidationErrorsProperty =
DependencyProperty.Register("ValidationErrors",
typeof(ObservableCollection<ValidationFailure>),
typeof(TextBoxEx),
new FrameworkPropertyMetadata(
(ObservableCollection<ValidationFailure>)null,
new PropertyChangedCallback(OnValidationErrorsChanged)));
/// <summary>
/// Gets or sets the ValidationErrors property.
/// </summary>
public ObservableCollection<ValidationFailure> ValidationErrors
{
get { return (ObservableCollection<ValidationFailure>)
GetValue(ValidationErrorsProperty); }
set { SetValue(ValidationErrorsProperty, value); }
}
/// <summary>
/// Handles changes to the ValidationErrors property.
/// </summary>
private static void OnValidationErrorsChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
ObservableCollection<ValidationFailure> failures =
e.NewValue as ObservableCollection<ValidationFailure>
TextBoxEx thisObj = (TextBoxEx)d;
if (failures == null)
thisObj.IsValid = true;
else thisObj.IsValid = failures.Count == 0 ? true : false;
}
#endregion
#endregion
#region Overrides
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
errorPopup = this.Template.FindName("PART_PopupErrors", this) as Popup;
if (errorPopup != null)
errorPopup.MouseUp += new MouseButtonEventHandler(ErrorPopup_MouseUp);
cmdClose = this.Template.FindName("PART_Close", this) as Button;
if (cmdClose != null)
cmdClose.Click += new RoutedEventHandler(CmdClose_Click);
}
#endregion
}
And here is the XAML that provides an actual Style for the TextBoxEx control.
<Style TargetType="{x:Type local:TextBoxEx}">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TextBoxEx}">
<Grid>
<!-- POPUP-->
<Popup x:Name="PART_PopupErrors"
PlacementTarget="{Binding ElementName=Bd}"
Placement="Relative"
AllowsTransparency="True"
PopupAnimation="Slide"
HorizontalOffset ="20"
StaysOpen="False"
VerticalOffset="20">
<Border HorizontalAlignment="Stretch"
BorderBrush="WhiteSmoke"
Background="Red"
Margin="0"
Width="250"
VerticalAlignment="Stretch"
Height="120"
Opacity="0.97"
BorderThickness="2"
CornerRadius="3" >
<Border HorizontalAlignment="Stretch"
BorderBrush="Red"
Background="White"
Margin="0"
VerticalAlignment="Stretch"
Opacity="1"
BorderThickness="5"
CornerRadius="0" >
......
......
......
......
<ScrollViewer Grid.Row="1" Margin="0"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsControl Margin="0" BorderThickness="0"
ItemsSource="{Binding
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type local:TextBoxEx}},
Path=ValidationErrors}"/>
</ScrollViewer>
</Grid>
</Border>
</Border>
</Popup>
<!-- Control-->
<Border SnapsToDevicePixels="true" x:Name="Bd"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
VerticalAlignment="Center"
Background="{TemplateBinding Background}"
CornerRadius="2"
BorderBrush="Black"
BorderThickness="2">
<ScrollViewer x:Name="PART_ContentHost"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
TextElement.FontSize="{TemplateBinding FontSize}"
VerticalAlignment="Center"
VerticalContentAlignment="Center" Margin="2,0,0,0"/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsValid" Value="False">
<Setter Property="BorderBrush"
TargetName="Bd" Value="Red"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And that is pretty much it. With this mechanism, we are able to pretty much perform any cross business object validation we like for the current View. And to prove that here are a couple of screen shots.
Here is a Single pop for a single TextBoxEx object.

And here is what I get for all errors for the current View

That is all I have to say, I hope this article has helped you a little, if you liked it could you please be kind enough to leave a vote or a message. Thanks
Since publishing this article a collegue (Colin Eberhardt) has informed me that as of .NET 3.5 SP1, this is actually possibly using the standard WPF mechanisms of BindingGroups. As .NET 3.5 SP1 was such a monster I am not that surprised I missed this. Anywayt Colin has written an excellent post on this, and you can read all about it over at his blog. Here is a link
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 23 Jan 2009 Editor: |
Copyright 2009 by Sacha Barber Everything else Copyright © CodeProject, 1999-2010 Web20 | Advertise on the Code Project |