PageRenderTime 52ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/ReactiveUI/PropertyBinding.cs

https://github.com/bsiegel/ReactiveUI
C# | 516 lines | 436 code | 65 blank | 15 comment | 60 complexity | d2ac0b893d0ffcb966ead30d7828adf4 MD5 | raw file
Possible License(s): Apache-2.0, CC-BY-SA-3.0, LGPL-2.0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Linq.Expressions;
  5. using System.Reactive;
  6. using System.Reactive.Disposables;
  7. using System.Reactive.Linq;
  8. using System.Reactive.Subjects;
  9. using System.Reactive.Threading.Tasks;
  10. using System.Reflection;
  11. using System.Text;
  12. using System.Threading.Tasks;
  13. namespace ReactiveUI
  14. {
  15. public static class BindingMixins
  16. {
  17. static IPropertyBinderImplementation binderImplementation;
  18. static BindingMixins()
  19. {
  20. binderImplementation = new PropertyBinderImplementation();
  21. }
  22. public static IDisposable Bind<TViewModel, TView, TVMProp, TVProp>(
  23. this TView view,
  24. TViewModel viewModel,
  25. Expression<Func<TViewModel, TVMProp>> vmProperty,
  26. Expression<Func<TView, TVProp>> viewProperty)
  27. where TViewModel : class
  28. where TView : IViewFor
  29. {
  30. return binderImplementation.Bind(viewModel, view, vmProperty, viewProperty, (IObservable<Unit>)null, null);
  31. }
  32. public static IDisposable Bind<TViewModel, TView, TProp>(
  33. this TView view,
  34. TViewModel viewModel,
  35. Expression<Func<TViewModel, TProp>> vmProperty)
  36. where TViewModel : class
  37. where TView : IViewFor
  38. {
  39. return binderImplementation.Bind<TViewModel, TView, TProp, TProp, Unit>(viewModel, view, vmProperty, null, null, null);
  40. }
  41. public static IDisposable Bind<TViewModel, TView, TVMProp, TVProp, TDontCare>(
  42. this TView view,
  43. TViewModel viewModel,
  44. Expression<Func<TViewModel, TVMProp>> vmProperty,
  45. Expression<Func<TView, TVProp>> viewProperty,
  46. IObservable<TDontCare> signalViewUpdate)
  47. where TViewModel : class
  48. where TView : IViewFor
  49. {
  50. return binderImplementation.Bind(viewModel, view, vmProperty, viewProperty, signalViewUpdate, null);
  51. }
  52. public static IDisposable Bind<TViewModel, TView, TProp, TDontCare>(
  53. this TView view,
  54. TViewModel viewModel,
  55. Expression<Func<TViewModel, TProp>> vmProperty,
  56. IObservable<TDontCare> signalViewUpdate)
  57. where TViewModel : class
  58. where TView : IViewFor
  59. {
  60. return binderImplementation.Bind<TViewModel, TView, TProp, TDontCare, TDontCare>(viewModel, view, vmProperty, null, signalViewUpdate, null);
  61. }
  62. public static IDisposable OneWayBind<TViewModel, TView, TVMProp, TVProp>(
  63. this TView view,
  64. TViewModel viewModel,
  65. Expression<Func<TViewModel, TVMProp>> vmProperty,
  66. Expression<Func<TView, TVProp>> viewProperty,
  67. Func<TVMProp> fallbackValue = null,
  68. object conversionHint = null)
  69. where TViewModel : class
  70. where TView : IViewFor
  71. {
  72. return binderImplementation.OneWayBind(viewModel, view, vmProperty, viewProperty, fallbackValue);
  73. }
  74. public static IDisposable OneWayBind<TViewModel, TView, TProp>(
  75. this TView view,
  76. TViewModel viewModel,
  77. Expression<Func<TViewModel, TProp>> vmProperty,
  78. Func<TProp> fallbackValue = null,
  79. object conversionHint = null)
  80. where TViewModel : class
  81. where TView : IViewFor
  82. {
  83. return binderImplementation.OneWayBind<TViewModel, TView, TProp, Unit>(viewModel, view, vmProperty, null, fallbackValue, null);
  84. }
  85. public static IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
  86. this TView view,
  87. TViewModel viewModel,
  88. Expression<Func<TViewModel, TProp>> vmProperty,
  89. Expression<Func<TView, TOut>> viewProperty,
  90. Func<TProp, TOut> selector,
  91. Func<TOut> fallbackValue = null)
  92. where TViewModel : class
  93. where TView : IViewFor
  94. {
  95. return binderImplementation.OneWayBind(viewModel, view, vmProperty, viewProperty, selector, fallbackValue);
  96. }
  97. public static IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
  98. this TView view,
  99. TViewModel viewModel,
  100. Expression<Func<TViewModel, TProp>> vmProperty,
  101. Func<TProp, TOut> selector,
  102. Func<TOut> fallbackValue = null)
  103. where TViewModel : class
  104. where TView : IViewFor
  105. {
  106. return binderImplementation.OneWayBind(viewModel, view, vmProperty, null, selector, fallbackValue);
  107. }
  108. public static IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
  109. this TView view,
  110. TViewModel viewModel,
  111. Expression<Func<TViewModel, TProp>> vmProperty,
  112. Expression<Func<TView, TOut>> viewProperty,
  113. Func<TProp, IObservable<TOut>> selector,
  114. Func<TOut> fallbackValue = null)
  115. where TViewModel : class
  116. where TView : IViewFor
  117. {
  118. return binderImplementation.AsyncOneWayBind(viewModel, view, vmProperty, viewProperty, selector, fallbackValue);
  119. }
  120. public static IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
  121. this TView view,
  122. TViewModel viewModel,
  123. Expression<Func<TViewModel, TProp>> vmProperty,
  124. Expression<Func<TView, TOut>> viewProperty,
  125. Func<TProp, Task<TOut>> selector,
  126. Func<TOut> fallbackValue = null)
  127. where TViewModel : class
  128. where TView : IViewFor
  129. {
  130. return binderImplementation.AsyncOneWayBind(viewModel, view, vmProperty, viewProperty, x => selector(x).ToObservable(), fallbackValue);
  131. }
  132. public static IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
  133. this TView view,
  134. TViewModel viewModel,
  135. Expression<Func<TViewModel, TProp>> vmProperty,
  136. Func<TProp, IObservable<TOut>> selector,
  137. Func<TOut> fallbackValue = null)
  138. where TViewModel : class
  139. where TView : IViewFor
  140. {
  141. return binderImplementation.AsyncOneWayBind(viewModel, view, vmProperty, null, selector, fallbackValue);
  142. }
  143. public static IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
  144. this TView view,
  145. TViewModel viewModel,
  146. Expression<Func<TViewModel, TProp>> vmProperty,
  147. Func<TProp, Task<TOut>> selector,
  148. Func<TOut> fallbackValue = null)
  149. where TViewModel : class
  150. where TView : IViewFor
  151. {
  152. return binderImplementation.AsyncOneWayBind(viewModel, view, vmProperty, null, x => selector(x).ToObservable(), fallbackValue);
  153. }
  154. }
  155. public interface IPropertyBinderImplementation : IEnableLogger
  156. {
  157. IDisposable Bind<TViewModel, TView, TVMProp, TVProp, TDontCare>(
  158. TViewModel viewModel,
  159. TView view,
  160. Expression<Func<TViewModel, TVMProp>> vmProperty,
  161. Expression<Func<TView, TVProp>> viewProperty,
  162. IObservable<TDontCare> signalViewUpdate,
  163. object conversionHint)
  164. where TViewModel : class
  165. where TView : IViewFor;
  166. IDisposable OneWayBind<TViewModel, TView, TVMProp, TVProp>(
  167. TViewModel viewModel,
  168. TView view,
  169. Expression<Func<TViewModel, TVMProp>> vmProperty,
  170. Expression<Func<TView, TVProp>> viewProperty,
  171. Func<TVMProp> fallbackValue = null,
  172. object conversionHint = null)
  173. where TViewModel : class
  174. where TView : IViewFor;
  175. IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
  176. TViewModel viewModel,
  177. TView view,
  178. Expression<Func<TViewModel, TProp>> vmProperty,
  179. Expression<Func<TView, TOut>> viewProperty,
  180. Func<TProp, TOut> selector,
  181. Func<TOut> fallbackValue = null)
  182. where TViewModel : class
  183. where TView : IViewFor;
  184. IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
  185. TViewModel viewModel,
  186. TView view,
  187. Expression<Func<TViewModel, TProp>> vmProperty,
  188. Expression<Func<TView, TOut>> viewProperty,
  189. Func<TProp, IObservable<TOut>> selector,
  190. Func<TOut> fallbackValue = null)
  191. where TViewModel : class
  192. where TView : IViewFor;
  193. }
  194. class PropertyBinderImplementation : IPropertyBinderImplementation
  195. {
  196. public IDisposable Bind<TViewModel, TView, TVMProp, TVProp, TDontCare>(
  197. TViewModel viewModel,
  198. TView view,
  199. Expression<Func<TViewModel, TVMProp>> vmProperty,
  200. Expression<Func<TView, TVProp>> viewProperty,
  201. IObservable<TDontCare> signalViewUpdate,
  202. object conversionHint)
  203. where TViewModel : class
  204. where TView : IViewFor
  205. {
  206. var signalInitialUpdate = new Subject<bool>();
  207. var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty);
  208. string[] viewPropChain;
  209. if (viewProperty == null) {
  210. // NB: In this case, TVProp is possibly wrong due to type
  211. // conversion. Figure out if this is the case, then re-call Bind
  212. // with the right TVProp
  213. viewPropChain = Reflection.getDefaultViewPropChain(view, vmPropChain);
  214. var tvProp = Reflection.GetTypesForPropChain(typeof (TView), viewPropChain).Last();
  215. if (tvProp != typeof (TVProp)) {
  216. var mi = this.GetType().GetMethod("Bind").MakeGenericMethod(typeof (TViewModel), typeof (TView), typeof (TVMProp), tvProp, typeof (TDontCare));
  217. return (IDisposable) mi.Invoke(this, new[] {viewModel, view, vmProperty, null, signalViewUpdate, conversionHint});
  218. }
  219. } else {
  220. viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty);
  221. }
  222. var vmToViewConverter = getConverterForTypes(typeof (TVMProp), typeof (TVProp));
  223. var viewToVMConverter = getConverterForTypes(typeof (TVProp), typeof (TVMProp));
  224. if (vmToViewConverter == null || viewToVMConverter == null) {
  225. throw new ArgumentException(
  226. String.Format("Can't two-way convert between {0} and {1}. To fix this, register a IBindingTypeConverter", typeof (TVMProp), typeof(TVProp)));
  227. }
  228. var somethingChanged = Observable.Merge(
  229. Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty).Select(_ => true),
  230. signalInitialUpdate,
  231. signalViewUpdate != null ?
  232. signalViewUpdate.Select(_ => false) :
  233. view.WhenAnyDynamic(viewPropChain, x => (TVProp) x.Value).Select(_ => false));
  234. var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain));
  235. var vString = String.Format("{0}.{1}", typeof (TView).Name, String.Join(".", viewPropChain));
  236. var vmChangedString = String.Format("Setting {0} => {1}", vmString, vString);
  237. var viewChangedString = String.Format("Setting {0} => {1}", vString, vmString);
  238. var changeWithValues = somethingChanged.Select(isVm => {
  239. TVMProp vmValue; TVProp vValue;
  240. if (!Reflection.TryGetValueForPropertyChain(out vmValue, view.ViewModel, vmPropChain) ||
  241. !Reflection.TryGetValueForPropertyChain(out vValue, view, viewPropChain)) {
  242. return null;
  243. }
  244. if (isVm) {
  245. object tmp;
  246. if (!vmToViewConverter.TryConvert(vmValue, typeof (TVProp), conversionHint, out tmp)) {
  247. return null;
  248. }
  249. var vmAsView = (tmp == null ? default(TVProp) : (TVProp) tmp);
  250. var changed = EqualityComparer<TVProp>.Default.Equals(vValue, vmAsView) != true;
  251. if (!changed) return null;
  252. this.Log().Info(vmChangedString + (vmAsView != null ? vmAsView.ToString() : "(null)"));
  253. return Tuple.Create((object)vmAsView, isVm);
  254. } else {
  255. object tmp;
  256. if (!viewToVMConverter.TryConvert(vValue, typeof (TVMProp), conversionHint, out tmp)) {
  257. return null;
  258. }
  259. var vAsViewModel = (tmp == null ? default(TVMProp) : (TVMProp) tmp);
  260. var changed = EqualityComparer<TVMProp>.Default.Equals(vmValue, vAsViewModel) != true;
  261. if (!changed) return null;
  262. this.Log().Info(viewChangedString + (vAsViewModel != null ? vAsViewModel.ToString() : "(null)"));
  263. return Tuple.Create((object)vAsViewModel, isVm);
  264. }
  265. });
  266. var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
  267. if (ret != null) return ret;
  268. ret = changeWithValues.Subscribe(isVmWithLatestValue => {
  269. if (isVmWithLatestValue == null) return;
  270. if (isVmWithLatestValue.Item2) {
  271. Reflection.SetValueToPropertyChain(view, viewPropChain, isVmWithLatestValue.Item1, false);
  272. } else {
  273. Reflection.SetValueToPropertyChain(view.ViewModel, vmPropChain, isVmWithLatestValue.Item1, false);
  274. }
  275. });
  276. // NB: Even though it's technically a two-way bind, most people
  277. // want the ViewModel to win at first.
  278. signalInitialUpdate.OnNext(true);
  279. return ret;
  280. }
  281. public IDisposable OneWayBind<TViewModel, TView, TVMProp, TVProp>(
  282. TViewModel viewModel,
  283. TView view,
  284. Expression<Func<TViewModel, TVMProp>> vmProperty,
  285. Expression<Func<TView, TVProp>> viewProperty,
  286. Func<TVMProp> fallbackValue = null,
  287. object conversionHint = null)
  288. where TViewModel : class
  289. where TView : IViewFor
  290. {
  291. var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty);
  292. var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain));
  293. if (viewProperty == null) {
  294. var viewPropChain = Reflection.getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty));
  295. var viewType = Reflection.GetTypesForPropChain(typeof (TView), viewPropChain).Last();
  296. var converter = getConverterForTypes(typeof (TVMProp), viewType);
  297. if (converter == null) {
  298. throw new ArgumentException(String.Format("Can't convert {0} to {1}. To fix this, register a IBindingTypeConverter", typeof (TVMProp), viewType));
  299. }
  300. var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
  301. if (ret != null) return ret;
  302. return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
  303. .SelectMany(x => {
  304. object tmp;
  305. if (!converter.TryConvert(x, viewType, conversionHint, out tmp)) return Observable.Empty<object>();
  306. return Observable.Return(tmp);
  307. })
  308. .Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false));
  309. } else {
  310. var converter = getConverterForTypes(typeof (TVMProp), typeof (TVProp));
  311. if (converter == null) {
  312. throw new ArgumentException(String.Format("Can't convert {0} to {1}. To fix this, register a IBindingTypeConverter", typeof (TVMProp), typeof(TVProp)));
  313. }
  314. var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty);
  315. var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
  316. if (ret != null) return ret;
  317. return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
  318. .SelectMany(x => {
  319. object tmp;
  320. if (!converter.TryConvert(x, typeof(TVProp), conversionHint, out tmp)) return Observable.Empty<TVProp>();
  321. return Observable.Return(tmp == null ? default(TVProp) : (TVProp) tmp);
  322. })
  323. .BindTo(view, viewProperty, () => {
  324. object tmp;
  325. return converter.TryConvert(fallbackValue(), typeof(TVProp), conversionHint, out tmp) ? (TVProp)tmp : default(TVProp);
  326. });
  327. }
  328. }
  329. public IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
  330. TViewModel viewModel,
  331. TView view,
  332. Expression<Func<TViewModel, TProp>> vmProperty,
  333. Expression<Func<TView, TOut>> viewProperty,
  334. Func<TProp, TOut> selector,
  335. Func<TOut> fallbackValue = null)
  336. where TViewModel : class
  337. where TView : IViewFor
  338. {
  339. var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty);
  340. var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain));
  341. if (viewProperty == null) {
  342. var viewPropChain = Reflection.getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty));
  343. var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
  344. if (ret != null) return ret;
  345. return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
  346. .Select(selector)
  347. .Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false));
  348. } else {
  349. var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty);
  350. var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
  351. if (ret != null) return ret;
  352. return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
  353. .Select(selector)
  354. .BindTo(view, viewProperty, fallbackValue);
  355. }
  356. }
  357. public IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
  358. TViewModel viewModel,
  359. TView view,
  360. Expression<Func<TViewModel, TProp>> vmProperty,
  361. Expression<Func<TView, TOut>> viewProperty,
  362. Func<TProp, IObservable<TOut>> selector,
  363. Func<TOut> fallbackValue = null)
  364. where TViewModel : class
  365. where TView : IViewFor
  366. {
  367. var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty);
  368. var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain));
  369. if (viewProperty == null) {
  370. var viewPropChain = Reflection.getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty));
  371. var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
  372. if (ret != null) return ret;
  373. return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
  374. .SelectMany(selector)
  375. .Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false));
  376. } else {
  377. var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty);
  378. var ret = evalBindingHooks(viewModel, view, vmPropChain, viewPropChain);
  379. if (ret != null) return ret;
  380. return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
  381. .SelectMany(selector)
  382. .BindTo(view, viewProperty, fallbackValue);
  383. }
  384. }
  385. IDisposable evalBindingHooks<TViewModel, TView>(TViewModel viewModel, TView view, string[] vmPropChain, string[] viewPropChain)
  386. where TViewModel : class
  387. where TView : IViewFor
  388. {
  389. var hooks = RxApp.GetAllServices<IPropertyBindingHook>();
  390. var vmFetcher = new Func<IObservedChange<object, object>[]>(() => {
  391. IObservedChange<object, object>[] fetchedValues;
  392. Reflection.TryGetAllValuesForPropertyChain(out fetchedValues, viewModel, vmPropChain);
  393. return fetchedValues;
  394. });
  395. var vFetcher = new Func<IObservedChange<object, object>[]>(() => {
  396. IObservedChange<object, object>[] fetchedValues;
  397. Reflection.TryGetAllValuesForPropertyChain(out fetchedValues, view, viewPropChain);
  398. return fetchedValues;
  399. });
  400. var shouldBind = hooks.Aggregate(true, (acc, x) =>
  401. acc && x.ExecuteHook(viewModel, view, vmFetcher, vFetcher, BindingDirection.TwoWay));
  402. if (!shouldBind) {
  403. var vmString = String.Format("{0}.{1}", typeof (TViewModel).Name, String.Join(".", vmPropChain));
  404. var vString = String.Format("{0}.{1}", typeof (TView).Name, String.Join(".", viewPropChain));
  405. this.Log().Warn("Binding hook asked to disable binding {0} => {1}", vmString, vString);
  406. return Disposable.Empty;
  407. }
  408. return null;
  409. }
  410. MemoizingMRUCache<Tuple<Type, Type>, IBindingTypeConverter> typeConverterCache = new MemoizingMRUCache<Tuple<Type, Type>, IBindingTypeConverter>(
  411. (types, _) =>
  412. RxApp.GetAllServices<IBindingTypeConverter>()
  413. .Aggregate(Tuple.Create(-1, default(IBindingTypeConverter)), (acc, x) => {
  414. var score = x.GetAffinityForObjects(types.Item1, types.Item2);
  415. return score > acc.Item1 && score > 0 ?
  416. Tuple.Create(score, x) : acc;
  417. }).Item2
  418. , 25);
  419. IBindingTypeConverter getConverterForTypes(Type lhs, Type rhs)
  420. {
  421. return typeConverterCache.Get(Tuple.Create(lhs, rhs));
  422. }
  423. }
  424. public static class ObservableBindingMixins
  425. {
  426. /// <summary>
  427. /// BindTo takes an Observable stream and applies it to a target
  428. /// property. Conceptually it is similar to "Subscribe(x =&gt;
  429. /// target.property = x)", but allows you to use child properties
  430. /// without the null checks.
  431. /// </summary>
  432. /// <param name="target">The target object whose property will be set.</param>
  433. /// <param name="property">An expression representing the target
  434. /// property to set. This can be a child property (i.e. x.Foo.Bar.Baz).</param>
  435. /// <returns>An object that when disposed, disconnects the binding.</returns>
  436. public static IDisposable BindTo<TTarget, TValue>(
  437. this IObservable<TValue> This,
  438. TTarget target,
  439. Expression<Func<TTarget, TValue>> property,
  440. Func<TValue> fallbackValue = null)
  441. {
  442. var pn = Reflection.ExpressionToPropertyNames(property);
  443. var lastValue = default(TValue);
  444. return Observable.Merge(target.WhenAny(property, _ => lastValue).Skip(1), This)
  445. .Subscribe(x => {
  446. lastValue = x;
  447. Reflection.SetValueToPropertyChain(target, pn, x);
  448. });
  449. }
  450. }
  451. }