Async Validation In WPF

Source Code

Introduction

Quite often, validation requires web requests, database calls, or some other kind of actions which require a significant amount of time. In this case, UI should be responsible for the validation, but saving/submitting data should be disabled until validations completion.

This article provides a solution for this problem.

Helpers

PropertyHelper is used to get property name.

public class PropertyHelper
  1. public class PropertyHelper  
  2. {  
  3.     public static string GetPropertyName<T>(Expression<Func<T>> propertyLambda)  
  4.     {  
  5.         var me = propertyLambda.Body as MemberExpression;  
  6.   
  7.         if (me == null)  
  8.         {  
  9.             throw new ArgumentException("You must pass a lambda of the form: '() => Class.Property' or '() => object.Property'");  
  10.         }  
  11.   
  12.         return me.Member.Name;  
  13.     }  

Validatable View Model

ValidatableViewModel implements INotifyDataErrorInfo to be able to use ValidatesOnNotifyDataErrors in binding and show errors.

IsValidating property shows that validation is still in progress.

The IsValid property will be set to true only when all properties are valid and no other background validations are taking place.

RegisterValidator registers validation function for property. The function should return an empty list if no errors or list of errors. Property can have only one validator. In case of adding another validator, the previously added validator will be removed.

  1. public abstract class ValidatableViewModel : INotifyDataErrorInfo, INotifyPropertyChanged  
  2. {  
  3.     private bool _isValidating;  
  4.     public bool IsValidating  
  5.     {  
  6.         get { return _isValidating; }  
  7.         protected set { Set(ref _isValidating, value); }  
  8.     }  
  9.   
  10.     private bool _isValid = true;  
  11.     public bool IsValid  
  12.     {  
  13.         get { return _isValid; }  
  14.         protected set { Set(ref _isValid, value); }  
  15.     }  
  16.   
  17.     private readonly Dictionary<string, List<string>> _validationErrors = new Dictionary<string, List<string>>();  
  18.     private readonly Dictionary<string, Guid> _lastValidationProcesses = new Dictionary<string, Guid>();  
  19.     private readonly Dictionary<string, Func<Task<List<string>>>> _validators = new Dictionary<string, Func<Task<List<string>>>>();  
  20.   
  21.     protected ValidatableViewModel()  
  22.     {  
  23.         PropertyChanged += (sender, args) => Validate(args.PropertyName);  
  24.     }  
  25.   
  26.     #region INotifyDataErrorInfo  
  27.     public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;  
  28.     public IEnumerable GetErrors(string propertyName)  
  29.     {  
  30.         if (string.IsNullOrEmpty(propertyName) || !_validationErrors.ContainsKey(propertyName))  
  31.         {  
  32.             return new List<string>();  
  33.         }  
  34.   
  35.         return _validationErrors[propertyName];  
  36.     }  
  37.   
  38.     public bool HasErrors => _validationErrors.Count > 0;  
  39.     #endregion  
  40.   
  41.     #region INotifyPropertyChanged  
  42.     public event PropertyChangedEventHandler PropertyChanged;  
  43.     #endregion  
  44.   
  45.     public List<string> GetErrors()  
  46.     {  
  47.         return _validationErrors.SelectMany(p => p.Value).ToList();  
  48.     }  
  49.   
  50.     protected void Set<T>(ref T storage, T value, [CallerMemberName] string property = null)  
  51.     {  
  52.         if (Equals(storage, value))  
  53.         {  
  54.             return;  
  55.         }  
  56.   
  57.         storage = value;  
  58.         RaisePropertyChanged(property);  
  59.     }  
  60.   
  61.     protected void RaisePropertyChanged(string property)  
  62.     {  
  63.         PropertyChanged?.Invoke(thisnew PropertyChangedEventArgs(property));  
  64.     }  
  65.   
  66.     protected void RegisterValidator<TProperty>(Expression<Func<TProperty>> propertyExpression, Func<Task<List<string>>> validatorFunc)  
  67.     {  
  68.         RegisterValidator(PropertyHelper.GetPropertyName(propertyExpression), validatorFunc);  
  69.     }  
  70.   
  71.     protected void RegisterValidator(string propertyName, Func<Task<List<string>>> validatorFunc)  
  72.     {  
  73.         if (_validators.ContainsKey(propertyName))  
  74.         {  
  75.             _validators.Remove(propertyName);  
  76.         }  
  77.   
  78.         _validators[propertyName] = validatorFunc;  
  79.     }  
  80.   
  81.     protected async Task Validate(string property)  
  82.     {  
  83.         if (string.IsNullOrWhiteSpace(property))  
  84.         {  
  85.             throw new ArgumentException();  
  86.         }  
  87.   
  88.         Func<Task<List<string>>> validator;  
  89.         if (!_validators.TryGetValue(property, out validator))  
  90.         {  
  91.             return;  
  92.         }  
  93.   
  94.         var validationProcessKey = Guid.NewGuid();  
  95.         _lastValidationProcesses[property] = validationProcessKey;  
  96.         IsValidating = true;  
  97.         try  
  98.         {  
  99.             var errors = await validator();  
  100.             if (_lastValidationProcesses.ContainsKey(property) &&   
  101.                 _lastValidationProcesses[property] == validationProcessKey)  
  102.             {  
  103.                 if (errors != null && errors.Any())  
  104.                 {  
  105.                     _validationErrors[property] = errors;  
  106.                 }  
  107.                 else if (_validationErrors.ContainsKey(property))  
  108.                 {  
  109.                     _validationErrors.Remove(property);  
  110.                 }  
  111.             }  
  112.         }  
  113.         catch (Exception ex)  
  114.         {  
  115.             _validationErrors[property] = new List<string>(new[] { ex.Message });  
  116.         }  
  117.         finally  
  118.         {  
  119.             if (_lastValidationProcesses.ContainsKey(property) &&   
  120.                 _lastValidationProcesses[property] == validationProcessKey)  
  121.             {  
  122.                 _lastValidationProcesses.Remove(property);  
  123.             }  
  124.   
  125.             IsValidating = _lastValidationProcesses.Any();  
  126.             IsValid = !_lastValidationProcesses.Any() && !_validationErrors.Any();  
  127.             OnErrorsChanged(property);  
  128.         }  
  129.     }  
  130.   
  131.     protected async Task ValidateAll()  
  132.     {  
  133.         var validators = _validators;  
  134.         foreach (var propertyName in validators.Keys)  
  135.         {  
  136.             await Validate(propertyName);  
  137.         }  
  138.     }  
  139.   
  140.     private void OnErrorsChanged(string propertyName)  
  141.     {  
  142.         ErrorsChanged?.Invoke(thisnew DataErrorsChangedEventArgs(propertyName));  
  143.     }  
  144. }  

Demo

DemoViewModel has 3 fields and imitates long validation process for these fields.

  1. class DemoViewModel : ValidatableViewModel  
  2. {  
  3.     private string _name;  
  4.     public string Name  
  5.     {  
  6.         get  
  7.         {  
  8.             return _name;  
  9.         }  
  10.   
  11.         set  
  12.         {  
  13.             Set(ref _name, value);  
  14.         }  
  15.     }  
  16.   
  17.     private string _description;  
  18.     public string Description  
  19.     {  
  20.         get  
  21.         {  
  22.             return _description;  
  23.         }  
  24.   
  25.         set  
  26.         {  
  27.             Set(ref _description, value);  
  28.         }  
  29.     }  
  30.   
  31.     private int _number;  
  32.     public int Number  
  33.     {  
  34.         get  
  35.         {  
  36.             return _number;  
  37.         }  
  38.   
  39.         set  
  40.         {  
  41.             Set(ref _number, value);  
  42.         }  
  43.     }  
  44.   
  45.     public List<int> AvailableNumbers => new List<int>(new[] { 1, 2, 3, 5, 7, 11 });  
  46.   
  47.     public DemoViewModel(ITaskFactory taskFactory, IProgramDispatcher programDispatcher) : base(taskFactory, programDispatcher)  
  48.     {  
  49.         RegisterValidator(() => Name, ValidateName);  
  50.         RegisterValidator(() => Description, ValidateDescription);  
  51.         RegisterValidator(() => Number, ValidateNumber);  
  52.         ValidateAll();  
  53.     }  
  54.   
  55.     private List<string> ValidateName()  
  56.     {  
  57.         Task.Delay(3000).Wait();  
  58.   
  59.         if (string.IsNullOrWhiteSpace(Name))  
  60.         {  
  61.             return new List<string> { "Name cannot be empty" };  
  62.         }  
  63.   
  64.         if (Name.Length > 10)  
  65.         {  
  66.             return new List<string> { "Name cannot be more than 10 characters" };  
  67.         }  
  68.   
  69.         return new List<string>();  
  70.     }  
  71.   
  72.     private List<string> ValidateDescription()  
  73.     {  
  74.         Task.Delay(4000).Wait();  
  75.   
  76.         if (string.IsNullOrWhiteSpace(Description))  
  77.         {  
  78.             return new List<string> { "Description cannot be empty" };  
  79.         }  
  80.   
  81.         if (Description.Length > 50)  
  82.         {  
  83.             return new List<string> { "Name cannot be more than 50 characters" };  
  84.         }  
  85.   
  86.         return new List<string>();  
  87.     }  
  88.   
  89.     private List<string> ValidateNumber()  
  90.     {  
  91.         Task.Delay(2000).Wait();  
  92.   
  93.         if (Number > 5)  
  94.         {  
  95.             return new List<string> { "Name cannot be more than 5" };  
  96.         }  
  97.   
  98.         return new List<string>();  
  99.     }  
  100. }  

Some styles to show errors.

  1. <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
  2.                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">  
  3.     <ControlTemplate x:Key="GlobalErrorTemplate">  
  4.         <DockPanel>  
  5.             <Border BorderBrush="Red"  
  6.                     BorderThickness="2"  
  7.                     CornerRadius="2">  
  8.                 <AdornedElementPlaceholder />  
  9.             </Border>  
  10.         </DockPanel>  
  11.     </ControlTemplate>  
  12.   
  13.     <Style TargetType="{x:Type TextBox}">  
  14.         <Setter Property="Validation.ErrorTemplate"  
  15.                 Value="{StaticResource GlobalErrorTemplate}" />  
  16.         <Style.Triggers>  
  17.             <Trigger Property="Validation.HasError"  
  18.                      Value="True">  
  19.                 <Setter Property="ToolTip"  
  20.                         Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />  
  21.             </Trigger>  
  22.         </Style.Triggers>  
  23.     </Style>  
  24.   
  25.     <Style TargetType="{x:Type ComboBox}">  
  26.         <Setter Property="Validation.ErrorTemplate"  
  27.                 Value="{StaticResource GlobalErrorTemplate}" />  
  28.         <Style.Triggers>  
  29.             <Trigger Property="Validation.HasError"  
  30.                      Value="True">  
  31.                 <Setter Property="ToolTip"  
  32.                         Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />  
  33.             </Trigger>  
  34.         </Style.Triggers>  
  35.     </Style>  
  36. </ResourceDictionary>  

And View.

  1. <Window x:Class="AsyncValidation.Demo.MainWindow"  
  2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
  3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
  4.         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"  
  5.         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"  
  6.         xmlns:demo="clr-namespace:AsyncValidation.Demo"  
  7.         mc:Ignorable="d"  
  8.         SizeToContent="WidthAndHeight"  
  9.         Title="Async Validation Demo"  
  10.         d:DataContext="{d:DesignInstance IsDesignTimeCreatable=False, d:Type=demo:DemoViewModelViewModel}">  
  11.     <Window.Resources>  
  12.         <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />  
  13.     </Window.Resources>  
  14.     <Grid>  
  15.         <Grid.ColumnDefinitions>  
  16.             <ColumnDefinition Width="120" />  
  17.             <ColumnDefinition Width="200" />  
  18.             <ColumnDefinition Width="*"/>  
  19.         </Grid.ColumnDefinitions>  
  20.         <Grid.RowDefinitions>  
  21.             <RowDefinition Height="Auto"/>  
  22.             <RowDefinition Height="Auto"/>  
  23.             <RowDefinition Height="Auto"/>  
  24.             <RowDefinition Height="*"/>  
  25.             <RowDefinition Height="35"/>  
  26.         </Grid.RowDefinitions>  
  27.         <Label Grid.Row="0"  
  28.                Grid.Column="0"  
  29.                Content="Name" />  
  30.         <TextBox Grid.Row="0"  
  31.                  Grid.Column="1"  
  32.                  Margin="5"  
  33.                  Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=LostFocus, ValidatesOnNotifyDataErrors=True}"/>  
  34.         <Label Grid.Row="1"  
  35.                Grid.Column="0"  
  36.                Content="Description" />  
  37.         <TextBox Grid.Row="1"  
  38.                  Grid.Column="1"  
  39.                  Margin="5"  
  40.                  Text="{Binding Description, Mode=TwoWay, UpdateSourceTrigger=LostFocus, ValidatesOnNotifyDataErrors=True}"/>  
  41.         <Label Grid.Row="2"  
  42.                Grid.Column="0"  
  43.                Content="Number" />  
  44.         <ComboBox Grid.Row="2"  
  45.                   Grid.Column="1"  
  46.                   Margin="5"  
  47.                   ItemsSource="{Binding AvailableNumbers, ValidatesOnNotifyDataErrors=True}"  
  48.                   SelectedValue="{Binding Number, Mode=TwoWay}"/>  
  49.         <StatusBar Grid.Row="4"  
  50.                    Grid.Column="0"  
  51.                    Grid.ColumnSpan="3">  
  52.             <Label Content="Validating"  
  53.                    Visibility="{Binding IsValidating, Converter={StaticResource BooleanToVisibilityConverter}}" />  
  54.             <Label Content="Valid"  
  55.                    Visibility="{Binding IsValid, Converter={StaticResource BooleanToVisibilityConverter}}" />  
  56.         </StatusBar>  
  57.     </Grid>  
  58. </Window> 

Demo looks like this.

 
 
 
 

Tests

NUnit and NSubstitute are used for writing Unit Tests.

  1. [TestFixture]  
  2. public class ValidatableViewModelTests  
  3. {  
  4.     private class ValidatableViewModelStub : ValidatableViewModel  
  5.     {  
  6.         private string _propertyToValidate1;  
  7.   
  8.         public string PropertyToValidate1  
  9.         {  
  10.             get { return _propertyToValidate1; }  
  11.             set { Set(ref _propertyToValidate1, value); }  
  12.         }  
  13.   
  14.         private string _propertyToValidate2;  
  15.   
  16.         public string PropertyToValidate2  
  17.         {  
  18.             get { return _propertyToValidate2; }  
  19.             set { Set(ref _propertyToValidate2, value); }  
  20.         }  
  21.   
  22.   
  23.         public new void RegisterValidator<T>(Expression<Func<T>> propertyExpression,  
  24.             Func<Task<List<string>>> validatorFunc) => base.RegisterValidator(propertyExpression, validatorFunc);  
  25.   
  26.         public new Task ValidateAll() => base.ValidateAll();  
  27.   
  28.         public new Task Validate(string property) => base.Validate(property);  
  29.     }  
  30.   
  31.     private readonly string _prop1Error1 = "Property 1 Error 1";  
  32.     private readonly string _prop2Error1 = "Property 2 Error 1";  
  33.     private readonly string _prop2Error2 = "Property 2 Error 2";  
  34.   
  35.     [Test]  
  36.     public void GetErrorsWhenNoErrors()  
  37.     {  
  38.         var viewModel = CreateTestViewModel(new List<string>(), new List<string>());  
  39.   
  40.         viewModel.PropertyToValidate1 = "Test";  
  41.         viewModel.PropertyToValidate2 = "Test";  
  42.         var errors = viewModel.GetErrors();  
  43.   
  44.         Assert.IsFalse(viewModel.HasErrors);  
  45.         Assert.IsTrue(viewModel.IsValid);  
  46.         Assert.IsFalse(viewModel.IsValidating);  
  47.         Assert.AreEqual(0, errors.Count);  
  48.     }  
  49.   
  50.     [Test]  
  51.     public void GetErrorsWhenOnePropertyHasError()  
  52.     {  
  53.         var viewModel = CreateTestViewModel(new List<string> { _prop1Error1 }, new List<string>());  
  54.   
  55.         viewModel.PropertyToValidate1 = "Test";  
  56.         var errors = viewModel.GetErrors();  
  57.         var prop1Errors = viewModel.GetErrors("PropertyToValidate1").Cast<string>().ToList();  
  58.   
  59.         Assert.IsTrue(viewModel.HasErrors);  
  60.         Assert.IsFalse(viewModel.IsValid);  
  61.         Assert.IsFalse(viewModel.IsValidating);  
  62.         Assert.AreEqual(1, errors.Count);  
  63.         CollectionAssert.Contains(errors, _prop1Error1);  
  64.         Assert.AreEqual(1, prop1Errors.Count);  
  65.         CollectionAssert.Contains(prop1Errors, _prop1Error1);  
  66.     }  
  67.   
  68.     [Test]  
  69.     public void GetErrorsWhenTwoPropertiesHaveErrors()  
  70.     {  
  71.         var viewModel = CreateTestViewModel(  
  72.             new List<string> { _prop1Error1 },  
  73.             new List<string> { _prop2Error1 });  
  74.   
  75.         viewModel.PropertyToValidate1 = _prop1Error1;  
  76.         viewModel.PropertyToValidate2 = _prop2Error1;  
  77.         var errors = viewModel.GetErrors();  
  78.   
  79.         var prop1Errors = viewModel.GetErrors("PropertyToValidate1").Cast<string>().ToList();  
  80.         var prop2Errors = viewModel.GetErrors("PropertyToValidate2").Cast<string>().ToList();  
  81.         Assert.AreEqual(2, errors.Count);  
  82.         Assert.AreEqual(1, prop1Errors.Count);  
  83.         Assert.AreEqual(1, prop2Errors.Count);  
  84.     }  
  85.   
  86.     [Test]  
  87.     public void GetErrorsWhenTwoPropertiesHaveErrorsButOnlyOneWasValidated()  
  88.     {  
  89.         var viewModel = CreateTestViewModel(new List<string> { _prop1Error1 },  
  90.             new List<string> { _prop2Error1, _prop2Error2 });  
  91.   
  92.         viewModel.PropertyToValidate2 = "Test";  
  93.         var errors = viewModel.GetErrors();  
  94.         var prop2Errors = viewModel.GetErrors("PropertyToValidate2").Cast<string>().ToList();  
  95.   
  96.         Assert.IsTrue(viewModel.HasErrors);  
  97.         Assert.IsFalse(viewModel.IsValid);  
  98.         Assert.IsFalse(viewModel.IsValidating);  
  99.         Assert.AreEqual(2, errors.Count);  
  100.         CollectionAssert.Contains(errors, _prop2Error1);  
  101.         CollectionAssert.Contains(errors, _prop2Error2);  
  102.         Assert.AreEqual(2, prop2Errors.Count);  
  103.         CollectionAssert.Contains(prop2Errors, _prop2Error1);  
  104.         CollectionAssert.Contains(prop2Errors, _prop2Error1);  
  105.     }  
  106.   
  107.     [Test]  
  108.     public void GetErrorsWhenValidatorException()  
  109.     {  
  110.         var viewModel = CreateTestViewModel(new List<string> { _prop1Error1 }, new List<string>());  
  111.         var validatorProp1Mock = Substitute.For<Func<Task<List<string>>>>();  
  112.         validatorProp1Mock.Invoke().Returns(Task.FromException<List<string>>(new Exception("Exception Message")));  
  113.         viewModel.RegisterValidator(() => viewModel.PropertyToValidate1, validatorProp1Mock);  
  114.   
  115.         viewModel.PropertyToValidate1 = "Test";  
  116.         var errors = viewModel.GetErrors();  
  117.         var prop1Errors = viewModel.GetErrors("PropertyToValidate1").Cast<string>().ToList();  
  118.   
  119.         Assert.IsTrue(viewModel.HasErrors);  
  120.         Assert.IsFalse(viewModel.IsValid);  
  121.         Assert.IsFalse(viewModel.IsValidating);  
  122.         Assert.AreEqual(1, errors.Count);  
  123.         Assert.AreEqual("Exception Message", errors[0]);  
  124.         Assert.AreEqual(1, prop1Errors.Count);  
  125.         CollectionAssert.DoesNotContain(errors, _prop1Error1);  
  126.         Assert.AreEqual("Exception Message", prop1Errors[0]);  
  127.     }  
  128.   
  129.     [TestCase(null)]  
  130.     [TestCase("")]  
  131.     [TestCase(" ")]  
  132.     [TestCase("UnexistingProperty")]  
  133.     public void GetErrorsWhenWrongPropertyName(string propertyName)  
  134.     {  
  135.         var viewModel = CreateTestViewModel(new List<string> { _prop1Error1 },  
  136.             new List<string> { _prop2Error1, _prop2Error2 });  
  137.   
  138.         var errors = viewModel.GetErrors(propertyName);  
  139.   
  140.         CollectionAssert.IsEmpty(errors);  
  141.     }  
  142.   
  143.     [Test]  
  144.     public void UseLastRegisteredValidator()  
  145.     {  
  146.         var viewModel = CreateTestViewModel(new List<string> { _prop1Error1 }, new List<string>());  
  147.         viewModel.PropertyToValidate1 = "Test";  
  148.         viewModel.RegisterValidator(() => viewModel.PropertyToValidate1, () => Task.FromResult(new List<string>()));  
  149.         viewModel.PropertyToValidate1 = "Test 2";  
  150.   
  151.         var errors = viewModel.GetErrors();  
  152.         var prop1Errors = viewModel.GetErrors("PropertyToValidate1");  
  153.   
  154.         Assert.IsFalse(viewModel.HasErrors);  
  155.         Assert.IsTrue(viewModel.IsValid);  
  156.         Assert.IsFalse(viewModel.IsValidating);  
  157.         CollectionAssert.IsEmpty(errors);  
  158.         CollectionAssert.IsEmpty(prop1Errors);  
  159.     }  
  160.   
  161.     [Test]  
  162.     public void ValidateAll()  
  163.     {  
  164.         var viewModel = CreateTestViewModel(new List<string> { _prop1Error1 },  
  165.             new List<string> { _prop2Error1, _prop2Error2 });  
  166.   
  167.         viewModel.ValidateAll();  
  168.         var errors = viewModel.GetErrors();  
  169.         var prop1Errors = viewModel.GetErrors("PropertyToValidate1").Cast<string>().ToList();  
  170.         var prop2Errors = viewModel.GetErrors("PropertyToValidate2").Cast<string>().ToList();  
  171.   
  172.         Assert.IsTrue(viewModel.HasErrors);  
  173.         Assert.IsFalse(viewModel.IsValid);  
  174.         Assert.IsFalse(viewModel.IsValidating);  
  175.         Assert.AreEqual(3, errors.Count);  
  176.         CollectionAssert.Contains(errors, _prop1Error1);  
  177.         CollectionAssert.Contains(errors, _prop2Error1);  
  178.         CollectionAssert.Contains(errors, _prop2Error2);  
  179.         Assert.AreEqual(1, prop1Errors.Count);  
  180.         CollectionAssert.Contains(prop1Errors, _prop1Error1);  
  181.         Assert.AreEqual(2, prop2Errors.Count);  
  182.         CollectionAssert.Contains(prop2Errors, _prop2Error1);  
  183.         CollectionAssert.Contains(prop2Errors, _prop2Error1);  
  184.     }  
  185.   
  186.     [TestCase(null)]  
  187.     [TestCase("")]  
  188.     [TestCase(" ")]  
  189.     public void ValidateWhenEmptyProperty(string propertyName)  
  190.     {  
  191.         var viewModel = CreateTestViewModel(new List<string> { _prop1Error1 },  
  192.             new List<string> { _prop2Error1, _prop2Error2 });  
  193.   
  194.         Assert.That(() => viewModel.Validate(propertyName), Throws.TypeOf<ArgumentException>());  
  195.     }  
  196.   
  197.     [Test]  
  198.     public void IgnorePreviousValidationResult()  
  199.     {  
  200.         var viewModel = new ValidatableViewModelStub();  
  201.         var isFirstCall = true;  
  202.         var task = Task.Run(async () =>  
  203.         {  
  204.             await Task.Delay(1000);  
  205.   
  206.             return new List<string> { "First Error!!!!" };  
  207.         });  
  208.         viewModel.RegisterValidator(() => viewModel.PropertyToValidate1, () =>  
  209.         {  
  210.             if (isFirstCall)  
  211.             {  
  212.                 isFirstCall = false;  
  213.   
  214.                 return task;  
  215.             }  
  216.   
  217.             return Task.FromResult(new List<string> { "Second Error!!!!" });  
  218.         });  
  219.   
  220.         viewModel.Validate("PropertyToValidate1");  
  221.         viewModel.Validate("PropertyToValidate1");  
  222.         task.Wait();  
  223.         var errors = viewModel.GetErrors();  
  224.   
  225.         Assert.AreEqual("Second Error!!!!", errors[0]);  
  226.     }  
  227.   
  228.     private ValidatableViewModelStub CreateTestViewModel(List<string> property1Errors, List<string> property2Errors)  
  229.     {  
  230.         var viewModel = new ValidatableViewModelStub();  
  231.         viewModel.RegisterValidator(() => viewModel.PropertyToValidate1, () => Task.FromResult(property1Errors));  
  232.         viewModel.RegisterValidator(() => viewModel.PropertyToValidate2, () => Task.FromResult(property2Errors));  
  233.   
  234.         return viewModel;  
  235.     }  
  236. }