Search code examples
c#data-bindingstatic-reflection

C# Binding data objects


I need binding between two similar objects (C#):

public class TypeA
{
     public int I;
     public string S;
}

public class TypeB
{
     public IntField I;
     public StringField S;
}

When a field in TypeA changes I need to update the matching field in TypeB.

IntField is an object that has a Value field of int type, so that updating TypeB can be written as:

bInstance.I.Value = aInstance.I;

If I understand correctly, if I use INotifyPropertyChanged in order to bind TypeB to TypeA, it'll cause boilerplate:

aInstance.PropertyChanged += (sender, args) =>
{
    if (args.PropertyName == "I")
        this.I.Value = sender.I;
    if (args.PropertyName == "S")
        this.S.Value = sender.S;
};

Also:

  • I have access to the code in both types, and I'd rather not change TypeB.
  • I have ~15 pairs of types like TypeA and TypeB - I'd like to avoid boilerplate.
  • Performance is very important, so reflection is not a preferred option.
  • Perhaps static reflection is an option? I've heard about it, but I'm not sure about:
    • How to use it without boilerplate.
    • Its performance.
    • Using it for different instances of pairs of the same type (i.e. a1Instance->b1Instance, a2Intance->b2Instance, etc.).

Edit:

IntField is a class. It's used for another type of data binding that exists in the system (complex, and the entire system relies on this). It inherits from a class that represents a general bindable field. here's part of it:

public class IntField : GeneralField
{
    private int _value;
    public int Value
    {
        get { return _value; }
        set
        {
            IsDirty = true;
            _value = value;
        }
    }

    // ... a couple of abstract method implementations go here (setting _value, and getting value in a non-type specific way)
}

Solution

  • If you don't want lots of manual coding, something reflection-based or meta-programming-based is going to be your best bet. For example:

    static void Entwine(INotifyPropertyChanged source, object target)
    {
        source.PropertyChanged += (sender,args) =>
        {
            var prop = target.GetType().GetProperty(args.PropertyName);
            if(prop != null)
            {
                var field = prop.GetValue(target) as GeneralField;
                if(field != null)
                {
                    var newVal = source.GetType().GetProperty(args.PropertyName)
                                       .GetValue(source);
                    field.SetValue(newVal); // <=== some method on GeneralField
                }
            }
        };
    }
    

    In many cases this will be fine, but if the reflection is genuinely a problem, tools like FastMember can help:

    static void Entwine(INotifyPropertyChanged source, object target)
    {
        var sourceAccessor = ObjectAccessor.Create(source);
        var targetAccessor = ObjectAccessor.Create(target);
        source.PropertyChanged += (sender, args) =>
        {
            var field = targetAccessor[args.PropertyName] as GeneralField;
            if (field != null)
            {
                var newVal = sourceAccessor[args.PropertyName];
                field.SetValue(newVal);
            }
        };
    }
    

    This is significantly faster than reflection - it uses a lot of tricks to avoid pain. That just leaves the need for something like:

    abstract class GeneralField
    {
        // ...
        public abstract void SetValue(object value);
    }
    class Int32Field : GeneralField
    {
        // ...
        public override void SetValue(object value)
        {
            Value = (int)value;
        }
    }
    

    And of course your INotifyPropertyChanged implementation, for example:

    public class TypeA : INotifyPropertyChanged
    {
        private int i;
        private string s;
        public int I
        {
            get { return i; }
            set { SetField(ref i, value); }
        }
        public string S
        {
            get { return s; }
            set { SetField(ref s, value); }
        }
        private void SetField<T>(ref T field, T value,
            [CallerMemberName]string propertyName = null)
        {
            if (!EqualityComparer<T>.Default.Equals(field, value))
            {
                field = value;
                var handler = PropertyChanged;
                if (handler != null) handler(
                    this, new PropertyChangedEventArgs(propertyName));
            }
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
    }