In this specific WPF application which follows the MVVM pattern, the view model implements the IDataErrorInfo interface to notify the view of invalid data in text fields.
A text box exists in the view where you can enter a volume. This has been specified with property changed update source, and validate data errors:
<TextBox
Text="{Binding Volume, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
The problem with this is that you get a validation error before the user has finished typing. E.g., a valid value is "25 ml". But before the user has typed in the last "l", then "25 m" is present in the text box. This is not a valid value and will result in the IDataError implementation saying so.
The result is, as the user is typing, a red marker flashes around the text box.
We would like to have a little delay (0.5 sec) before the red marker appears around the text box, so we can assume that the user has finished typing before showing validation errors.
The first attempt was to create a specialized text box that waits .5 sec before updating the property in the view model. But that is no good, because in the case that the user does enter a valid value, then .5 second passes before the submit button gets enabled.
I have an idea that you could write a specialized binding (i.e. create a specialized class derived from System.Windows.Data.Binding) that implements this behavior, but I have no idea how to do this.
Is that a plausible way, or is there a better?
Sounds like you could use a custom DelayBinding that Paul Stovell blogged about. I've used it with great success implementing delayed search/filtering. You can read about it here:
http://www.paulstovell.com/wpf-delaybinding
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>