I was looking for the same and haven't found a solution, so i built something myself. I wanted to delay the validation but not delay setting the property. So i made it with timers and the INotifyDataErrorInfo which allows async checks and the notification over events.
Further I improved it that on typing the validation errors are cleared immediately and only a second after typing the errors are shown again.
public abstract class NotifyDataErrorInfoViewModelBase : ViewModelBase, INotifyDataErrorInfo
{
private ConcurrentDictionary<string, List<ValidationResult>> modelErrors = new ConcurrentDictionary<string, List<ValidationResult>>();
private ConcurrentDictionary<string, Timer> modelTimers = new ConcurrentDictionary<string, Timer>();
public bool HasErrors { get => modelErrors.Any(); }
public IEnumerable GetErrors(string propertyName)
{
modelErrors.TryGetValue(propertyName, out var propertyErrors);
return propertyErrors;
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
protected NotifyDataErrorInfoViewModelBase() : base()
{ PropertyChanged += (s, e) => Validate(e.PropertyName); }
private void NotifyErrorsChanged([CallerMemberName] string propertyName = "")
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
private void Validate([CallerMemberName] string propertyName = "")
{
var timer = modelTimers.AddOrUpdate(propertyName, new Timer(), (key, existingTimer) => { existingTimer.Stop(); return new Timer(); });
timer.Interval = 1000;
timer.AutoReset = false;
modelErrors.TryRemove(propertyName, out var existingErrors); // clear existing errors immediately
if (existingErrors?.Count > 0)
NotifyErrorsChanged(propertyName);
timer.Elapsed += (s, e) => CheckForErrors(propertyName, existingErrors);
timer.Start();
}
private async void CheckForErrors(string propertyName)
{
await Task.Factory.StartNew(() =>
{
var errorMessage = "";
try
{
errorMessage = GetValidationMessage(propertyName);
}
catch (Exception ex) { errorMessage = "strValidationError"; }
if (string.IsNullOrEmpty(errorMessage))
{
if (existingErrors?.Count > 0)
NotifyErrorsChanged(propertyName);
}
else
{
modelErrors[propertyName] = new List<ValidationResult> { new ValidationResult(errorMessage) };
NotifyErrorsChanged(propertyName);
}
});
}
private string GetValidationMessage(string propertyName)
{
var property = GetType().GetProperty(propertyName).GetValue(this);
var validationContext = new ValidationContext(this) { MemberName = propertyName };
var validationResults = new List<ValidationResult>();
if (!Validator.TryValidateProperty(property, validationContext, validationResults) && validationResults.Count > 0)
{
var messages = new List<string>();
foreach (var validationResult in validationResults)
{
messages.Add(validationResult.ErrorMessage);
}
var message = string.Join(Environment.NewLine + "\u25c9 ", messages);
if (messages.Count > 1)
message = "\u25c9 " + message; // add bullet point
return message;
}
return null;
}
}
I do use it with GalaSoft.MvvmLight, but I'm sure you could use something else (or don't use ViewModelBase at all).
The function Validate("variableName") starts the validation (here 1 second delay), in my case I have attached it to the event PropertyChanged, but you could also call Validate() in the setter of the properties instead if you want.
I use it combined with this to show the Validation in the WPF UI: https://stackoverflow.com/a/20394432/9758687
Edit:
Alternatively the WPF part could be delayed also using Animations without using the timers above. That has the advantage that the validation is done immediately and that is useful for example to disable buttons if the validation isn't successful. Here the code which I use in my ErrorTemplate:
<Style.Triggers>
<Trigger Property="IsVisible" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation BeginTime="0:0:0.8" Duration="0:0:0.5" To="1.0" Storyboard.TargetProperty="Opacity" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>