Skip to content

Instantly share code, notes, and snippets.

@bradphelan
Created January 25, 2017 06:47
Show Gist options
  • Save bradphelan/77baac0b1ac7829d923df93d2b2e96c6 to your computer and use it in GitHub Desktop.
Save bradphelan/77baac0b1ac7829d923df93d2b2e96c6 to your computer and use it in GitHub Desktop.
Implementation of ValidatingReactiveObject and ancillary files
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 (!@this.IsSome)
{
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
}
/// <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);
}
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