4
\$\begingroup\$

This is my attempt to write a data-bindable, platform independent property that doesn't require any literal strings. The basic idea is to basically have the same functionality as IObservable, but with a value which has a getter (and a setter in a derived interface). The core interface only has a getter:

public interface IPropertySource<out T>
{
    T Value { get; }
    IDisposable RawSubscribe(Action rawObserver);
}

Since it has a getter, the Action in RawSubscribe doesn't need to receive a value. This can be turned into a monad and add Linq operators with some extra code (I've removed all the null checks to make it shorter) (note that this code requires a reference to reactive extensions):

internal class ExplicitPropertySource<T> : IPropertySource<T>
{
    private readonly Func<T> _GetValue;
    private readonly Func<Action, IDisposable> _RawSubscribe;

    public T Value
    {
        get { return _GetValue(); }
    }

    public ExplicitPropertySource(Func<Action, IDisposable> rawSubscribe, Func<T> getValue)
    {
        _RawSubscribe = rawSubscribe;
        _GetValue = getValue;
    }

    public IDisposable RawSubscribe(Action rawObserver)
    {
        return _RawSubscribe(rawObserver);
    }
}

public static partial class PropertySource
{
    public static IPropertySource<T> Create<T>(Func<Action, IDisposable> rawSubscribe, Func<T> getValue)
    {
        return new ExplicitPropertySource<T>(rawSubscribe, getValue);
    }

    public static IPropertySource<T> Return<T>(T value)
    {
        return PropertySource.Create(observer => Disposable.Empty, () => value);
    }

    public static IPropertySource<T> Distinct<T>(this IPropertySource<T> source, IEqualityComparer<T> comparer)
    {
        return PropertySource.Create(
            action =>
            {
                T cachedValue = default(T);

                Action<T> sendAndCache = value =>
                {
                    action();
                    cachedValue = value;
                };

                Action sendIfChanged = () =>
                {
                    var value = source.Value;

                    if (!comparer.Equals(value, cachedValue))
                        sendAndCache(value);
                };

                Action observer = null;

                observer = () =>
                {
                    observer = sendIfChanged;
                    sendAndCache(source.Value);
                };

                return source.RawSubscribe(() =>
                {
                    observer();
                });
            },
            () => source.Value
        );
    }

    public static IPropertySource<T> Distinct<T>(this IPropertySource<T> source)
    {
        return source.Distinct(EqualityComparer<T>.Default);
    }

    public static IPropertySource<T> Eager<T>(this IPropertySource<T> source)
    {
        return PropertySource.Create(
            action =>
            {
                action();
                return source.RawSubscribe(action);
            },
            () => source.Value
        );
    }

    public static IPropertySource<T> Lazy<T>(this IPropertySource<T> source)
    {
        return PropertySource.Create(
            action =>
            {
                Action send = () => { };
                var subscription = source.RawSubscribe(() => send());
                send = action;
                return subscription;
            },
            () => source.Value
        );
    }

    public static IPropertySource<TResult> SelectMany<TSource, TResult>(this IPropertySource<TSource> source, Func<TSource, IPropertySource<TResult>> selector)
    {
        return PropertySource.Create(
            observer =>
            {
                IDisposable rightSubscription = Disposable.Empty;

                Func<TSource, IPropertySource<TResult>> reattachRight = leftValue =>
                {
                    rightSubscription.Dispose();
                    var rightSource = selector(leftValue);
                    rightSubscription = rightSource.Lazy().RawSubscribe(observer);
                    return rightSource;
                };

                IDisposable leftSubscription = source.Lazy().RawSubscribe(() =>
                {
                    reattachRight(source.Value);
                    observer();
                });

                reattachRight(source.Value);

                return Disposable.Create(() =>
                {
                    leftSubscription.Dispose();
                    rightSubscription.Dispose();
                });
            },
            () => selector(source.Value).Value
        );
    }

    public static IPropertySource<TResult> Select<TSource, TResult>(this IPropertySource<TSource> source, Func<TSource, TResult> selector)
    {
        return source.SelectMany(value => PropertySource.Return(selector(value)));
    }

    public static IPropertySource<TResult> Merge<TLeft, TRight, TResult>(this IPropertySource<TLeft> left, IPropertySource<TRight> right, Func<TLeft, TRight, TResult> selector)
    {
        return left.SelectMany(leftValue =>
            right.Select(rightValue => selector(leftValue, rightValue))
        );
    }
}

Since I'm using WinForms, I can then create these using standard WinForms properties:

public class MemberAccessInfo
{
    public readonly object Instance;
    public readonly string MemberName;

    public MemberAccessInfo(object instance, string memberName)
    {
        Instance = instance;
        MemberName = memberName;
    }
}

public static partial class PropertySource
{
    private static MemberAccessInfo GetMemberAccessInfo<T>(Expression<Func<T>> memberAccessExpression)
    {
        LambdaExpression lambda = (LambdaExpression)memberAccessExpression;

        if (lambda.Body.NodeType != ExpressionType.MemberAccess)
            throw new Exception("Expression must be a member access.");

        MemberExpression memberExpr = (MemberExpression)lambda.Body;

        var instance = Expression.Lambda(memberExpr.Expression).Compile().DynamicInvoke();

        return new MemberAccessInfo(instance, memberExpr.Member.Name);
    }

    internal static IPropertySource<T> FromProperty<T>(object instance, Type instanceType, PropertyInfo memberInfo)
    {
        var eventInfo = instanceType.GetEvent(memberInfo.Name + "Changed", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public);

        Func<Action, IDisposable> subscribe;

        if (eventInfo != null && eventInfo.EventHandlerType == typeof(EventHandler))
        {
            subscribe = observer =>
            {
                EventHandler handler = (s, e) => observer();
                eventInfo.AddEventHandler(instance, handler);
                return Disposable.Create(() => eventInfo.RemoveEventHandler(instance, handler));
            };
        }
        else
        {
            var notifyPropertyChanged = instance as INotifyPropertyChanged;

            if (notifyPropertyChanged == null)
                throw new Exception("This member cannot be observed.");

            subscribe = observer =>
            {
                PropertyChangedEventHandler handler = (s, e) =>
                {
                    if (e.PropertyName == memberInfo.Name)
                        observer();
                };

                notifyPropertyChanged.PropertyChanged += handler;
                return Disposable.Create(() => notifyPropertyChanged.PropertyChanged -= handler);
            };
        }

        return Create(subscribe, () => (T)memberInfo.GetValue(instance));
    }

    public static IPropertySource<T> FromProperty<T>(object instance, string propertyName)
    {
        var type = instance.GetType();
        return FromProperty<T>(instance, type, type.GetProperty(propertyName));
    }

    public static IPropertySource<T> FromProperty<T>(Expression<Func<T>> memberAccessExpression)
    {
        var propertyInfo = GetMemberAccessInfo(memberAccessExpression);
        return FromProperty<T>(propertyInfo.Instance, propertyInfo.MemberName);
    }
}

And finally, subscribe with these:

public static partial class PropertySource
{
    public static IDisposable Subscribe<T>(this IPropertySource<T> source, Action<T> observer, IEqualityComparer<T> comparer)
    {
        return source.Eager().Distinct(comparer).RawSubscribe(() => observer(source.Value));
    }

    public static IDisposable Subscribe<T>(this IPropertySource<T> source, Action<T> observer)
    {
        return source.Eager().Distinct().RawSubscribe(() => observer(source.Value));
    }
}

So I can do stuff like:

var firstNameProperty = PropertySource.FromProperty(() => firstNameTextBox.Text);
var lastNameProperty = PropertySource.FromProperty(() => lastNameTextBox.Text);
var fullNameProperty = firstNameProperty.Merge(lastNameProperty, (fn, ln) => fn + " " + ln);
var disposable = fullNameProperty.Subscribe(val => fullNameLabel.Text = val);

This seems to me like such an obvious missing feature I was wondering if something like this already exists, and if not, whether I should make any improvements to my code. Also, I'm still not 100% my monad is actually a monad, but so far it's been working fine.

This whole thing probably doesn't make much sense without the IProperty interface which also has a property setter, but is not a monad (it does have a SelectMany and Select methods though), and can also be created given a WinForms property. I'm not including it here since it's already pretty big, but I uploaded the whole thing to GitHib, and included a small sample WinForms application.

\$\endgroup\$
  • \$\begingroup\$ Ok.. So, I admittedly haven't worked out all the details in this implementation, but it's 3 lines of code. Keep it stupid simple. There's really nothing wrong with Winforms' data binding if you create some simple wrappers and implement INotifyPropertyChanged. \$\endgroup\$ – RubberDuck Jun 21 '16 at 1:16
  • \$\begingroup\$ @RubberDuck I agree WinForms bindings can be simplified with some wrappers, but I'm not really trying to wrap around WinForms bindings but to write an independent library that doesn't relay on WinForms at all (although it can be integrated with it). I was inspired by your README file and wrote a more comprehensive one to give a better idea of what the point of the library is. \$\endgroup\$ – Juan Jun 21 '16 at 4:44
1
\$\begingroup\$
T cachedValue = default(T);

Action<T> sendAndCache = value =>
{
    action();
    cachedValue = value;
};

Action sendIfChanged = () =>
{
    var value = source.Value;

    if (!comparer.Equals(value, cachedValue))
        sendAndCache(value);
};

Action observer = null;

observer = () =>
{
    observer = sendIfChanged;
    sendAndCache(source.Value);
};

return source.RawSubscribe(() =>
{
    observer();
});

Here, you create a special initial observer which always sends the value (since there is nothing to compare it to) and then replace it with a normal comparing observer. I think it would be simpler and clearer to have a single observer, along with a variable tracking whether the cachedValue has been set:

T cachedValue = default(T);
bool cachedValueSet = false;

Action observer = () =>
{
    var value = source.Value;

    if (!cachedValueSet || !comparer.Equals(value, cachedValue))
    {
        action();
        cachedValue = value;
        cachedValueSet = true;
    }
};

return source.RawSubscribe(observer);

() => selector(source.Value).Value

What happens if source.Value is null?


I'm also wondering about the monad thing: does using LINQ queries on your objects make sense? If not, I probably wouldn't use names like Select or SelectMany.

\$\endgroup\$
  • \$\begingroup\$ I've modified the Distinct method a bit. Here's the new version. Regarding the null, it'll just throw. This is the same behavior as Enumerable's SelectMany, so I guess that'd be the expected thing to happen. \$\endgroup\$ – Juan Jul 13 '16 at 19:13
  • \$\begingroup\$ @Juan I don't think it is, I would expect binding to A.B to not throw, even when A is null. Also an enumerable can be empty, to me, null property seems to be a natural equivalent to that. \$\endgroup\$ – svick Jul 13 '16 at 19:21
  • \$\begingroup\$ I misunderstood you, I thought you meant when the result of selector is null. If source.Value is null, It'd just pass null to the selector and I'd be up to the developer to determine what to do (I actually do this in the sample application). Likewise, Enumerable's SelectMany would pass null if there's a null item in the source list, which is the equivalent situation. It doesn't really make sense to think of an empty property since, unlike IObservable, you can get its value at any time. I think what you're talking about is a mix of this and the nullable monad. \$\endgroup\$ – Juan Jul 13 '16 at 19:41
  • \$\begingroup\$ BTW, a mix of nullable and property monads could be easily written with an alternative SelectMany with this code: source.SelectMany(val => val == null ? null : selector(val)). But I don't think It'd be a good idea to force this mixed monad, since there's no way to go back to the pure property monad (unlike the other way around). Also, I'd be forcing users to use nullable types only. \$\endgroup\$ – Juan Jul 13 '16 at 19:46
  • \$\begingroup\$ Actually my code was wrong, it should be source.SelectMany(val => val == null ? PropertySource.Return<T>(null) : selector(val)) \$\endgroup\$ – Juan Jul 13 '16 at 20:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.