/ C#

Bindings

As was described in Architecture Overview - Bindings is the way to connect view and controller without using direct access. It's useful when you need different views for same controller(context) in several parts of your application. How it works described in a picture below: bindings Binding will connect both parts in runtime when view will be created, but we need to know what exactly needs to be connected. Best way is to use some data, containing names of members and references if it's possible. Also we can define the way to bind was descibed in previous article:

  • Model To View
  • View To Model
  • Two Way
    We can use enum for that:
    /// <summary>
    ///     Defines way how bindings will affect each other.
    /// </summary>
    [Flags]
    public enum BindingMethod : byte
    {
        /// <summary>
        ///     Source changes will affect destination.
        /// </summary>
        SourceToDestination = 0b00000001,
        /// <summary>
        ///     Destination changes will affect source.
        /// </summary>
        DestinationToSource = 0b00000010,
        /// <summary>
        ///     Changes in destination and source will affect each other.
        /// </summary>
        TwoWay = SourceToDestination | DestinationToSource
    }

It defined as flags attribute cause of it's usage. As example we must change value of Destination in SourceToDestination and TwoWay bindings, so in code we'll check for a SourceToDestination flag.
Another class needed to design is: BindingTarget. It represents a container to assess property at runtime. Cause of all possible usages we must provide possibility to bind to:

  • Fields
  • Properies
  • Methods

Also fields and properties can be an ArrayTypes, so we need to contemplate that. GetValue() and SetValue() methods must be provided as well. Also we can use GetValue() for methods with return value. We'll don't gonna provide indexed properties bindings cause indexed properties used to access children contexts and binding to methods with more than one argument.
Let's code:

        public static MemberTarget Create(MemberInfo member, string memberName)
        {
            if (member is FieldInfo)
                return new FieldTarget((FieldInfo) member, memberName);
            if (member is PropertyInfo)
                return new PropertyTarget((PropertyInfo)member, memberName);
            if (member is MethodInfo)
                return new MethodTarget((MethodInfo)member, memberName);
            throw new NotSupportedException("Can't bind to non filed, property or method types.");
        }

        public abstract object GetValue(object target);
        public abstract void SetValue(object target, object value);

This class is base for a different binding target types, also provides factory method to create bunch of target instances based on member info. Additionally we need to make three devided classes: FieldTarget, PropertyTarget and MethodTarget. There they are:

    internal class FieldTarget : MemberTarget
    {
        private readonly FieldInfo _field;
        private readonly bool _isArray;
        private readonly int _index = -1;
        private object _tmp;

        public bool CanSet { get; private set; }

        public FieldTarget(FieldInfo field, string memberName)
        {
            _field = field;
            _isArray = typeof(IList).IsAssignableFrom(_field.FieldType);

            CanSet = _isArray || !_field.IsInitOnly;

            var match = new Regex(BindingConstants.INDEXER_REGEX).Match(memberName);

            if (match.Success && !_isArray) throw new ArgumentException("Indexed access to non array type");
            if (!match.Success && _isArray) throw new ArgumentException("Indexer not specified for array type.");

            if (match.Success && _isArray)
                _index = int.Parse(match.Value);
        }

        public override object GetValue(object target)
        {
            if (!_isArray) return _field.GetValue(target);
            _tmp = _field.GetValue(target);
            return ((IList) _tmp)[_index];
        }

        public override void SetValue(object target, object value)
        {
            if (!_isArray)
            {
                _field.SetValue(target, value);
                return;
            }
            _tmp = _field.GetValue(target);
            ((IList) _tmp)[_index] = value;
        }
    }
    internal class PropertyTarget : MemberTarget
    {
        private readonly PropertyInfo _property;
        private readonly bool _isArray;
        private readonly int _index = -1;
        private object _tmp;

        public bool CanGet { get; private set; }
        public bool CanSet { get; private set; }

        public PropertyTarget(PropertyInfo property, string memberName)
        {
            _property = property;

            CanGet = property.CanRead;
            CanSet = property.CanWrite;

            _isArray = typeof(IList).IsAssignableFrom(_property.PropertyType);
            
            var match = new Regex(BindingConstants.INDEXER_REGEX).Match(memberName);

            if (match.Success && !_isArray) throw new ArgumentException("Indexed access to non array type");
            if (!match.Success && _isArray) throw new ArgumentException("Indexer not specified for array type.");

            if (match.Success && _isArray)
                _index = int.Parse(match.Value);
        }

        public override object GetValue(object target)
        {
            if(!CanGet) throw new MemberAccessException("Property not allowed to be get.");
            if (!_isArray) return _property.GetValue(target);
            _tmp = _property.GetValue(target);
            return ((IList)_tmp)[_index];
        }

        public override void SetValue(object target, object value)
        {
            if (!CanSet) throw new MemberAccessException("Property not allowed to be set.");
            if (!_isArray) _property.SetValue(target, value);
            _tmp = _property.GetValue(target);
            ((IList)_tmp)[_index] = value;
        }
    }
    internal class MethodTarget : MemberTarget
    {
        private readonly MethodInfo _method;
        private readonly bool _isArray;
        private readonly int _index = -1;
        private object _tmp;

        public bool CanGet { get; private set; }
        public bool CanSet { get; private set; }

        public MethodTarget(MethodInfo method, string memberName)
        {
            _method = method;

            if (_method.ReturnType == typeof(void))
            {
                if(_method.GetParameters().Length != 1) throw new ArgumentException("Binding to method with parameters count not equals to 1.");
                CanGet = false;
                CanSet = true;
            }
            else
            {
                if (_method.GetParameters().Length > 0) throw new ArgumentException("Binding to method with return type and parameters.");
                _isArray = typeof(IList).IsAssignableFrom(_method.ReturnType);
                if (!_isArray)
                {
                    CanGet = true;
                    CanSet = false;
                }
                else
                {
                    var match = new Regex(BindingConstants.INDEXER_REGEX).Match(memberName);

                    if (match.Success && !_isArray) throw new ArgumentException("Indexed access to non array type.");
                    if (!match.Success && _isArray)
                        throw new ArgumentException("Indexer not specified for array type.");

                    _index = int.Parse(match.Value);
                    CanGet = true;
                    CanGet = true;
                }
            }

        }

        public override object GetValue(object target)
        {
            if (!CanGet) throw new MemberAccessException("Property not allowed to be get.");
            if (!_isArray) return _method.Invoke(target, null);
            _tmp = _method.Invoke(target, null);
            return ((IList)_tmp)[_index];
        }

        public override void SetValue(object target, object value)
        {
            if (!CanSet) throw new MemberAccessException("Property not allowed to be set.");
            if (!_isArray) _method.Invoke(target, new [] {value});
            _tmp = _method.Invoke(target, null);
            ((IList)_tmp)[_index] = value;
        }
    }

With these implementation we can bind to members with such signatures as:

  • T Field - read, write
  • readonly T Field - read
  • IList Field - read, write elements (possible index out of range)
  • readonly IList Field - read, write elements (possible index out of range)
  • T Properties (get; set;) - read, write
  • T Properties (get;) - read
  • T Properties (set;) - write
  • IList Properties (get; set;) - read, write elements (possible index out of range)
  • IList Properties (get;) - read, write elements (possible index out of range)
  • void Method(T value) - write
  • T Method() - read
  • IList Method() - read, write elements (possible index out of range)

Also i want to provide a way to make bindings like this field[0].innerField.method() and in any different order, so we need to make it like a linked list structure. I'll use simple array and wrap all things to binding target:

    internal class BindingTarget
    {
        private readonly object _target;
        private readonly string[] _members;
        private MemberTarget[] _membersTree;
        private object _targetTmp;

        public BindingTarget(object target, string member)
        {
            if (target == null) throw new ArgumentException("Target must not be null.");
            if(string.IsNullOrEmpty(member)) throw new ArgumentException("Member must not be null or empty.");

            _target = target;
            _members = member.Split(new[] {"."}, StringSplitOptions.None);
            if(!_members.All(ValidateMemberPath)) throw new ArgumentException("Member specified incorrectly.");
        }

        public void Bind()
        {
            _membersTree = new MemberTarget[_members.Length];
            for (int i = 0; i < _members.Length; i++)
            {
                var member = _target.GetType().GetMember(new Regex(BindingConstants.PROPERY_REGEX).Match(_members[i]).Value)[0];
                if(member == null) throw new MissingMemberException("Member with specified name not found.");

                _membersTree[i] = MemberTarget.Create(member, _members[i]);
            }
        }

        public void Unbind()
        {
            _membersTree = null;
        }

        public object GetValue()
        {
            _targetTmp = _target;
            for (int i = 0; i < _membersTree.Length; i++)
            {
                if (i + 1 == _membersTree.Length) return _membersTree[i].GetValue(_targetTmp);
                _targetTmp = _membersTree[i].GetValue(_targetTmp);
            }
            throw new Exception("Something went wrong!");
        }

        public void SetValue(object value)
        {
            _targetTmp = _target;
            for (int i = 0; i < _membersTree.Length; i++)
            {
                if (i + 1 == _membersTree.Length) _membersTree[i].SetValue(_targetTmp, value);
                _targetTmp = _membersTree[i].GetValue(_targetTmp);
            }
        }

        private bool ValidateMemberPath(string member)
        {
            return new Regex(
                string.Format(
                    "({0}|{1}|{2})", 
                    BindingConstants.ARRAY_REGEX, 
                    BindingConstants.METHOD_REGEX, 
                    BindingConstants.PROPERY_REGEX))
                .Match(member)
                .Success;
        }
    }

This class presents an single binding entity we want bind to. It's also internal and will be used in a Binding class two times to describe Source and Destination parts of binding.

    internal class BindingConstants
    {
        internal const string PROPERY_REGEX = @"[a-zA-Z0-9]\w+";
        internal const string ARRAY_REGEX = @"[a-zA-Z0-9]\w+\[[0-9]+\]";
        internal const string METHOD_REGEX = @"[a-zA-Z0-9]\w+\(\)";

        internal const string INDEXER_REGEX = @"[^\[]\d+(?=\])";

        internal const BindingFlags MEMBER_FLAGS = BindingFlags.Instance | BindingFlags.Public;
    }

As you can see above constants are used with regex to check members signatures.
And now it's time to make public accessible binding class which will aggregate all logic you seen above. It's functionality quite simple and hides all that scary stuff written prevoiusly. So, Binding class:

    /// <summary>
    ///     Provides possibility to bind one instance member to another.
    /// </summary>
    public class Binding
    {
        private readonly BindingMethod _method;
        private readonly bool _sourceFirst;

        private BindingTarget _source;
        private BindingTarget _target;
        
        /// <summary>
        ///     Creates new instance of binding.
        /// </summary>
        /// <param name="source"> Source object to bind. </param>
        /// <param name="sourceMember"> Source member path to bind. </param>
        /// <param name="target"> Target object to bind. </param>
        /// <param name="targetMember"> Target member path to bind. </param>
        /// <param name="bindingMethod"> Update method. <see cref="BindingMethod"/></param>
        /// <param name="sourceFirst"> Controls ordrer in case of <see cref="BindingMethod.TwoWay"/> binding of sides overwriting values. </param>
        public Binding(object source, string sourceMember, object target, string targetMember, BindingMethod bindingMethod = BindingMethod.TwoWay, bool sourceFirst = true)
        {
            _source = new BindingTarget(source, sourceMember);
            _target = new BindingTarget(target, targetMember);

            _method = bindingMethod;
            _sourceFirst = sourceFirst;
        }

        /// <summary>
        ///     Binds target and source to each other.
        /// </summary>
        public void Bind()
        {
            _source.Bind();
            _target.Bind();
        }

        /// <summary>
        ///     Unbinds target and source from each other.
        /// </summary>
        public void Unbind()
        {
            _source.Unbind();
            _target.Unbind();
        }

        /// <summary>
        ///     Syncronizes values of members.
        /// </summary>
        public void Update()
        {
            if(_source == null || _target == null) throw new NullReferenceException("Binding not stablished.");

            switch (_method)
            {
                case BindingMethod.SourceToDestination:
                    _target.SetValue(_source.GetValue());
                    break;
                case BindingMethod.DestinationToSource:
                    _source.SetValue(_target.GetValue());
                    break;
                case BindingMethod.TwoWay:
                    if (_sourceFirst)
                    {
                        _target.SetValue(_source.GetValue());
                        _source.SetValue(_target.GetValue());
                    }
                    else
                    {
                        _source.SetValue(_target.GetValue());
                        _target.SetValue(_source.GetValue());
                    }
                    break;
            }
        }
    }

This common solution will allow us to do some cool things. Let me explain in an example:

        public Example()
        {
            var d1 = new Data();
            var d2 = new Data();
            var binding = new Binding(d1, "Field", d2, "Field");
            binding.Bind();
            d1.Field = "Value1";
            binding.Update();
            // now d1's and d2's Field value equals to "Value1"
            d2.Field = "Value2";
            binding.Update();
            // now d1's and d2's Field value equals to "Value2"
        }

        class Data
        {
            public string Field;
        }

It's a little bit too much code in example, but keep in mind that most of it will be called not directy and the only thing we need to do, is specify which members to bind and how they will bind. Unity3D example straight ahead.

Bindings
Share this