Created
January 25, 2017 06:47
-
-
Save bradphelan/77baac0b1ac7829d923df93d2b2e96c6 to your computer and use it in GitHub Desktop.
Implementation of ValidatingReactiveObject and ancillary files
This file contains 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; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Reactive.Linq; | |
using System.Runtime.CompilerServices; | |
using System.Threading.Tasks; | |
namespace Weingartner.Lens | |
{ | |
#region Maybe | |
public delegate TOutput ElseDelegate< TOutput>(); | |
public delegate Maybe<TOutput> ElseDelegate2< TOutput>(); | |
public struct Maybe<TResult> : IEnumerable<TResult> | |
{ | |
public bool Equals(Maybe<TResult> other) | |
{ | |
return EqualityComparer<TResult>.Default.Equals( Value, other.Value ) && IsSome == other.IsSome; | |
} | |
public override bool Equals(object obj) | |
{ | |
if (ReferenceEquals( null, obj )) return false; | |
return obj is Maybe<TResult> && Equals( (Maybe<TResult>) obj ); | |
} | |
public override int GetHashCode() | |
{ | |
unchecked { return (EqualityComparer<TResult>.Default.GetHashCode( Value )*397) ^ IsSome.GetHashCode(); } | |
} | |
public static bool operator ==(Maybe<TResult> left, Maybe<TResult> right) | |
{ | |
return left.Equals( right ); | |
} | |
public static bool operator !=(Maybe<TResult> left, Maybe<TResult> right) | |
{ | |
return !left.Equals( right ); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Maybe(TResult r) : this() | |
{ | |
IsSome = true; | |
Value = r; | |
} | |
public static Maybe<TResult> None = new Maybe<TResult>(); | |
public static implicit operator Maybe<TResult>(None none) | |
{ | |
return None<TResult>.Default; | |
} | |
public static implicit operator Maybe<TResult>(TResult t) | |
{ | |
return t.ToMaybe(); | |
} | |
public override string ToString() | |
{ | |
var wrapper = IsSome ? "Some<{0}>" : "None<{0}>"; | |
return string.Format(wrapper, typeof (TResult).Name); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Maybe<B> Bind<B>(Func<TResult, Maybe<B>> f) | |
{ | |
return !IsSome ? new Maybe<B>() : f(Value); | |
} | |
public TResult Value { | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
get; | |
} | |
public bool IsSome { get; } | |
public bool IsNone => !IsSome; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public IEnumerator<TResult> GetEnumerator() | |
{ | |
if (IsSome) | |
{ | |
yield return Value; | |
} | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
IEnumerator IEnumerable.GetEnumerator() | |
{ | |
if (IsSome) | |
{ | |
yield return Value; | |
} | |
} | |
} | |
[Serializable] | |
public sealed class None<T> | |
{ | |
public static readonly Maybe<T> Default = new Maybe<T>(); | |
} | |
/// <summary> | |
/// Helper class | |
/// </summary> | |
public sealed class None | |
{ | |
/// <summary> | |
/// This can be implicitly cast to <![CDATA[Maybe<T>]]> | |
/// </summary> | |
public static readonly None Default = new None(); | |
} | |
public static class MaybeMixins | |
{ | |
public static IEnumerable<T> WhereIsSome<T>(this IEnumerable<Maybe<T>> This) | |
{ | |
return This.SelectMany(x => x, (x, y) => y); | |
} | |
public static IObservable<T> WhereIsSome<T>(this IObservable<Maybe<T>> This) | |
{ | |
return This.SelectMany(x => x, (x, y) => y); | |
} | |
/// <typeparam name="TSource"></typeparam> | |
/// <typeparam name="TResult"></typeparam> | |
/// <param name="this"></param> | |
/// <param name="selector"></param> | |
/// <returns></returns> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static IObservable<Maybe<TResult>> SelectMaybe | |
<TSource, TResult> | |
(this IObservable<Maybe<TSource>> @this, | |
Func<TSource, TResult> selector) | |
{ | |
return @this.Select(q => q.Select(selector)); | |
} | |
public static IObservable<Maybe<TResult>> SelectManyMaybe | |
<TSource, TResult> | |
(this IObservable<Maybe<TSource>> @this, | |
Func<TSource, Maybe<TResult>> selector) | |
{ | |
return @this.Select(q => q.SelectMany(selector)); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static IObservable<TResult> SelectMaybe | |
<TSource, TResult> | |
(this IObservable<Maybe<TSource>> @this, | |
Func<TSource, TResult> then | |
,TResult @else ) | |
{ | |
return @this.Select(q => q.Select(then).Else(@else)); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static IObservable<TResult> SelectMaybe | |
<TSource, TResult> | |
(this IObservable<Maybe<TSource>> @this, | |
Func<TSource, TResult> selector | |
,ElseDelegate<TResult> @else ) | |
{ | |
return @this.Select(q => q.Select(selector).Else(@else)); | |
} | |
/// <summary> | |
/// SelectMany helper bridging IObservable and Maybe | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <typeparam name="TO"></typeparam> | |
/// <typeparam name="TResult"></typeparam> | |
/// <param name="this"></param> | |
/// <param name="func"></param> | |
/// <param name="func2"></param> | |
/// <returns></returns> | |
public static IObservable<TResult> | |
SelectMany<T, TO, TResult> | |
( this Maybe<T> @this | |
, Func<T, IObservable<TO>> func | |
, Func<T, TO, TResult> func2) | |
{ | |
if ([email protected]) | |
{ | |
return Observable.Empty<TResult>(); | |
} | |
var v0 = func(@this.Value); | |
return v0.Select(v => func2(@this.Value, v)); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static Maybe<T> Match<T>(this Maybe<T> This, Action<T> action) | |
{ | |
if (This.IsSome) | |
{ | |
action(This.Value); | |
} | |
return This; | |
} | |
public static Maybe<T> Match<T>(this Maybe<T> This, Action action) | |
{ | |
if (!This.IsSome) | |
{ | |
action(); | |
} | |
return This; | |
} | |
public static Maybe<TValue> MaybeGetItem<TKey, TValue> | |
(this IDictionary<TKey, TValue> @this, TKey key) | |
{ | |
TValue value; | |
if (@this.TryGetValue(key, out value)) | |
{ | |
return value.ToMaybe(); | |
} | |
else | |
{ | |
return None<TValue>.Default; | |
}; | |
} | |
/// <summary> | |
/// This implementation of Switch makes the observation that | |
/// None of IObservable is equivalent to IObservable.Empty so | |
/// it is reasonable and possible to switch on this pattern. | |
/// | |
/// Note that getting an event of None of Observable is equivalent | |
/// to shutting off the downstream events until a Some of Observable | |
/// is encountered | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="This"></param> | |
/// <returns></returns> | |
public static IObservable<T> Switch<T>(this IObservable<Maybe<IObservable<T>>> This){ | |
return This.Select(e=>e.Else(() => Observable.Empty<T>())).Switch(); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static T Else<T>(this Maybe<T> This, T defaultValue) | |
{ | |
if (This.IsSome) | |
{ | |
return This.Value; | |
} | |
else | |
{ | |
return defaultValue; | |
} | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static T ElseNull<T>(this Maybe<T> This) | |
where T : class | |
{ | |
if (This.IsSome) | |
{ | |
return This.Value; | |
} | |
else | |
{ | |
return null; | |
} | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static T Else<T>(this Maybe<T> This, ElseDelegate<T> del) | |
{ | |
if (This.IsSome) | |
{ | |
return This.Value; | |
} | |
else | |
{ | |
return del(); | |
} | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static Maybe<T> Else<T>(this Maybe<T> This, ElseDelegate2<T> del) | |
{ | |
if (This.IsSome) | |
{ | |
return This; | |
} | |
else | |
{ | |
return del(); | |
} | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static T ElseThrowWith<TException, T>(this Maybe<T> This, Func<TException> xFactory) | |
where TException : Exception | |
{ | |
if (This.IsSome) | |
{ | |
return This.Value; | |
} | |
else | |
{ | |
throw xFactory(); | |
} | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static Maybe<TResult> Select<TSource, TResult>(this Maybe<TSource> m, Func<TSource, TResult> f) | |
{ | |
return m.Bind(x => f(x).ToMaybe()); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static async Task<Maybe<TResult>> Select<TSource,TResult>(this Maybe<TSource> This, Func<TSource,Task<TResult>> action) | |
{ | |
if (This.IsSome) | |
{ | |
return await action(This.Value); | |
} | |
return Maybe<TResult>.None; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static async Task Select<TSource>(this Maybe<TSource> This, Func<TSource,Task> action) | |
{ | |
if (This.IsSome) | |
{ | |
await action(This.Value); | |
} | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static Maybe<TResult> SelectMany<TSource, TResult>(this Maybe<TSource> m, Func<TSource, Maybe<TResult>> f) | |
{ | |
return m.Bind(x => f(x)); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static Maybe<TResult> SelectMany<TSource, TMaybe, TResult>(this Maybe<TSource> m, Func<TSource, Maybe<TMaybe>> f, Func<TSource, TMaybe, TResult> g) | |
{ | |
return m.Bind(x => f(x).Bind(y => g(x, y).ToMaybe())); | |
} | |
#region IEnumerableMaybe | |
//public static IEnumerable<TResult> SelectMany<TSource, TResult>( | |
//this Maybe<TSource> source, | |
//Func<TSource, IEnumerable<TResult>> @then) | |
//{ | |
// if (source.IsSome) | |
// { | |
// foreach (var v in @then(source.Value)) | |
// yield return v; | |
// } | |
//} | |
//public static IEnumerable<TResult> SelectMany<TSource, TResult>( | |
//this Maybe<TSource> source, | |
//Func<TSource, int, IEnumerable<TResult>> @then) | |
//{ | |
// int i = 0; | |
// if (source.IsSome) | |
// { | |
// foreach (var v in @then(source.Value, i++)) | |
// yield return v; | |
// } | |
//} | |
//public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>( | |
// this Maybe<TSource> source, | |
// Func<TSource, IEnumerable<TCollection>> collectionSelector, | |
// Func<TSource, TCollection, TResult> resultSelector) | |
//{ | |
// if (source.IsSome) | |
// { | |
// foreach(var v in collectionSelector(source.Value)) | |
// yield return resultSelector(source.Value, v); | |
// } | |
//} | |
//public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>( | |
// this Maybe<TSource> source, | |
// Func<TSource, int, IEnumerable<TCollection>> collectionSelector, | |
// Func<TSource, TCollection, TResult> resultSelector) | |
//{ | |
// int i = 0; | |
// if (source.IsSome) | |
// { | |
// foreach(var v in collectionSelector(source.Value, i++)) | |
// yield return resultSelector(source.Value, v); | |
// } | |
//} | |
#endregion | |
#region IEnumerable | |
//public static IEnumerable<TResult> SelectMany<TSource, TResult>( | |
//this IEnumerable<Maybe<TSource>> source, | |
//Func<TSource, Maybe<TResult>> @then) | |
//{ | |
// foreach (var item in source.Where((x) => x.IsSome)) | |
// { | |
// if (item.IsSome) | |
// { | |
// var item2 = @then(item.Value); | |
// if (item2.IsSome) | |
// { | |
// yield return item2.Value; | |
// } | |
// } | |
// } | |
//} | |
//public static IEnumerable<TResult> SelectMany<TSource, TResult>( | |
// this IEnumerable<Maybe<TSource>> source, | |
// Func<TSource, int, Maybe<TResult>> @then) | |
//{ | |
// int i = 0; | |
// foreach (var item in source.Where((x) => x.IsSome)) | |
// { | |
// var item2 = @then(item.Value, i++); | |
// if (item2.IsSome) | |
// { | |
// yield return item2.Value; | |
// } | |
// } | |
//} | |
//public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>( | |
// this IEnumerable<Maybe<TSource>> source, | |
// Func<TSource, Maybe<TCollection>> collectionSelector, | |
// Func<TSource, TCollection, TResult> resultSelector) | |
//{ | |
// foreach (var item in source.Where((x) => x.IsSome)) | |
// { | |
// var item2 = collectionSelector(item.Value); | |
// if (item2.IsSome) | |
// { | |
// yield return resultSelector(item.Value, item2.Value); | |
// } | |
// } | |
//} | |
//public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>( | |
// this IEnumerable<Maybe<TSource>> source, | |
// Func<TSource, int, Maybe<TCollection>> collectionSelector, | |
// Func<TSource, TCollection, TResult> resultSelector) | |
//{ | |
// int i = 0; | |
// foreach (var item in source.Where((x) => x.IsSome)) | |
// { | |
// var item2 = collectionSelector(item.Value, i); | |
// if (item2.IsSome) | |
// { | |
// yield return resultSelector(item.Value, item2.Value); | |
// } | |
// } | |
//} | |
#endregion | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static Maybe<T> Where<T>(this Maybe<T> This, Predicate<T> p) | |
{ | |
return This.Bind((x) => (p(x) ? x.ToMaybe() : None<T>.Default)); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static dynamic ToMaybeDynamic(dynamic This) | |
{ | |
Type type = This.GetType(); | |
var nonetype = typeof(Maybe<>).MakeGenericType(type); | |
var sometype = typeof(Maybe<>).MakeGenericType(type); | |
if (This == null) | |
{ | |
return Activator.CreateInstance(nonetype); | |
} | |
else | |
{ | |
return Activator.CreateInstance(sometype, This); | |
} | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static Maybe<TSource> ToMaybe<TSource>(this TSource This) | |
{ | |
if (This == null) | |
{ | |
return None<TSource>.Default; | |
} | |
else | |
{ | |
return new Maybe<TSource>(This); | |
} | |
} | |
} | |
#endregion | |
} |
This file contains 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
/// <summary> | |
/// Will get the property if it exists and it is the | |
/// correct type otherwise it will return None. | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <typeparam name="V"></typeparam> | |
/// <param name="This"></param> | |
/// <param name="name"></param> | |
/// <returns></returns> | |
public static Maybe<V> TryGet<T, V> | |
(this T This, string name) | |
where V:class | |
{ | |
if (!This.HasPrivateProperty(name)) | |
return Maybe<V>.None; | |
return This.GetPrivatePropertyValue<object>(name) as V ?? Maybe<V>.None; | |
} | |
public static bool HasPrivateProperty(this object obj, string propName) | |
{ | |
if (obj == null) throw new ArgumentNullException(nameof(obj)); | |
return obj.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) != null; | |
} | |
/// <summary> | |
/// Returns a _private_ Property Value from a given Object. Uses Reflection. | |
/// Throws a ArgumentOutOfRangeException if the Property is not found. | |
/// </summary> | |
/// <typeparam name="T">Type of the Property</typeparam> | |
/// <param name="obj">Object from where the Property Value is returned</param> | |
/// <param name="propName">Propertyname as string.</param> | |
/// <returns>PropertyValue</returns> | |
public static T GetPrivatePropertyValue<T>(this object obj, string propName) | |
{ | |
if (obj == null) throw new ArgumentNullException(nameof(obj)); | |
PropertyInfo pi = obj.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); | |
if (pi == null) throw new ArgumentOutOfRangeException(nameof(propName), string.Format("Property {0} was not found in Type {1}", propName, obj.GetType().FullName)); | |
return (T)pi.GetValue(obj, null); | |
} | |
This file contains 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; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Reactive; | |
using System.Reactive.Linq; | |
using System.Threading; | |
using System.Windows; | |
using FluentValidation; | |
using FluentValidation.Results; | |
using ReactiveUI.Ext.Logging; | |
using ReactiveUI.Fody.Helpers; | |
using Weingartner.Lens; | |
using Weingartner.Utils; | |
namespace ReactiveUI.Ext | |
{ | |
public interface INotifyDataExceptionInfo : INotifyDataErrorInfo | |
{ | |
ILookup<string, Exception> Errors { get; } | |
/// <summary> | |
/// Add errors to the object. If nest is true then the path | |
/// is checked for nesting. If the path is nested then | |
/// AddErrors is applied recursively to all found objects. | |
/// </summary> | |
/// <param name="path"></param> | |
/// <param name="errors"></param> | |
/// <param name="nest"></param> | |
void AddErrors(string path, IEnumerable<Exception> errors, bool nest = true); | |
void RemoveAllErrors(bool nest = true); | |
/// <summary> | |
/// processes the validation result and set's errors | |
/// for each property. | |
/// </summary> | |
/// <param name="error"></param> | |
void AddValidationErrors(ValidationResult error); | |
void AddError(Exception error); | |
} | |
public class ValidatingReactiveObject<TSelf> : ReactiveObject, IEnableSerilog, INotifyDataExceptionInfo | |
where TSelf : ValidatingReactiveObject<TSelf> | |
{ | |
/// <summary> | |
/// Get all the errors for the property | |
/// </summary> | |
/// <param name="propertyName"></param> | |
/// <returns></returns> | |
public IEnumerable GetErrors( string propertyName ) | |
{ | |
return from key in _Errors.Keys | |
where key == propertyName | |
from err in _Errors[key] | |
select err.Message; | |
} | |
public bool HasErrors => _Errors.SelectMany(p => p).Any(); | |
protected ValidatingReactiveObject(bool verifyOnUIThread = true ) | |
{ | |
_Errors = new MultiLookup<string, Exception, List<Exception>>(); | |
PropertyChanged += ValidatingReactiveObject_PropertyChanged; | |
VerifyOnUIThread = verifyOnUIThread; | |
RemoveAllErrors(); | |
this.WhenAnyValue(p=>p.HasErrors) | |
.Subscribe(e=> | |
{ | |
if(e) | |
{ | |
foreach (var propErrors in _Errors) | |
{ | |
foreach (var error in propErrors) | |
{ | |
((TSelf)this).Serilog().Error("Property Validation Error {key} {error} ", propErrors.Key, error.Message); | |
} | |
} | |
} | |
}); | |
RaiseErrorEvents(); | |
} | |
protected bool VerifyOnUIThread { get; set; } | |
private void ValidatingReactiveObject_PropertyChanged(object sender, PropertyChangedEventArgs e) | |
{ | |
if(Application.Current !=null && VerifyOnUIThread) | |
Debug.Assert(Thread.CurrentThread == Application.Current.Dispatcher.Thread); | |
} | |
/// <summary> | |
/// Raises the error changed event for the property 'path' | |
/// and all the other events that will fire. If 'path' is | |
/// null then it is a global error event change. | |
/// </summary> | |
/// <param name="path"></param> | |
private void RaiseErrorEvents(string path = null) | |
{ | |
if(path!=null) | |
RaiseErrorChanged(path); | |
// Create a new immutable lookup on every error change. | |
Errors = _Errors.SelectMany(p => p.Select(i => new {p.Key, i})).ToLookup(o => o.Key, o => o.i); | |
// ReSharper disable once ExplicitCallerInfoArgument | |
this.RaisePropertyChanged(nameof(HasErrors)); | |
} | |
public void AddErrors(string path, IEnumerable<Exception> errors, bool nest = true) | |
{ | |
var exceptions = errors as IList<Exception> ?? errors.ToList(); | |
var nestedPath = path.Split('.').ToList(); | |
if (nestedPath.Count > 1 && nest) | |
{ | |
var tail = string.Join(".", nestedPath.Skip(1)); | |
var notifyDataExceptionInfo = this.TryGet<INotifyDataExceptionInfo,INotifyDataExceptionInfo>(nestedPath[0]); | |
if(notifyDataExceptionInfo.IsSome) | |
notifyDataExceptionInfo.Value.AddErrors(tail, exceptions); | |
} | |
_Errors.RemoveKey(path); | |
foreach (var error in exceptions) | |
{ | |
_Errors.Add(path, error); | |
} | |
RaiseErrorEvents(path); | |
} | |
public void AddError(Exception error) | |
{ | |
AddErrors("", new [] {error}); | |
} | |
/// <summary> | |
/// processes the validation result and set's errors | |
/// for each property. | |
/// </summary> | |
/// <param name="error"></param> | |
public void AddValidationErrors(ValidationResult error) | |
{ | |
lock (this) | |
{ | |
error | |
.Errors | |
.GroupBy(o=>o.PropertyName) | |
.ForEach(group => | |
{ | |
AddErrors(group.Key, group.Select(p=> | |
{ | |
var propertyNameKey = "PropertyName"; | |
if (!p?.FormattedMessagePlaceholderValues?.ContainsKey(propertyNameKey)??false) | |
return new Exception(p.ErrorMessage); | |
var name = p?.FormattedMessagePlaceholderValues?[propertyNameKey] as string; | |
return name != null && (p.ErrorMessage?.Contains(name)??false) | |
? new Exception(p.ErrorMessage) | |
: new Exception(name + " " + p.ErrorMessage); | |
})); | |
}); | |
} | |
} | |
public void RemoveAllErrors(bool nest = true) | |
{ | |
var cleared = _Errors.Select(k => k.Key).ToList(); | |
foreach (var path in cleared) | |
{ | |
AddErrors(path, new Exception [] {}, nest); | |
} | |
} | |
public Maybe<Exception> Error | |
{ | |
get { | |
if (!_Errors.Any()) | |
return Maybe<Exception>.None; | |
var exceptions = _Errors | |
.Keys | |
.SelectMany(key => _Errors[key] ) | |
.ToList(); | |
return exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); | |
} | |
} | |
/// <summary> | |
/// We store either an Exception or a plain error message here. | |
/// </summary> | |
private readonly MultiLookup<string, Exception, List<Exception>> _Errors; | |
[Reactive]public ILookup<string, Exception> Errors { get; private set; } | |
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; | |
protected void RaiseErrorChanged(string propertyName) => ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); | |
/// <summary> | |
/// Set an error not associated with a property and register an observable | |
/// that will cause the error to be cleared. | |
/// </summary> | |
/// <typeparam name="TTrigger"></typeparam> | |
/// <param name="e"></param> | |
/// <param name="clearTrigger"></param> | |
public async void SetTransientError<TTrigger> | |
(Exception e, IObservable<TTrigger> clearTrigger) | |
{ | |
try | |
{ | |
AddError(e); | |
await clearTrigger; | |
} | |
finally | |
{ | |
AddError(null); | |
} | |
} | |
} | |
public class Validator<Q> : AbstractValidator<Q> | |
{ | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment