M <-> VM をBindするのにプロパティ一個一個 ToReactiveProperty()
していくのが面倒くさい
特に INotifyPropertyChanged を実装できない PlainObject の場合。
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;
}
}
Refletion と Expressionを駆使してViewModel のプロパティに ReactiveProperty.FromObject()
の呼び出しを代入していく。
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);
}
}
注: FromObjectメソッドを取得するのにパラメータの数の一致だけ見てるけど、オーバーロードはパラメータの数ではなくてパラメータの型をチェックするはずなので破綻する可能性がある。
既にありがちなんだけどまあ Expression の練習と思って。
オブジェクト構築のコストなんぞ、知らん。