如何通过合成而不是继承来创建动态对象

时间:2016-12-21 15:50:28

标签: c# dynamic

我知道创建动态对象的经典方法是从 DynamicObject 继承。但是,如果我已经有一个类,并且我希望将动态属性添加到其子类中,那么我就会被卡住。

假设我有一个类 ReactiveObject 我希望使用DynamicObject为它添加动态属性。所以我这样做

public class MyReactiveObject : ReactiveObject,  IDynamicMetaObjectProvider{
    public DynamicMetaObject GetMetaObject(Expression parameter)
    {
         ...
    }
}

我认为这样做的简单方法可能是创建一个DynamicObject实例并代理对它的调用。

public class MyDynamicObject : DynamicObject{}
public class MyReactiveObject : ReactiveObject,  IDynamicMetaObjectProvider{
    MyDynamicObject DynamicObject = new MyDynamicObject();
    public DynamicMetaObject GetMetaObject(Expression parameter)
    {
         return this.DynamicObject.GetMetaObject(parameter);
    }
}

除了不起作用,因为返回的元对象对MyReactiveObject上的方法一无所知。如果没有完全重新实现DynamicObject,有没有简单的方法。

2 个答案:

答案 0 :(得分:1)

我遇到了以下要点。

https://gist.github.com/breezhang/8954586

public sealed class ForwardingMetaObject : DynamicMetaObject
{
    private readonly DynamicMetaObject _metaForwardee;

    public ForwardingMetaObject(
        Expression expression,
        BindingRestrictions restrictions,
        object forwarder,
        IDynamicMetaObjectProvider forwardee,
        Func<Expression, Expression> forwardeeGetter
        ) : base(expression, restrictions, forwarder)
    {

        // We'll use forwardee's meta-object to bind dynamic operations.
        _metaForwardee = forwardee.GetMetaObject(
            forwardeeGetter(
                Expression.Convert(expression, forwarder.GetType())   // [1]
            )
        );
    }

    // Restricts the target object's type to TForwarder. 
    // The meta-object we are forwarding to assumes that it gets an instance of TForwarder (see [1]).
    // We need to ensure that the assumption holds.
    private DynamicMetaObject AddRestrictions(DynamicMetaObject result)
    {
        var restricted = new DynamicMetaObject(
            result.Expression,
            BindingRestrictions.GetTypeRestriction(Expression, Value.GetType()).Merge(result.Restrictions),
            _metaForwardee.Value
            );
        return restricted;
    }

    // Forward all dynamic operations or some of them as needed //

    public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
    {
        return AddRestrictions(_metaForwardee.BindGetMember(binder));
    }

    public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
    {
        return AddRestrictions(_metaForwardee.BindSetMember(binder, value));
    }

    public override DynamicMetaObject BindDeleteMember(DeleteMemberBinder binder)
    {
        return AddRestrictions(_metaForwardee.BindDeleteMember(binder));
    }

    public override DynamicMetaObject BindGetIndex(GetIndexBinder binder, DynamicMetaObject[] indexes)
    {
        return AddRestrictions(_metaForwardee.BindGetIndex(binder, indexes));
    }

    public override DynamicMetaObject BindSetIndex(SetIndexBinder binder, DynamicMetaObject[] indexes, DynamicMetaObject value)
    {
        return AddRestrictions(_metaForwardee.BindSetIndex(binder, indexes, value));
    }

    public override DynamicMetaObject BindDeleteIndex(DeleteIndexBinder binder, DynamicMetaObject[] indexes)
    {
        return AddRestrictions(_metaForwardee.BindDeleteIndex(binder, indexes));
    }

    public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
    {
        return AddRestrictions(_metaForwardee.BindInvokeMember(binder, args));
    }

    public override DynamicMetaObject BindInvoke(InvokeBinder binder, DynamicMetaObject[] args)
    {
        return AddRestrictions(_metaForwardee.BindInvoke(binder, args));
    }

    public override DynamicMetaObject BindCreateInstance(CreateInstanceBinder binder, DynamicMetaObject[] args)
    {
        return AddRestrictions(_metaForwardee.BindCreateInstance(binder, args));
    }

    public override DynamicMetaObject BindUnaryOperation(UnaryOperationBinder binder)
    {
        return AddRestrictions(_metaForwardee.BindUnaryOperation(binder));
    }

    public override DynamicMetaObject BindBinaryOperation(BinaryOperationBinder binder, DynamicMetaObject arg)
    {
        return AddRestrictions(_metaForwardee.BindBinaryOperation(binder, arg));
    }

    public override DynamicMetaObject BindConvert(ConvertBinder binder)
    {
        return AddRestrictions(_metaForwardee.BindConvert(binder));
    }


}

所以我写了

using System;
using System.CodeDom;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using ReactiveUI;

namespace Weingartner.Lens
{

    public class Dyno : DynamicObject
    {
        private readonly DynamicNotifyingObject _D;
        public Dyno(DynamicNotifyingObject d)
        {
            _D = d;
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            bool ret = base.TryGetMember(binder, out result);

            if (ret == false)
            {
                result = _D.GetPropertyValue(binder.Name);
                if (result != null)
                {
                    ret = true;
                }
            }

            return ret;
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            bool ret = base.TrySetMember(binder, value);

            if (ret == false)
            {
                _D.SetPropertyValue(binder.Name, value);
                ret = true;
            }

            return ret;
        }
    }

继承自ReactiveObject的主要对象,但我们也可以添加动态属性。

    /// <summary>
    /// An object you can add properties to at runtime which raises INPC events when those
    /// properties are changed.
    /// </summary>
    [DataContract]
    public class DynamicNotifyingObject : ReactiveObject, IDynamicMetaObjectProvider
    {
        #region Private Members

        [DataMember]
        private Dictionary<string, object> _dynamicProperties;
        [DataMember]
        private Dictionary<string, Type> _dynamicPropertyTypes;

        [IgnoreDataMember]
        private Dyno _dynamicObject { get; set; }

        public Dyno DynamicObject
        {
            get
            {
                lock (this)
                {
                    return _dynamicObject ?? (_dynamicObject = new Dyno(this));
                }
            }
        }

        #endregion Private Members

        #region Constructor

        public DynamicNotifyingObject() : this(new Tuple<string,Type>[] { }) { }

        public DynamicNotifyingObject(IEnumerable<Tuple<string,Type>> propertyNames)
        {
            if (propertyNames == null)
            {
                throw new Exception("propertyNames is empty");
            }

            _dynamicProperties = new Dictionary<string, object>();
            _dynamicPropertyTypes = new Dictionary<string, Type>();
            foreach ( var prop in propertyNames )
            {
                AddProperty(prop.Item1, prop.Item2);
            }
        }
        #endregion Constructor

        #region Public Methods

        public void AddProperty<T>( string propertyName, T initialValue )
        {
            _dynamicProperties.Add(propertyName, initialValue);
            _dynamicPropertyTypes.Add(propertyName, typeof(T));
            this.RaisePropertyChanged(propertyName);

        }
        public void AddProperty<T>( string propertyName)
        {
            AddProperty(propertyName, typeof(T));
        }
        public void AddProperty( string propertyName, Type type)
        {
            _dynamicProperties.Add(propertyName, null);
            _dynamicPropertyTypes.Add(propertyName, type);
            this.RaisePropertyChanged(propertyName);
        }

        public void SetPropertyValue<T>(string propertyName, T raw)
        {
            if (!_dynamicProperties.ContainsKey(propertyName))
            {
                throw new ArgumentException(propertyName + " property does not exist on " + GetType().Name);
            }

            var converter = DynamicLens2INPC.CreateConverter<T>(raw.GetType(), _dynamicPropertyTypes[propertyName]);
            var value = converter(raw);

            if (!value.Equals(_dynamicProperties[propertyName]))
            {
                _dynamicProperties[propertyName] = (object) value;
                this.RaisePropertyChanged(propertyName);
            }
        }

        public object GetPropertyValue(string propertyName)
        {
            if (!_dynamicProperties.ContainsKey(propertyName))
            {
                throw new ArgumentException(propertyName + " property does not exist " + GetType().Name);
            }
            return _dynamicProperties.ContainsKey(propertyName) ? _dynamicProperties[propertyName] : null;
        }

        #endregion Public Methods


        DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)
        {
            return new ForwardingMetaObject(parameter, BindingRestrictions.Empty, this, DynamicObject,
                // B's meta-object needs to know where to find the instance of B it is operating on.
                // Assuming that an instance of A is passed to the 'parameter' expression
                // we get the corresponding instance of B by reading the "B" property.
                exprA => Expression.Property(exprA, nameof(DynamicObject))
            );
        }

    }

    public static class DynamicNotifyingObjectMixin
    {
        public static TRet RaiseAndSetIfChanged<TObj, TRet>(this TObj This, TRet newValue, ref TRet backingField, [CallerMemberName] string property = "")
            where TObj : DynamicNotifyingObject
        {

            if (EqualityComparer<TRet>.Default.Equals(newValue, backingField))
            {
                return newValue;
            }

            This.RaisePropertyChanging(property);
            backingField = newValue;
            This.RaisePropertyChanged(property);

            return newValue;
        }
    }
}

带有测试用例

using FluentAssertions;
using Xunit;

namespace Weingartner.Lens.Spec
{
    public class DynamicNotifyingObjectSpec
    {
        class Fixture : DynamicNotifyingObject
        {
            public Fixture (): 
                base()
            {
                this.AddProperty<string>("A");
                this.AddProperty<string>("B");
                this.SetPropertyValue("A", "AAA");
                this.SetPropertyValue("B", "BBB");
            }
        }

        [Fact]
        public void ShouldBeAbleToAddPropertiesLaterOn()
        {
            var ff = new Fixture();

            ff.AddProperty<string>("newProp");
            ff.AddProperty<string>("XXXX");

            dynamic f = ff;

            ff.SetPropertyValue("newProp", "CCC");

            ((object)(f.newProp)).Should().Be("CCC");
            f.XXXX = "XXXX";
            f.newProp = "DDD";
            ((object)(f.newProp)).Should().Be("DDD");
            ((object)(f.XXXX)).Should().Be("XXXX");
        }

        [Fact]
        public void ShouldGenerateNotificationOnPropertyChange()
        {
            var a = new string []{"A"};
            var b = new string []{"B"};
            object oa = null;
            object ob = null;

            var f = new Fixture();
            dynamic fd = f;

            f.PropertyChanged += (sender, ev) =>
            {
                dynamic s = sender;
                oa = s.A;
                ob = s.B;
            };

            oa.Should().Be(null);
            ob.Should().Be(null);

            fd.A = "A";

            oa.Should().Be("A");
            ob.Should().Be("BBB");

            fd.B = "B";

            oa.Should().Be("A");
            ob.Should().Be("B");
        }


    }
}

答案 1 :(得分:1)

另一种可能性就是使用这个库

https://github.com/remi/MetaObject

using System;
using System.Dynamic;

public class MyClass : Whatever, IDynamicMetaObjectProvider {

    // This 1 line is *ALL* you need to add support for all of the DynamicObject methods
    public DynamicMetaObject GetMetaObject(System.Linq.Expressions.Expression e)
        { return new MetaObject(e, this); }

    // Now, if you want to handle dynamic method calls, 
    // you can implement TryInvokeMember, just like you would in DynamicObject!
    public bool TryInvokeMember
       (InvokeMemberBinder binder, object[] args, out object result) {
        if (binder.Name.Contains("Cool")) {
            result = "You called a method with Cool in the name!";
            return true;
        } else {
            result = null;
            return false;
        }
    }
}

以及我的特定用例,它继承自ReactiveUI.Reactive对象并具有支持INPC生成的动态属性

using System;
using System.CodeDom;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using ReactiveUI;

namespace Weingartner.Lens
{
    /// <summary>
    /// An object you can add properties to at runtime which raises INPC events when those
    /// properties are changed.
    /// </summary>
    [DataContract]
    public class DynamicNotifyingObject : ReactiveObject, IDynamicMetaObjectProvider
    {
        #region Private Members

        [DataMember]
        private Dictionary<string, object> _DynamicProperties;
        [DataMember]
        private Dictionary<string, Type> _DynamicPropertyTypes;

        #endregion Private Members

        #region Constructor

        public DynamicNotifyingObject() : this(new Tuple<string,Type>[] { }) { }

        public DynamicNotifyingObject(IEnumerable<Tuple<string,Type>> propertyNames)
        {
            if (propertyNames == null)
            {
                throw new Exception("propertyNames is empty");
            }

            _DynamicProperties = new Dictionary<string, object>();
            _DynamicPropertyTypes = new Dictionary<string, Type>();
            foreach ( var prop in propertyNames )
            {
                AddProperty(prop.Item1, prop.Item2);
            }
        }
        #endregion Constructor

        public void AddProperty<T>( string propertyName, T initialValue )
        {
            _DynamicProperties.Add(propertyName, initialValue);
            _DynamicPropertyTypes.Add(propertyName, typeof(T));
            this.RaisePropertyChanged(propertyName);
        }

        /// <summary>
        /// Set the property. Will throw an exception if the property does not exist. 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="propertyName"></param>
        /// <param name="raw"></param>
        public void SetPropertyValue<T>(string propertyName, T raw)
        {
            if (!_DynamicProperties.ContainsKey(propertyName))
            {
                throw new ArgumentException(propertyName + " property does not exist on " + GetType().Name);
            }

            var converter = DynamicLens2INPC.CreateConverter<T>(raw.GetType(), _DynamicPropertyTypes[propertyName]);
            var value = converter(raw);

            if (!value.Equals(_DynamicProperties[propertyName]))
            {
                this.RaisePropertyChanging(propertyName);
                _DynamicProperties[propertyName] = (object) value;
                this.RaisePropertyChanged(propertyName);
            }
        }
        /// <summary>
        /// Get the property. Will throw an exception if the property does not exist.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        public object GetPropertyValue(string propertyName)
        {
            if (!_DynamicProperties.ContainsKey(propertyName))
            {
                throw new ArgumentException(propertyName + " property does not exist " + GetType().Name);
            }
            return _DynamicProperties.ContainsKey(propertyName) ? _DynamicProperties[propertyName] : null;
        }

        public bool HasDynamicProperty(string propertyName) => _DynamicProperties.ContainsKey(propertyName);

        DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression e) { return new MetaObject(e, this); }

        /// <summary>
        /// Support for MetaObject. See https://github.com/remi/MetaObject 
        /// </summary>
        /// <param name="binder"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if (HasDynamicProperty(binder.Name))
            {
                result = GetPropertyValue(binder.Name);
                return true;
            }

            // This path will return any real properties on the object
            result = null;
            return false;
        }

        /// <summary>
        /// Support for MetaObject. See https://github.com/remi/MetaObject
        /// </summary>
        /// <param name="binder"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public bool TrySetMember(SetMemberBinder binder, object value)
        {
            if (HasDynamicProperty(binder.Name))
            {
                SetPropertyValue(binder.Name, value);
                return true;
            }

            // This path will try to set any real properties on the object
            return false;
        }

    }
}