Skip to content

Commit 3292571

Browse files
author
Paul Betts
committed
Merge pull request #201 from reactiveui/rewrite-bindto
Rewrite BindTo to support Type Conversion and Binding Hooks
2 parents daa5841 + 08ce999 commit 3292571

File tree

2 files changed

+181
-80
lines changed

2 files changed

+181
-80
lines changed

ReactiveUI.Tests/PropertyBindingTest.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,21 @@ public void BindToShouldntInitiallySetToNull()
336336
Assert.Equal(vm.Model.AnotherThing, view.FakeControl.NullHatingString);
337337
}
338338

339+
[Fact]
340+
public void BindToTypeConversionSmokeTest()
341+
{
342+
var vm = new PropertyBindViewModel();
343+
var view = new PropertyBindView() {ViewModel = null};
344+
345+
view.WhenAny(x => x.ViewModel.JustADouble, x => x.Value)
346+
.BindTo(view, x => x.FakeControl.NullHatingString);
347+
348+
Assert.Equal("", view.FakeControl.NullHatingString);
349+
350+
view.ViewModel = vm;
351+
Assert.Equal(vm.JustADouble.ToString(), view.FakeControl.NullHatingString);
352+
}
353+
339354
void configureDummyServiceLocator()
340355
{
341356
var types = new Dictionary<Tuple<Type, string>, List<Type>>();

ReactiveUI/PropertyBinding.cs

Lines changed: 166 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,26 @@ public static IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
539539
{
540540
return binderImplementation.AsyncOneWayBind(viewModel, view, vmProperty, null, x => selector(x).ToObservable(), fallbackValue);
541541
}
542+
543+
/// <summary>
544+
/// BindTo takes an Observable stream and applies it to a target
545+
/// property. Conceptually it is similar to "Subscribe(x =&gt;
546+
/// target.property = x)", but allows you to use child properties
547+
/// without the null checks.
548+
/// </summary>
549+
/// <param name="target">The target object whose property will be set.</param>
550+
/// <param name="property">An expression representing the target
551+
/// property to set. This can be a child property (i.e. x.Foo.Bar.Baz).</param>
552+
/// <returns>An object that when disposed, disconnects the binding.</returns>
553+
public static IDisposable BindTo<TValue, TTarget, TTValue>(
554+
this IObservable<TValue> This,
555+
TTarget target,
556+
Expression<Func<TTarget, TTValue>> property,
557+
Func<TValue> fallbackValue = null,
558+
object conversionHint = null)
559+
{
560+
return binderImplementation.BindTo(This, target, property, fallbackValue);
561+
}
542562
}
543563

544564
/// <summary>
@@ -739,6 +759,23 @@ IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
739759
Func<TOut> fallbackValue = null)
740760
where TViewModel : class
741761
where TView : IViewFor;
762+
763+
/// <summary>
764+
/// BindTo takes an Observable stream and applies it to a target
765+
/// property. Conceptually it is similar to "Subscribe(x =&gt;
766+
/// target.property = x)", but allows you to use child properties
767+
/// without the null checks.
768+
/// </summary>
769+
/// <param name="target">The target object whose property will be set.</param>
770+
/// <param name="property">An expression representing the target
771+
/// property to set. This can be a child property (i.e. x.Foo.Bar.Baz).</param>
772+
/// <returns>An object that when disposed, disconnects the binding.</returns>
773+
IDisposable BindTo<TValue, TTarget, TTValue>(
774+
IObservable<TValue> This,
775+
TTarget target,
776+
Expression<Func<TTarget, TTValue>> property,
777+
Func<TValue> fallbackValue = null,
778+
object conversionHint = null);
742779
}
743780

744781
public class PropertyBinderImplementation : IPropertyBinderImplementation
@@ -870,7 +907,7 @@ public IDisposable Bind<TViewModel, TView, TVMProp, TVProp, TDontCare>(
870907
}
871908
});
872909

873-
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
910+
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.TwoWay);
874911
if (ret != null) return ret;
875912

876913
ret = changeWithValues.Subscribe(isVmWithLatestValue => {
@@ -944,6 +981,8 @@ public IDisposable OneWayBind<TViewModel, TView, TVMProp, TVProp>(
944981
{
945982
var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty);
946983
var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain));
984+
var source = default(IObservable<TVProp>);
985+
var fallbackWrapper = default(Func<TVProp>);
947986

948987
if (viewProperty == null) {
949988
var viewPropChain = Reflection.getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty));
@@ -955,16 +994,20 @@ public IDisposable OneWayBind<TViewModel, TView, TVMProp, TVProp>(
955994
throw new ArgumentException(String.Format("Can't convert {0} to {1}. To fix this, register a IBindingTypeConverter", typeof (TVMProp), viewType));
956995
}
957996

958-
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
997+
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay);
959998
if (ret != null) return ret;
960999

961-
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
1000+
source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
9621001
.SelectMany(x => {
9631002
object tmp;
964-
if (!converter.TryConvert(x, viewType, conversionHint, out tmp)) return Observable.Empty<object>();
965-
return Observable.Return(tmp);
966-
})
967-
.Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false));
1003+
if (!converter.TryConvert(x, viewType, conversionHint, out tmp)) return Observable.Empty<TVProp>();
1004+
return Observable.Return((TVProp)tmp);
1005+
});
1006+
1007+
fallbackWrapper = () => {
1008+
object tmp;
1009+
return converter.TryConvert(fallbackValue(), typeof(TVProp), conversionHint, out tmp) ? (TVProp)tmp : default(TVProp);
1010+
};
9681011
} else {
9691012
var converter = getConverterForTypes(typeof (TVMProp), typeof (TVProp));
9701013

@@ -974,20 +1017,23 @@ public IDisposable OneWayBind<TViewModel, TView, TVMProp, TVProp>(
9741017

9751018
var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty);
9761019

977-
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
1020+
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay);
9781021
if (ret != null) return ret;
9791022

980-
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
1023+
source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
9811024
.SelectMany(x => {
9821025
object tmp;
9831026
if (!converter.TryConvert(x, typeof(TVProp), conversionHint, out tmp)) return Observable.Empty<TVProp>();
984-
return Observable.Return(tmp == null ? default(TVProp) : (TVProp) tmp);
985-
})
986-
.BindTo(view, viewProperty, () => {
987-
object tmp;
988-
return converter.TryConvert(fallbackValue(), typeof(TVProp), conversionHint, out tmp) ? (TVProp)tmp : default(TVProp);
1027+
return Observable.Return(tmp == null ? default(TVProp) : (TVProp)tmp);
9891028
});
1029+
1030+
fallbackWrapper = () => {
1031+
object tmp;
1032+
return converter.TryConvert(fallbackValue(), typeof(TVProp), conversionHint, out tmp) ? (TVProp)tmp : default(TVProp);
1033+
};
9901034
}
1035+
1036+
return bindToDirect(source, view, viewProperty, fallbackWrapper);
9911037
}
9921038

9931039
/// <summary>
@@ -1040,25 +1086,24 @@ public IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
10401086
{
10411087
var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty);
10421088
var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain));
1089+
var source = default(IObservable<TOut>);
10431090

10441091
if (viewProperty == null) {
10451092
var viewPropChain = Reflection.getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty));
10461093

1047-
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
1094+
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay);
10481095
if (ret != null) return ret;
10491096

1050-
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
1051-
.Select(selector)
1052-
.Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false));
1097+
source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty).Select(selector);
10531098
} else {
10541099
var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty);
1055-
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
1100+
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay);
10561101
if (ret != null) return ret;
10571102

1058-
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
1059-
.Select(selector)
1060-
.BindTo(view, viewProperty, fallbackValue);
1103+
source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty).Select(selector);
10611104
}
1105+
1106+
return bindToDirect(source, view, viewProperty, fallbackValue);
10621107
}
10631108

10641109
/// <summary>
@@ -1117,45 +1162,120 @@ public IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
11171162
{
11181163
var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty);
11191164
var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain));
1165+
var source = default(IObservable<TOut>);
11201166

11211167
if (viewProperty == null) {
11221168
var viewPropChain = Reflection.getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty));
11231169

1124-
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
1170+
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.AsyncOneWay);
11251171
if (ret != null) return ret;
11261172

1127-
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
1128-
.SelectMany(selector)
1129-
.Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false));
1173+
source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty).SelectMany(selector);
11301174
} else {
11311175
var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty);
11321176

1133-
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
1177+
var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.AsyncOneWay);
11341178
if (ret != null) return ret;
11351179

1136-
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
1137-
.SelectMany(selector)
1138-
.BindTo(view, viewProperty, fallbackValue);
1180+
source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty).SelectMany(selector);
11391181
}
1182+
1183+
return bindToDirect(source, view, viewProperty, fallbackValue);
11401184
}
11411185

1142-
IDisposable evalBindingHooks<TViewModel, TView>(TViewModel viewModel, TView view, string[] vmPropChain, string[] viewPropChain)
1186+
public IDisposable BindTo<TValue, TTarget, TTValue>(
1187+
IObservable<TValue> This,
1188+
TTarget target,
1189+
Expression<Func<TTarget, TTValue>> property,
1190+
Func<TValue> fallbackValue = null,
1191+
object conversionHint = null)
1192+
{
1193+
var viewPropChain = Reflection.ExpressionToPropertyNames(property);
1194+
var ret = evalBindingHooks(This, target, null, viewPropChain, BindingDirection.OneWay);
1195+
if (ret != null) return ret;
1196+
1197+
var converter = getConverterForTypes(typeof (TValue), typeof(TTValue));
1198+
1199+
if (converter == null) {
1200+
throw new ArgumentException(String.Format("Can't convert {0} to {1}. To fix this, register a IBindingTypeConverter", typeof (TValue), typeof(TTValue)));
1201+
}
1202+
1203+
var source = This.SelectMany(x => {
1204+
object tmp;
1205+
if (!converter.TryConvert(x, typeof(TTValue), conversionHint, out tmp)) return Observable.Empty<TTValue>();
1206+
return Observable.Return(tmp == null ? default(TTValue) : (TTValue)tmp);
1207+
});
1208+
1209+
return bindToDirect(source, target, property, fallbackValue == null ? default(Func<TTValue>) : new Func<TTValue>(() => {
1210+
object tmp;
1211+
if (!converter.TryConvert(fallbackValue(), typeof(TTValue), conversionHint, out tmp)) return default(TTValue);
1212+
return tmp == null ? default(TTValue) : (TTValue)tmp;
1213+
}));
1214+
}
1215+
1216+
IDisposable bindToDirect<TTarget, TValue>(
1217+
IObservable<TValue> This,
1218+
TTarget target,
1219+
Expression<Func<TTarget, TValue>> property,
1220+
Func<TValue> fallbackValue = null)
1221+
{
1222+
var types = new[] { typeof(TTarget) }.Concat(Reflection.ExpressionToPropertyTypes(property)).ToArray();
1223+
var names = Reflection.ExpressionToPropertyNames(property);
1224+
1225+
var setter = Reflection.GetValueSetterOrThrow(types.Reverse().Skip(1).First(), names.Last());
1226+
if (names.Length == 1) {
1227+
return This.Subscribe(
1228+
x => setter(target, x),
1229+
ex => {
1230+
this.Log().ErrorException("Binding recieved an Exception!", ex);
1231+
if (fallbackValue != null) setter(target, fallbackValue());
1232+
});
1233+
}
1234+
1235+
var bindInfo = Observable.CombineLatest(
1236+
This, target.WhenAnyDynamic(names.SkipLast(1).ToArray(), x => x.Value),
1237+
(val, host) => new { val, host });
1238+
1239+
return bindInfo
1240+
.Where(x => x.host != null)
1241+
.Subscribe(
1242+
x => setter(x.host, x.val),
1243+
ex => {
1244+
this.Log().ErrorException("Binding recieved an Exception!", ex);
1245+
if (fallbackValue != null) setter(target, fallbackValue());
1246+
});
1247+
}
1248+
1249+
IDisposable evalBindingHooks<TViewModel, TView>(TViewModel viewModel, TView view, string[] vmPropChain, string[] viewPropChain, BindingDirection direction)
11431250
where TViewModel : class
1144-
where TView : IViewFor
11451251
{
11461252
var hooks = RxApp.GetAllServices<IPropertyBindingHook>();
1147-
var vmFetcher = new Func<IObservedChange<object, object>[]>(() => {
1148-
IObservedChange<object, object>[] fetchedValues;
1149-
Reflection.TryGetAllValuesForPropertyChain(out fetchedValues, viewModel, vmPropChain);
1150-
return fetchedValues;
1151-
});
1253+
1254+
var vmFetcher = default(Func<IObservedChange<object, object>[]>);
1255+
if (vmPropChain != null) {
1256+
vmFetcher = () => {
1257+
IObservedChange<object, object>[] fetchedValues;
1258+
Reflection.TryGetAllValuesForPropertyChain(out fetchedValues, viewModel, vmPropChain);
1259+
return fetchedValues;
1260+
};
1261+
} else {
1262+
vmFetcher = () => {
1263+
return new[] {
1264+
new ObservedChange<object, object>() {
1265+
Sender = null, PropertyName = null, Value = viewModel,
1266+
}
1267+
};
1268+
};
1269+
}
1270+
11521271
var vFetcher = new Func<IObservedChange<object, object>[]>(() => {
11531272
IObservedChange<object, object>[] fetchedValues;
11541273
Reflection.TryGetAllValuesForPropertyChain(out fetchedValues, view, viewPropChain);
11551274
return fetchedValues;
11561275
});
1276+
11571277
var shouldBind = hooks.Aggregate(true, (acc, x) =>
1158-
acc && x.ExecuteHook(viewModel, view, vmFetcher, vFetcher, BindingDirection.TwoWay));
1278+
acc && x.ExecuteHook(viewModel, view, vmFetcher, vFetcher, direction));
11591279

11601280
if (!shouldBind) {
11611281
var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain));
@@ -1167,56 +1287,22 @@ IDisposable evalBindingHooks<TViewModel, TView>(TViewModel viewModel, TView view
11671287
return null;
11681288
}
11691289

1170-
11711290
MemoizingMRUCache<Tuple<Type, Type>, IBindingTypeConverter> typeConverterCache = new MemoizingMRUCache<Tuple<Type, Type>, IBindingTypeConverter>(
1172-
(types, _) =>
1173-
RxApp.GetAllServices<IBindingTypeConverter>()
1174-
.Aggregate(Tuple.Create(-1, default(IBindingTypeConverter)), (acc, x) => {
1291+
(types, _) => {
1292+
return RxApp.GetAllServices<IBindingTypeConverter>()
1293+
.Aggregate(Tuple.Create(-1, default(IBindingTypeConverter)), (acc, x) =>
1294+
{
11751295
var score = x.GetAffinityForObjects(types.Item1, types.Item2);
1176-
return score > acc.Item1 && score > 0 ?
1296+
return score > acc.Item1 && score > 0 ?
11771297
Tuple.Create(score, x) : acc;
1178-
}).Item2
1179-
, 25);
1298+
}).Item2;
1299+
}, 25);
11801300

1181-
IBindingTypeConverter getConverterForTypes(Type lhs, Type rhs)
1301+
internal IBindingTypeConverter getConverterForTypes(Type lhs, Type rhs)
11821302
{
11831303
lock (typeConverterCache) {
11841304
return typeConverterCache.Get(Tuple.Create(lhs, rhs));
11851305
}
11861306
}
11871307
}
1188-
1189-
public static class ObservableBindingMixins
1190-
{
1191-
/// <summary>
1192-
/// BindTo takes an Observable stream and applies it to a target
1193-
/// property. Conceptually it is similar to "Subscribe(x =&gt;
1194-
/// target.property = x)", but allows you to use child properties
1195-
/// without the null checks.
1196-
/// </summary>
1197-
/// <param name="target">The target object whose property will be set.</param>
1198-
/// <param name="property">An expression representing the target
1199-
/// property to set. This can be a child property (i.e. x.Foo.Bar.Baz).</param>
1200-
/// <returns>An object that when disposed, disconnects the binding.</returns>
1201-
public static IDisposable BindTo<TTarget, TValue>(
1202-
this IObservable<TValue> This,
1203-
TTarget target,
1204-
Expression<Func<TTarget, TValue>> property,
1205-
Func<TValue> fallbackValue = null)
1206-
{
1207-
var pn = Reflection.ExpressionToPropertyNames(property);
1208-
var bn = pn.Take(pn.Length - 1);
1209-
1210-
var lastValue = default(TValue);
1211-
1212-
var o = target.SubscribeToExpressionChain<TTarget, object>(bn, false, true)
1213-
.Select(x => lastValue);
1214-
1215-
return Observable.Merge(o, This)
1216-
.Subscribe(x => {
1217-
lastValue = x;
1218-
Reflection.SetValueToPropertyChain(target, pn, x);
1219-
});
1220-
}
1221-
}
12221308
}

0 commit comments

Comments
 (0)