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>(); diff --git a/ReactiveUI/PropertyBinding.cs b/ReactiveUI/PropertyBinding.cs index be3ea72e15..d2bbef0b11 100644 --- a/ReactiveUI/PropertyBinding.cs +++ b/ReactiveUI/PropertyBinding.cs @@ -539,6 +539,26 @@ 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, + object conversionHint = null) + { + return binderImplementation.BindTo(This, target, property, fallbackValue); + } } /// @@ -739,6 +759,23 @@ 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, + object conversionHint = null); } public class PropertyBinderImplementation : IPropertyBinderImplementation @@ -870,7 +907,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 => { @@ -944,6 +981,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)); @@ -955,16 +994,20 @@ 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) + 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)); @@ -974,20 +1017,23 @@ 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) + 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); } /// @@ -1040,25 +1086,24 @@ 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)); - 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) - .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); + 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); } /// @@ -1117,45 +1162,120 @@ 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)); - 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) - .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); + 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); } - IDisposable evalBindingHooks(TViewModel viewModel, TView view, string[] vmPropChain, string[] viewPropChain) + public IDisposable BindTo( + IObservable This, + TTarget target, + Expression> property, + Func fallbackValue = null, + object conversionHint = null) + { + 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( + 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(); - 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; Reflection.TryGetAllValuesForPropertyChain(out fetchedValues, view, viewPropChain); return fetchedValues; }); + 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)); @@ -1167,56 +1287,22 @@ 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); - 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