Initializing ReactiveProperty is troublesome, so try initializing all at once (Plain Object version)

1 minute read

M <-> It is troublesome to do ToReactiveProperty () one by one to bind the VM.

Especially for PlainObjects that cannot implement INotifyPropertyChanged.

    public class ReactiveBinder<TSource, TTarget>
    {
        private readonly static Dictionary<string, Action<TSource, TTarget>> binderActions = new Dictionary<string, Action<TSource, TTarget>>();

        static ReactiveBinder()
        {
            BuildBinders();
        }

        public void Bind(TSource source, TTarget target)
        {
            foreach(var action in binderActions)
            {
                action.Value.Invoke(source, target);
            }
        }
        
        private static void BuildBinders()
        {
            var targetType = typeof(TTarget);
            var sourceType = typeof(TSource);
            MethodInfo fromObject = GetBinderMethod(targetType);

            var sourceParameter = Expression.Parameter(sourceType, "$source");
            var targetParameter = Expression.Parameter(targetType, "$destination");
            var modeParameter = Expression.Constant(ReactivePropertyMode.Default);
            var ignoreValidationErrorValue = Expression.Constant(false);

            foreach (var property in targetType.GetProperties())
            {
                var sourceProperty = sourceType.GetProperty(property.Name);
                var sourcePropertyAccessor = Expression.Property(
                    Expression.Convert(sourceParameter, sourceType),
                    property.Name);

                var propertySelector = Expression.Lambda(sourcePropertyAccessor, sourceParameter);

                var genericMethod = fromObject.MakeGenericMethod(new Type[] { sourceType, sourceProperty.PropertyType });
                var fromObjectCall = Expression.Call(genericMethod, sourceParameter, propertySelector, modeParameter, ignoreValidationErrorValue);

                var bindingProperty = Expression.Property(
                    targetParameter,
                    property);

                var assignment = Expression.Assign(bindingProperty, fromObjectCall);

                var lambda = Expression.Lambda<Action<TSource, TTarget>>(
                    assignment,
                    sourceParameter,
                    targetParameter
                    );

                binderActions.Add(property.Name, lambda.Compile());
            }
        }
        private static MethodInfo GetBinderMethod(Type targetType)
        {
            MethodInfo fromObject = null;
            Type[] paramTypes = new Type[] {
                targetType,
                typeof(int),
                typeof(ReactivePropertyMode),
                typeof(bool)
            };
            var flags = BindingFlags.Static | BindingFlags.Public;
            foreach (MethodInfo method in typeof(ReactiveProperty).GetMethods(flags))
            {

                if (method.IsGenericMethod && method.IsGenericMethodDefinition && method.ContainsGenericParameters)
                {
                    if (method.Name == "FromObject" && method.GetParameters().Length == paramTypes.Length)
                    {
                        fromObject = method;
                        break;
                    }
                }
            }
            return fromObject;
        }
    }

By making full use of Refletion and Expression, assign the call of ReactiveProperty.FromObject () to the property of ViewModel.

Example

Model

    public class SampleModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime? UpdateDateTime { get; set; }
        public bool Flag { get; set; }
    }

ViewModel

    public class SampleItemViewModel : BindableBase, INotifyPropertyChanged
    {

        public ReactiveProperty<int> Id { get; set; }
        public ReactiveProperty<string> Name { get; set; }
        public ReactiveProperty<DateTime?> UpdateDateTime { get; set; }
        public ReactiveProperty<bool> Flag { get; set; }

        public SampleItemViewModel()
        {

        }

        public SampleItemViewModel(SampleModel model)
        {
            var binder = new ReactiveBinder<SampleModel, SampleItemViewModel>();
            binder.Bind(model, this);
        }
    }

Note: I’m only looking at matching the number of parameters to get the FromObject method, but the overload should check the type of the parameter, not the number of the parameter, so it can break.

It’s already common, but I think it’s an expression practice.
I don’t know the cost of building an object.