From a893f1a45d9f414dbfd21236d73d4b090999135f Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Sat, 2 Mar 2013 16:57:12 -0800 Subject: [PATCH 1/6] Move BindTo into IPropertyBinderImplementation --- ReactiveUI/PropertyBinding.cs | 83 ++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/ReactiveUI/PropertyBinding.cs b/ReactiveUI/PropertyBinding.cs index be3ea72e15..f39a78961c 100644 --- a/ReactiveUI/PropertyBinding.cs +++ b/ReactiveUI/PropertyBinding.cs @@ -539,6 +539,25 @@ public static IDisposable AsyncOneWayBind( { return binderImplementation.AsyncOneWayBind(viewModel, view, vmProperty, null, x => selector(x).ToObservable(), fallbackValue); } + + /// + /// BindTo takes an Observable stream and applies it to a target + /// property. Conceptually it is similar to "Subscribe(x => + /// target.property = x)", but allows you to use child properties + /// without the null checks. + /// + /// The target object whose property will be set. + /// An expression representing the target + /// property to set. This can be a child property (i.e. x.Foo.Bar.Baz). + /// An object that when disposed, disconnects the binding. + public static IDisposable BindTo( + this IObservable This, + TTarget target, + Expression> property, + Func fallbackValue = null) + { + return binderImplementation.BindTo(This, target, property, fallbackValue); + } } /// @@ -739,6 +758,22 @@ IDisposable AsyncOneWayBind( Func fallbackValue = null) where TViewModel : class where TView : IViewFor; + + /// + /// BindTo takes an Observable stream and applies it to a target + /// property. Conceptually it is similar to "Subscribe(x => + /// target.property = x)", but allows you to use child properties + /// without the null checks. + /// + /// The target object whose property will be set. + /// An expression representing the target + /// property to set. This can be a child property (i.e. x.Foo.Bar.Baz). + /// An object that when disposed, disconnects the binding. + IDisposable BindTo( + IObservable This, + TTarget target, + Expression> property, + Func fallbackValue = null); } public class PropertyBinderImplementation : IPropertyBinderImplementation @@ -1139,21 +1174,33 @@ public IDisposable AsyncOneWayBind( } } + public IDisposable BindTo( + IObservable This, + TTarget target, + Expression> property, + Func fallbackValue = null) + { + throw new NotImplementedException(); + } + IDisposable evalBindingHooks(TViewModel viewModel, TView view, string[] vmPropChain, string[] viewPropChain) where TViewModel : class where TView : IViewFor { var hooks = RxApp.GetAllServices(); + var vmFetcher = new Func[]>(() => { IObservedChange[] fetchedValues; Reflection.TryGetAllValuesForPropertyChain(out fetchedValues, viewModel, vmPropChain); return fetchedValues; }); + var vFetcher = new Func[]>(() => { IObservedChange[] fetchedValues; Reflection.TryGetAllValuesForPropertyChain(out fetchedValues, view, viewPropChain); return fetchedValues; }); + var shouldBind = hooks.Aggregate(true, (acc, x) => acc && x.ExecuteHook(viewModel, view, vmFetcher, vFetcher, BindingDirection.TwoWay)); @@ -1178,45 +1225,11 @@ IDisposable evalBindingHooks(TViewModel viewModel, TView view }).Item2 , 25); - IBindingTypeConverter getConverterForTypes(Type lhs, Type rhs) + internal IBindingTypeConverter getConverterForTypes(Type lhs, Type rhs) { lock (typeConverterCache) { return typeConverterCache.Get(Tuple.Create(lhs, rhs)); } } } - - public static class ObservableBindingMixins - { - /// - /// BindTo takes an Observable stream and applies it to a target - /// property. Conceptually it is similar to "Subscribe(x => - /// target.property = x)", but allows you to use child properties - /// without the null checks. - /// - /// The target object whose property will be set. - /// An expression representing the target - /// property to set. This can be a child property (i.e. x.Foo.Bar.Baz). - /// An object that when disposed, disconnects the binding. - public static IDisposable BindTo( - this IObservable This, - TTarget target, - Expression> property, - Func fallbackValue = null) - { - var pn = Reflection.ExpressionToPropertyNames(property); - var bn = pn.Take(pn.Length - 1); - - var lastValue = default(TValue); - - var o = target.SubscribeToExpressionChain(bn, false, true) - .Select(x => lastValue); - - return Observable.Merge(o, This) - .Subscribe(x => { - lastValue = x; - Reflection.SetValueToPropertyChain(target, pn, x); - }); - } - } } \ No newline at end of file From 32a8348581b0d2461018f0636ff880e303875e50 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Sat, 2 Mar 2013 17:08:20 -0800 Subject: [PATCH 2/6] Correctly pass BindingDirection to hooks --- ReactiveUI/PropertyBinding.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ReactiveUI/PropertyBinding.cs b/ReactiveUI/PropertyBinding.cs index f39a78961c..13253b347e 100644 --- a/ReactiveUI/PropertyBinding.cs +++ b/ReactiveUI/PropertyBinding.cs @@ -905,7 +905,7 @@ public IDisposable Bind( } }); - var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain); + var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.TwoWay); if (ret != null) return ret; ret = changeWithValues.Subscribe(isVmWithLatestValue => { @@ -990,7 +990,7 @@ public IDisposable OneWayBind( throw new ArgumentException(String.Format("Can't convert {0} to {1}. To fix this, register a IBindingTypeConverter", typeof (TVMProp), viewType)); } - var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain); + var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay); if (ret != null) return ret; return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) @@ -1009,7 +1009,7 @@ public IDisposable OneWayBind( var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty); - var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain); + var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay); if (ret != null) return ret; return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) @@ -1079,7 +1079,7 @@ public IDisposable OneWayBind( if (viewProperty == null) { var viewPropChain = Reflection.getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty)); - var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain); + var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay); if (ret != null) return ret; return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) @@ -1087,7 +1087,7 @@ public IDisposable OneWayBind( .Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false)); } else { var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty); - var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain); + var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay); if (ret != null) return ret; return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) @@ -1156,7 +1156,7 @@ public IDisposable AsyncOneWayBind( if (viewProperty == null) { var viewPropChain = Reflection.getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty)); - var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain); + var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.AsyncOneWay); if (ret != null) return ret; return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) @@ -1165,7 +1165,7 @@ public IDisposable AsyncOneWayBind( } else { var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty); - var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain); + var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.AsyncOneWay); if (ret != null) return ret; return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) @@ -1202,7 +1202,7 @@ IDisposable evalBindingHooks(TViewModel viewModel, TView view }); var shouldBind = hooks.Aggregate(true, (acc, x) => - acc && x.ExecuteHook(viewModel, view, vmFetcher, vFetcher, BindingDirection.TwoWay)); + acc && x.ExecuteHook(viewModel, view, vmFetcher, vFetcher, direction)); if (!shouldBind) { var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain)); From 509717351c965c3ab224e34597d06e8e19c83592 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Sat, 2 Mar 2013 17:08:56 -0800 Subject: [PATCH 3/6] Make evalBindingHooks compatible with ViewModel-less hooks by faking it --- ReactiveUI/PropertyBinding.cs | 38 ++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/ReactiveUI/PropertyBinding.cs b/ReactiveUI/PropertyBinding.cs index 13253b347e..7a574e5977 100644 --- a/ReactiveUI/PropertyBinding.cs +++ b/ReactiveUI/PropertyBinding.cs @@ -1180,20 +1180,30 @@ public IDisposable BindTo( Expression> property, Func fallbackValue = null) { - throw new NotImplementedException(); } - IDisposable evalBindingHooks(TViewModel viewModel, TView view, string[] vmPropChain, string[] viewPropChain) + IDisposable evalBindingHooks(TViewModel viewModel, TView view, string[] vmPropChain, string[] viewPropChain, BindingDirection direction) where TViewModel : class where TView : IViewFor { var hooks = RxApp.GetAllServices(); - var vmFetcher = new Func[]>(() => { - IObservedChange[] fetchedValues; - Reflection.TryGetAllValuesForPropertyChain(out fetchedValues, viewModel, vmPropChain); - return fetchedValues; - }); + var vmFetcher = default(Func[]>); + if (vmPropChain != null) { + vmFetcher = () => { + IObservedChange[] fetchedValues; + Reflection.TryGetAllValuesForPropertyChain(out fetchedValues, viewModel, vmPropChain); + return fetchedValues; + }; + } else { + vmFetcher = () => { + return new[] { + new ObservedChange() { + Sender = null, PropertyName = null, Value = viewModel, + } + }; + }; + } var vFetcher = new Func[]>(() => { IObservedChange[] fetchedValues; @@ -1214,16 +1224,16 @@ IDisposable evalBindingHooks(TViewModel viewModel, TView view return null; } - MemoizingMRUCache, IBindingTypeConverter> typeConverterCache = new MemoizingMRUCache, IBindingTypeConverter>( - (types, _) => - RxApp.GetAllServices() - .Aggregate(Tuple.Create(-1, default(IBindingTypeConverter)), (acc, x) => { + (types, _) => { + return RxApp.GetAllServices() + .Aggregate(Tuple.Create(-1, default(IBindingTypeConverter)), (acc, x) => + { var score = x.GetAffinityForObjects(types.Item1, types.Item2); - return score > acc.Item1 && score > 0 ? + return score > acc.Item1 && score > 0 ? Tuple.Create(score, x) : acc; - }).Item2 - , 25); + }).Item2; + }, 25); internal IBindingTypeConverter getConverterForTypes(Type lhs, Type rhs) { From e307466fd43abfcf9c03a0d6e26565fa807c144d Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Mon, 4 Mar 2013 12:00:03 -0800 Subject: [PATCH 4/6] Rewrite the actual bind part of BindTo so that the other binding ops can use it --- ReactiveUI/PropertyBinding.cs | 88 +++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/ReactiveUI/PropertyBinding.cs b/ReactiveUI/PropertyBinding.cs index 7a574e5977..f584033288 100644 --- a/ReactiveUI/PropertyBinding.cs +++ b/ReactiveUI/PropertyBinding.cs @@ -979,6 +979,8 @@ public IDisposable OneWayBind( { var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty); var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain)); + var source = default(IObservable); + var fallbackWrapper = default(Func); if (viewProperty == null) { var viewPropChain = Reflection.getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty)); @@ -993,13 +995,17 @@ public IDisposable OneWayBind( var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay); if (ret != null) return ret; - return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) + source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) .SelectMany(x => { object tmp; - if (!converter.TryConvert(x, viewType, conversionHint, out tmp)) return Observable.Empty(); - return Observable.Return(tmp); - }) - .Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false)); + if (!converter.TryConvert(x, viewType, conversionHint, out tmp)) return Observable.Empty(); + return Observable.Return((TVProp)tmp); + }); + + fallbackWrapper = () => { + object tmp; + return converter.TryConvert(fallbackValue(), typeof(TVProp), conversionHint, out tmp) ? (TVProp)tmp : default(TVProp); + }; } else { var converter = getConverterForTypes(typeof (TVMProp), typeof (TVProp)); @@ -1012,17 +1018,20 @@ public IDisposable OneWayBind( var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay); if (ret != null) return ret; - return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) + source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) .SelectMany(x => { object tmp; if (!converter.TryConvert(x, typeof(TVProp), conversionHint, out tmp)) return Observable.Empty(); - return Observable.Return(tmp == null ? default(TVProp) : (TVProp) tmp); - }) - .BindTo(view, viewProperty, () => { - object tmp; - return converter.TryConvert(fallbackValue(), typeof(TVProp), conversionHint, out tmp) ? (TVProp)tmp : default(TVProp); + return Observable.Return(tmp == null ? default(TVProp) : (TVProp)tmp); }); + + fallbackWrapper = () => { + object tmp; + return converter.TryConvert(fallbackValue(), typeof(TVProp), conversionHint, out tmp) ? (TVProp)tmp : default(TVProp); + }; } + + return bindToDirect(source, view, viewProperty, fallbackWrapper); } /// @@ -1075,6 +1084,7 @@ public IDisposable OneWayBind( { var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty); var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain)); + var source = default(IObservable); if (viewProperty == null) { var viewPropChain = Reflection.getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty)); @@ -1082,18 +1092,16 @@ public IDisposable OneWayBind( var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay); if (ret != null) return ret; - return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) - .Select(selector) - .Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false)); + source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty).Select(selector); } else { var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty); var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.OneWay); if (ret != null) return ret; - return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) - .Select(selector) - .BindTo(view, viewProperty, fallbackValue); + source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty).Select(selector); } + + return bindToDirect(source, view, viewProperty, fallbackValue); } /// @@ -1152,6 +1160,7 @@ public IDisposable AsyncOneWayBind( { var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty); var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain)); + var source = default(IObservable); if (viewProperty == null) { var viewPropChain = Reflection.getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty)); @@ -1159,19 +1168,17 @@ public IDisposable AsyncOneWayBind( var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.AsyncOneWay); if (ret != null) return ret; - return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) - .SelectMany(selector) - .Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false)); + source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty).SelectMany(selector); } else { var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty); var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain, BindingDirection.AsyncOneWay); if (ret != null) return ret; - return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty) - .SelectMany(selector) - .BindTo(view, viewProperty, fallbackValue); + source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty).SelectMany(selector); } + + return bindToDirect(source, view, viewProperty, fallbackValue); } public IDisposable BindTo( @@ -1180,11 +1187,44 @@ public IDisposable BindTo( Expression> property, Func fallbackValue = null) { + throw new NotImplementedException(); + } + + IDisposable bindToDirect( + IObservable This, + TTarget target, + Expression> property, + Func fallbackValue = null) + { + var types = new[] { typeof(TTarget) }.Concat(Reflection.ExpressionToPropertyTypes(property)).ToArray(); + var names = Reflection.ExpressionToPropertyNames(property); + + var setter = Reflection.GetValueSetterOrThrow(types.Reverse().Skip(1).First(), names.Last()); + if (names.Length == 1) { + return This.Subscribe( + x => setter(target, x), + ex => { + this.Log().ErrorException("Binding recieved an Exception!", ex); + if (fallbackValue != null) setter(target, fallbackValue()); + }); + } + + var bindInfo = Observable.CombineLatest( + This, target.WhenAnyDynamic(names.SkipLast(1).ToArray(), x => x.Value), + (val, host) => new { val, host }); + + return bindInfo + .Where(x => x.host != null) + .Subscribe( + x => setter(x.host, x.val), + ex => { + this.Log().ErrorException("Binding recieved an Exception!", ex); + if (fallbackValue != null) setter(target, fallbackValue()); + }); } IDisposable evalBindingHooks(TViewModel viewModel, TView view, string[] vmPropChain, string[] viewPropChain, BindingDirection direction) where TViewModel : class - where TView : IViewFor { var hooks = RxApp.GetAllServices(); From 1a77b21ad9ac4dac7a76aad0b9f19c65e837a8a5 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Mon, 4 Mar 2013 12:13:31 -0800 Subject: [PATCH 5/6] Fix up BindTo to enable binding hooks and type conversions --- ReactiveUI/PropertyBinding.cs | 43 +++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/ReactiveUI/PropertyBinding.cs b/ReactiveUI/PropertyBinding.cs index f584033288..d2bbef0b11 100644 --- a/ReactiveUI/PropertyBinding.cs +++ b/ReactiveUI/PropertyBinding.cs @@ -550,11 +550,12 @@ public static IDisposable AsyncOneWayBind( /// An expression representing the target /// property to set. This can be a child property (i.e. x.Foo.Bar.Baz). /// An object that when disposed, disconnects the binding. - public static IDisposable BindTo( + public static IDisposable BindTo( this IObservable This, TTarget target, - Expression> property, - Func fallbackValue = null) + Expression> property, + Func fallbackValue = null, + object conversionHint = null) { return binderImplementation.BindTo(This, target, property, fallbackValue); } @@ -769,11 +770,12 @@ IDisposable AsyncOneWayBind( /// An expression representing the target /// property to set. This can be a child property (i.e. x.Foo.Bar.Baz). /// An object that when disposed, disconnects the binding. - IDisposable BindTo( + IDisposable BindTo( IObservable This, TTarget target, - Expression> property, - Func fallbackValue = null); + Expression> property, + Func fallbackValue = null, + object conversionHint = null); } public class PropertyBinderImplementation : IPropertyBinderImplementation @@ -1181,13 +1183,34 @@ public IDisposable AsyncOneWayBind( return bindToDirect(source, view, viewProperty, fallbackValue); } - public IDisposable BindTo( + public IDisposable BindTo( IObservable This, TTarget target, - Expression> property, - Func fallbackValue = null) + Expression> property, + Func fallbackValue = null, + object conversionHint = null) { - throw new NotImplementedException(); + var viewPropChain = Reflection.ExpressionToPropertyNames(property); + var ret = evalBindingHooks(This, target, null, viewPropChain, BindingDirection.OneWay); + if (ret != null) return ret; + + var converter = getConverterForTypes(typeof (TValue), typeof(TTValue)); + + if (converter == null) { + throw new ArgumentException(String.Format("Can't convert {0} to {1}. To fix this, register a IBindingTypeConverter", typeof (TValue), typeof(TTValue))); + } + + var source = This.SelectMany(x => { + object tmp; + if (!converter.TryConvert(x, typeof(TTValue), conversionHint, out tmp)) return Observable.Empty(); + return Observable.Return(tmp == null ? default(TTValue) : (TTValue)tmp); + }); + + return bindToDirect(source, target, property, fallbackValue == null ? default(Func) : new Func(() => { + object tmp; + if (!converter.TryConvert(fallbackValue(), typeof(TTValue), conversionHint, out tmp)) return default(TTValue); + return tmp == null ? default(TTValue) : (TTValue)tmp; + })); } IDisposable bindToDirect( From 08ce999cff6c17ebc907af92c06e26e7d8da0bbd Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Mon, 4 Mar 2013 13:38:02 -0800 Subject: [PATCH 6/6] Add a test to verify type conversion works --- ReactiveUI.Tests/PropertyBindingTest.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ReactiveUI.Tests/PropertyBindingTest.cs b/ReactiveUI.Tests/PropertyBindingTest.cs index eb4d4e94d1..11a55f65e0 100644 --- a/ReactiveUI.Tests/PropertyBindingTest.cs +++ b/ReactiveUI.Tests/PropertyBindingTest.cs @@ -336,6 +336,21 @@ public void BindToShouldntInitiallySetToNull() Assert.Equal(vm.Model.AnotherThing, view.FakeControl.NullHatingString); } + [Fact] + public void BindToTypeConversionSmokeTest() + { + var vm = new PropertyBindViewModel(); + var view = new PropertyBindView() {ViewModel = null}; + + view.WhenAny(x => x.ViewModel.JustADouble, x => x.Value) + .BindTo(view, x => x.FakeControl.NullHatingString); + + Assert.Equal("", view.FakeControl.NullHatingString); + + view.ViewModel = vm; + Assert.Equal(vm.JustADouble.ToString(), view.FakeControl.NullHatingString); + } + void configureDummyServiceLocator() { var types = new Dictionary, List>();