Last active
May 30, 2020 22:28
-
-
Save canton7/6727693 to your computer and use it in GitHub Desktop.
Caliburn Micro Validations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Caliburn.Micro; | |
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace ValidationTest | |
{ | |
public class ValidatingScreen : Screen, IDataErrorInfo, IValidationProvider | |
{ | |
private IValidator validator; | |
public ValidatingScreen() | |
{ | |
this.validator = new Validator(); | |
} | |
public string Error | |
{ | |
get { return String.Join(Environment.NewLine, this.validator.Errors); } | |
} | |
public string this[string columnName] | |
{ | |
get | |
{ | |
if (this.validator.IsPropertyChecked(columnName)) | |
{ | |
var errors = String.Join(Environment.NewLine, this.validator.CheckProperty(columnName)); | |
this.NotifyOfPropertyChange(() => this.Error); | |
return errors; | |
} | |
else | |
{ | |
return ""; | |
} | |
} | |
} | |
public IValidation ValidateWith<TProperty>(System.Linq.Expressions.Expression<Func<TProperty>> property, Func<TProperty, bool> validator, string message) | |
{ | |
return this.validator.ValidateWith(property, validator, message); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
using System.Windows; | |
using System.Windows.Data; | |
using System.Windows.Markup; | |
namespace ValidationTest | |
{ | |
public class ValidationBindingExtension : MarkupExtension | |
{ | |
[ConstructorArgument("Path")] | |
public PropertyPath Path { get; set; } | |
public bool ReplaceInvalidValues { get; set; } | |
public object ReplaceInvalidValuesWith { get; set; } | |
public IValueConverter Converter { get; set; } | |
public string StringFormat { get; set; } | |
public UpdateSourceTrigger UpdateSourceTrigger { get; set;} | |
public ValidationBindingExtension(string path) | |
{ | |
this.Path = new PropertyPath(path); | |
this.ReplaceInvalidValues = true; | |
this.ReplaceInvalidValuesWith = null; | |
this.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged; | |
} | |
public override object ProvideValue(IServiceProvider serviceProvider) | |
{ | |
var binding = new Binding(this.Path.Path); | |
binding.ValidatesOnDataErrors = true; | |
binding.ValidatesOnExceptions = true; | |
binding.UpdateSourceTrigger = this.UpdateSourceTrigger; | |
binding.StringFormat = this.StringFormat; | |
if (this.ReplaceInvalidValues) | |
{ | |
var converter = new ReplaceInvalidValuesConverter(); | |
converter.ReplaceInvalidValuesWith = this.ReplaceInvalidValues; | |
if (this.Converter == null) | |
binding.Converter = converter; | |
else | |
binding.Converter = new ValueConverterGroup() { this.Converter, converter }; | |
} | |
else | |
{ | |
binding.Converter = this.Converter; | |
} | |
return binding.ProvideValue(serviceProvider); | |
} | |
} | |
public class ReplaceInvalidValuesConverter : DependencyObject, IValueConverter | |
{ | |
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) | |
{ | |
return value; | |
} | |
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) | |
{ | |
var converter = TypeDescriptor.GetConverter(targetType); | |
if (!converter.IsValid(value)) | |
return null; | |
var convertedValue = converter.ConvertFrom(value); | |
return convertedValue; | |
} | |
public object ReplaceInvalidValuesWith | |
{ | |
get { return (object)GetValue(ReplaceInvalidValuesWithProperty); } | |
set { SetValue(ReplaceInvalidValuesWithProperty, value); } | |
} | |
public static readonly DependencyProperty ReplaceInvalidValuesWithProperty = | |
DependencyProperty.Register("ReplaceInvalidValuesWith", typeof(object), typeof(ReplaceInvalidValuesConverter), new PropertyMetadata(null)); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Caliburn.Micro; | |
using System; | |
using System.Collections.Concurrent; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace ValidationTest | |
{ | |
/// <summary> | |
/// Validating member. It's recommended that you implement IValidationProvider yourself, which wraps this | |
/// </summary> | |
public class Validator : PropertyChangedBase, IValidator | |
{ | |
private ConcurrentDictionary<string, PropertyValidator> propertyValidators = new ConcurrentDictionary<string, PropertyValidator>(); | |
/// <summary> | |
/// Fired whenever a set of errors occur. Could be quite frequent! | |
/// </summary> | |
public event EventHandler<ErrorEventArgs> OnError; | |
public Validator() | |
{ | |
} | |
/// <summary> | |
/// Check an individual, named property for errors. Wrap with IDataErrorInfo.this[string] | |
/// </summary> | |
/// <param name="propertyName">Property name to check</param> | |
/// <returns>Array of errors</returns> | |
public string[] CheckProperty(string propertyName) | |
{ | |
return this.CheckProperty(propertyName, false); | |
} | |
/// <summary> | |
/// /// Check an individual property for errors. Wrap with IDataErrorInfo.this[string] | |
/// </summary> | |
/// <param name="property">Expression identifying the property to check</param> | |
/// <returns>Array of errors</returns> | |
public string[] CheckProperty<TProperty>(Expression<Func<TProperty>> property) | |
{ | |
return this.CheckProperty(this.ExtractName(property)); | |
} | |
/// <summary> | |
/// Same as CheckProperty, but suppress the OnError event and INotifyPropertyChanged for HasErrors | |
/// </summary> | |
/// <param name="propertyName">Property name to check</param> | |
/// <returns>Array of errors</returns> | |
public string[] CheckPropertyWithoutNotifications(string propertyName) | |
{ | |
return this.CheckProperty(propertyName, true); | |
} | |
/// <summary> | |
/// Same as CheckProperty, but suppress the OnError event and INotifyPropertyChanged for HasErrors | |
/// </summary> | |
/// <param name="propertyName">Property name to check</param> | |
/// <returns>Array of errors</returns> | |
public string[] CheckPropertyWithoutNotifications<TProperty>(Expression<Func<TProperty>> property) | |
{ | |
return this.CheckProperty(this.ExtractName(property), true); | |
} | |
private string[] CheckProperty(string propertyName, bool suppressNotifications) | |
{ | |
PropertyValidator validator; | |
if (this.propertyValidators.TryGetValue(propertyName, out validator)) | |
{ | |
var errors = validator.Validate(); | |
if (!suppressNotifications) | |
{ | |
this.FireOnError(errors); | |
this.NotifyOfPropertyChange(() => this.HasErrors); | |
this.NotifyOfPropertyChange(() => this.Errors); | |
} | |
return errors; | |
} | |
else | |
{ | |
return new string[0]; | |
} | |
} | |
/// <summary> | |
/// Check whether a named property is checked | |
/// </summary> | |
/// <param name="propertyName">Name of property</param | |
public bool IsPropertyChecked(string propertyName) | |
{ | |
return this.propertyValidators.ContainsKey(propertyName); | |
} | |
/// <summary> | |
/// Checked whether a given property is checked | |
/// </summary> | |
/// <param name="property">Expression identifying property</param> | |
/// <returns></returns> | |
public bool IsPropertyChecked<TProperty>(Expression<Func<TProperty>> property) | |
{ | |
return this.IsPropertyChecked(this.ExtractName(property)); | |
} | |
/// <summary> | |
/// Check all properties for errors. This differs from this.Errors in that this.Errors returns cached errors, whereas this re-checks all | |
/// </summary> | |
/// <returns>Array of errors</returns> | |
public string[] CheckAllProperties() | |
{ | |
var errors = this.propertyValidators.Keys.SelectMany(key => this.CheckProperty(key, false)).ToArray(); | |
this.FireOnError(errors); | |
this.NotifyOfPropertyChange(() => this.HasErrors); | |
this.NotifyOfPropertyChange(() => this.Errors); | |
return errors; | |
} | |
/// <summary> | |
/// Return cached validation errors. Wrap with IDataErrorInfo.Error | |
/// </summary> | |
public string[] Errors | |
{ | |
get { return this.propertyValidators.Values.SelectMany(x => x.ResultCache).ToArray(); } | |
} | |
/// <summary> | |
/// True if any properties report errors | |
/// </summary> | |
public bool HasErrors | |
{ | |
get { return this.propertyValidators.Values.Any(x => x.ResultCache.Length > 0); } | |
} | |
/// <summary> | |
/// Base function for validators. Wrap with extension methods in IValidationProvider | |
/// </summary> | |
/// <param name="property">Expression identifying property to validate</param> | |
/// <param name="validator">Delegate accepting property value, and returning false if it fails validation</param> | |
/// <param name="message">Message to return on validation failure. {0} is replaced with property name, {1} with its value</param> | |
/// <returns></returns> | |
public IValidation ValidateWith<TProperty>(Expression<Func<TProperty>> property, Func<TProperty, bool> validator, string message) | |
{ | |
var name = this.ExtractName(property); | |
var compiled = property.Compile(); | |
var validation = new Validation(name, () => compiled(), val => validator((TProperty)val), message); | |
// Validations are never removed, so this is safe | |
this.propertyValidators.TryAdd(name, new PropertyValidator()); | |
this.propertyValidators[name].AddValidation(validation); | |
return validation; | |
} | |
/// <summary> | |
/// Register a callback to be called when a specific property has errors | |
/// </summary> | |
/// <param name="propertyName">Name of property</param> | |
/// <param name="onError">Callback to call</param> | |
public void OnColumnError(string propertyName, Action<string[]> onError) | |
{ | |
this.propertyValidators.TryAdd(propertyName, new PropertyValidator()); | |
this.propertyValidators[propertyName].OnError = onError; | |
} | |
/// <summary> | |
/// Register a callback to be called when a specific property has errors | |
/// </summary> | |
/// <param name="property">Expression identifying property</param> | |
/// <param name="onError">Callback to call</param> | |
public void OnColumnError<TProperty>(Expression<Func<TProperty>> property, Action<string[]> onError) | |
{ | |
this.OnColumnError(this.ExtractName(property), onError); | |
} | |
/// <summary> | |
/// Remove all validators on all properties | |
/// </summary> | |
public void Clear() | |
{ | |
foreach (var validator in this.propertyValidators.Values) | |
{ | |
validator.ClearValidations(); | |
} | |
} | |
private string ExtractName<TProperty>(Expression<Func<TProperty>> property) | |
{ | |
var body = property.Body as MemberExpression; | |
if (body == null) | |
throw new ArgumentException("not a MemberExpression", "property"); | |
return body.Member.Name; | |
} | |
private void FireOnError(string[] errors) | |
{ | |
if (errors.Length == 0) | |
return; | |
var handler = this.OnError; | |
if (handler != null) | |
{ | |
handler(this, new ErrorEventArgs(errors)); | |
} | |
} | |
private class PropertyValidator | |
{ | |
private object validationsLock = new object(); | |
private List<Validation> validations; | |
public IReadOnlyList<Validation> Validations | |
{ | |
get { return this.validations.AsReadOnly(); } | |
} | |
// True if the last validation passed successfully | |
public string[] ResultCache { get; private set; } | |
public Action<string[]> OnError { get; set; } | |
public PropertyValidator() | |
{ | |
this.validations = new List<Validation>(); | |
this.ResultCache = new string[0]; | |
} | |
public string[] Validate() | |
{ | |
// Current behaviour is to stop on the first failure | |
string error; | |
lock (this.validationsLock) | |
{ | |
error = this.validations.Select(x => x.Validate()).FirstOrDefault(x => !String.IsNullOrEmpty(x)); | |
} | |
var errors = (error == null) ? new string[0] : new[] { error }; | |
this.ResultCache = errors; | |
var onError = this.OnError; | |
if (errors.Length > 0 && onError != null) | |
onError(errors); | |
return errors; | |
} | |
public void AddValidation(Validation validation) | |
{ | |
lock (this.validationsLock) | |
{ | |
this.validations.Add(validation); | |
} | |
} | |
public void ClearValidations() | |
{ | |
lock (this.validationsLock) | |
{ | |
this.validations.Clear(); | |
} | |
} | |
} | |
private class Validation : IValidation | |
{ | |
private string propertyName; | |
private string message; | |
private Func<bool> condition; | |
private bool testNull = true; | |
private Func<object> propertySelector; | |
public Func<object, bool> Test { get; private set; } | |
public Validation(string propertyName, Func<object> propertySelector, Func<object, bool> test, string defaultMessage) | |
{ | |
this.propertyName = propertyName; | |
this.propertySelector = propertySelector; | |
this.Test = test; | |
this.message = defaultMessage; | |
} | |
public string Validate() | |
{ | |
object val = this.propertySelector(); | |
var condition = this.condition; | |
if ((val != null || this.testNull) && (condition == null || condition())) | |
return this.Test(val) ? null : String.Format(this.message, this.propertyName, val); | |
else | |
return null; | |
} | |
public IValidation WithMessage(string message) | |
{ | |
this.message = message; | |
return this; | |
} | |
public IValidation When(Func<bool> condition) | |
{ | |
this.condition = condition; | |
return this; | |
} | |
public IValidation TestNull(bool testNull) | |
{ | |
this.testNull = testNull; | |
return this; | |
} | |
} | |
} | |
public class ErrorEventArgs : EventArgs | |
{ | |
public string[] Errors { get; private set; } | |
public ErrorEventArgs(string[] errors) | |
: base() | |
{ | |
this.Errors = errors; | |
} | |
} | |
public interface IValidator : IValidationProvider, INotifyPropertyChanged | |
{ | |
event EventHandler<ErrorEventArgs> OnError; | |
string[] CheckProperty(string propertyName); | |
string[] CheckProperty<TProperty>(Expression<Func<TProperty>> property); | |
string[] CheckPropertyWithoutNotifications(string propertyName); | |
string[] CheckPropertyWithoutNotifications<TProperty>(Expression<Func<TProperty>> property); | |
bool IsPropertyChecked(string propertyName); | |
bool IsPropertyChecked<TProperty>(Expression<Func<TProperty>> property); | |
string[] CheckAllProperties(); | |
string[] Errors { get; } | |
bool HasErrors { get; } | |
void OnColumnError(string propertyName, Action<string[]> onError); | |
void OnColumnError<TProperty>(Expression<Func<TProperty>> property, Action<string[]> onError); | |
void Clear(); | |
} | |
public interface IValidationProvider | |
{ | |
IValidation ValidateWith<TProperty>(Expression<Func<TProperty>> property, Func<TProperty, bool> validator, string message); | |
} | |
public interface IValidation | |
{ | |
/// <summary> | |
/// Provide a custom message for when the validation failes | |
/// </summary> | |
/// <param name="message">Message to display. {0} is replaced with property name, {1} with its value</param> | |
IValidation WithMessage(string message); | |
/// <summary> | |
/// Add a condition for property validation evaluation | |
/// </summary> | |
/// <param name="condition">Validation isn't evaluated if condition returns true - automatically passes</param> | |
IValidation When(Func<bool> condition); | |
/// <summary> | |
/// Validations are run for null values by default. Change this behaviour | |
/// </summary> | |
/// <param name="testNull">False to automatically pass null values</param> | |
IValidation TestNull(bool testNull); | |
} | |
public static class ValidationExtensions | |
{ | |
public static IValidation ValidateNotNull(this IValidationProvider validator, Expression<Func<string>> property) | |
{ | |
return validator.ValidateWith(property, val => val != null, "{0} is null"); | |
} | |
public static IValidation ValidateLength(this IValidationProvider validator, Expression<Func<string>> property, int minLength, int maxLength) | |
{ | |
return validator.ValidateWith(property, val => val.Length >= minLength && val.Length <= maxLength, "{0} is the wrong length").TestNull(false); | |
} | |
public static IValidation ValidateRange(this IValidationProvider validator, Expression<Func<int>> property, int min, int max) | |
{ | |
return validator.ValidateWith(property, val => val >= min && val <= max, "{0} must be between " + min + " and " + max); | |
} | |
public static IValidation ValidateNullRange(this IValidationProvider validator, Expression<Func<int?>> property, int min, int max) | |
{ | |
return validator.ValidateWith(property, val => val != null && val >= min && val <= max, "{0} must be between " + min + " and " + max); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
using System.Windows.Data; | |
namespace ValidationTest | |
{ | |
// http://stackoverflow.com/a/8326207/1086121 | |
public class ValueConverterGroup : List<IValueConverter>, IValueConverter | |
{ | |
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) | |
{ | |
return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture)); | |
} | |
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) | |
{ | |
return this.Aggregate(value, (current, converter) => converter.ConvertBack(current, targetType, parameter, culture)); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment