ExpressionObserverTests_Property.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Reactive;
  4. using System.Reactive.Linq;
  5. using System.Reactive.Subjects;
  6. using Microsoft.Reactive.Testing;
  7. using Avalonia.Data;
  8. using Avalonia.Data.Core;
  9. using Avalonia.UnitTests;
  10. using Xunit;
  11. using System.Threading.Tasks;
  12. using Avalonia.Markup.Parsers;
  13. using Avalonia.Threading;
  14. namespace Avalonia.Base.UnitTests.Data.Core
  15. {
  16. public class ExpressionObserverTests_Property
  17. {
  18. [Fact]
  19. public async Task Should_Get_Simple_Property_Value()
  20. {
  21. var data = new { Foo = "foo" };
  22. var target = ExpressionObserver.Create(data, o => o.Foo);
  23. var result = await target.Take(1);
  24. Assert.Equal("foo", result);
  25. GC.KeepAlive(data);
  26. }
  27. [Fact]
  28. public void Should_Get_Simple_Property_Value_Type()
  29. {
  30. var data = new { Foo = "foo" };
  31. var target = ExpressionObserver.Create(data, o => o.Foo);
  32. target.Subscribe(_ => { });
  33. Assert.Equal(typeof(string), target.ResultType);
  34. GC.KeepAlive(data);
  35. }
  36. [Fact]
  37. public async Task Should_Get_Simple_Property_Value_Null()
  38. {
  39. var data = new { Foo = (string)null };
  40. var target = ExpressionObserver.Create(data, o => o.Foo);
  41. var result = await target.Take(1);
  42. Assert.Null(result);
  43. GC.KeepAlive(data);
  44. }
  45. [Fact]
  46. public async Task Should_Get_Simple_Property_From_Base_Class()
  47. {
  48. var data = new Class3 { Foo = "foo" };
  49. var target = ExpressionObserver.Create(data, o => o.Foo);
  50. var result = await target.Take(1);
  51. Assert.Equal("foo", result);
  52. GC.KeepAlive(data);
  53. }
  54. [Fact]
  55. public async Task Should_Return_BindingNotification_Error_For_Root_Null()
  56. {
  57. var target = ExpressionObserver.Create(default(Class3), o => o.Foo);
  58. var result = await target.Take(1);
  59. Assert.Equal(
  60. new BindingNotification(
  61. new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
  62. BindingErrorType.Error,
  63. AvaloniaProperty.UnsetValue),
  64. result);
  65. }
  66. [Fact]
  67. public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue()
  68. {
  69. var target = ExpressionObserver.Create(AvaloniaProperty.UnsetValue, o => (o as Class3).Foo);
  70. var result = await target.Take(1);
  71. Assert.Equal(
  72. new BindingNotification(
  73. new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty),
  74. BindingErrorType.Error,
  75. AvaloniaProperty.UnsetValue),
  76. result);
  77. }
  78. [Fact]
  79. public async Task Should_Return_BindingNotification_Error_For_Observable_Root_Null()
  80. {
  81. var target = ExpressionObserver.Create(Observable.Return(default(Class3)), o => o.Foo);
  82. var result = await target.Take(1);
  83. Assert.Equal(
  84. new BindingNotification(
  85. new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
  86. BindingErrorType.Error,
  87. AvaloniaProperty.UnsetValue),
  88. result);
  89. }
  90. [Fact]
  91. public async void Should_Return_BindingNotification_Error_For_Observable_Root_UnsetValue()
  92. {
  93. var target = ExpressionObserver.Create<object, string>(Observable.Return(AvaloniaProperty.UnsetValue), o => (o as Class3).Foo);
  94. var result = await target.Take(1);
  95. Assert.Equal(
  96. new BindingNotification(
  97. new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty),
  98. BindingErrorType.Error,
  99. AvaloniaProperty.UnsetValue),
  100. result);
  101. }
  102. [Fact]
  103. public async Task Should_Get_Simple_Property_Chain()
  104. {
  105. var data = new { Foo = new { Bar = new { Baz = "baz" } } };
  106. var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz);
  107. var result = await target.Take(1);
  108. Assert.Equal("baz", result);
  109. GC.KeepAlive(data);
  110. }
  111. [Fact]
  112. public void Should_Get_Simple_Property_Chain_Type()
  113. {
  114. var data = new { Foo = new { Bar = new { Baz = "baz" } } };
  115. var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz);
  116. target.Subscribe(_ => { });
  117. Assert.Equal(typeof(string), target.ResultType);
  118. GC.KeepAlive(data);
  119. }
  120. [Fact]
  121. public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value()
  122. {
  123. var data = new { Foo = default(Class1) };
  124. var target = ExpressionObserver.Create(data, o => o.Foo.Foo.Length);
  125. var result = new List<object>();
  126. target.Subscribe(x => result.Add(x));
  127. Assert.Equal(
  128. new[]
  129. {
  130. new BindingNotification(
  131. new MarkupBindingChainException("Null value", "o => o.Foo.Foo.Length", "Foo"),
  132. BindingErrorType.Error,
  133. AvaloniaProperty.UnsetValue),
  134. },
  135. result);
  136. GC.KeepAlive(data);
  137. }
  138. [Fact]
  139. public void Should_Track_Simple_Property_Value()
  140. {
  141. var data = new Class1 { Foo = "foo" };
  142. var target = ExpressionObserver.Create(data, o => o.Foo);
  143. var result = new List<object>();
  144. var sub = target.Subscribe(x => result.Add(x));
  145. data.Foo = "bar";
  146. Assert.Equal(new[] { "foo", "bar" }, result);
  147. sub.Dispose();
  148. // Forces WeakEvent compact
  149. Dispatcher.UIThread.RunJobs();
  150. Assert.Equal(0, data.PropertyChangedSubscriptionCount);
  151. GC.KeepAlive(data);
  152. }
  153. [Fact]
  154. public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String()
  155. {
  156. var data = new Class1 { Bar = "foo" };
  157. var target = ExpressionObserver.Create(data, o => o.Bar);
  158. var result = new List<object>();
  159. var sub = target.Subscribe(x => result.Add(x));
  160. Assert.Equal(new[] { "foo" }, result);
  161. data.Bar = "bar";
  162. Assert.Equal(new[] { "foo" }, result);
  163. data.RaisePropertyChanged(string.Empty);
  164. Assert.Equal(new[] { "foo", "bar" }, result);
  165. data.RaisePropertyChanged(null);
  166. Assert.Equal(new[] { "foo", "bar", "bar" }, result);
  167. sub.Dispose();
  168. // Forces WeakEvent compact
  169. Dispatcher.UIThread.RunJobs();
  170. Assert.Equal(0, data.PropertyChangedSubscriptionCount);
  171. GC.KeepAlive(data);
  172. }
  173. [Fact]
  174. public void Should_Track_End_Of_Property_Chain_Changing()
  175. {
  176. var data = new Class1 { Next = new Class2 { Bar = "bar" } };
  177. var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
  178. var result = new List<object>();
  179. var sub = target.Subscribe(x => result.Add(x));
  180. ((Class2)data.Next).Bar = "baz";
  181. ((Class2)data.Next).Bar = null;
  182. Assert.Equal(new[] { "bar", "baz", null }, result);
  183. sub.Dispose();
  184. // Forces WeakEvent compact
  185. Dispatcher.UIThread.RunJobs();
  186. Assert.Equal(0, data.PropertyChangedSubscriptionCount);
  187. Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
  188. GC.KeepAlive(data);
  189. }
  190. [Fact]
  191. public void Should_Track_Property_Chain_Changing()
  192. {
  193. var data = new Class1 { Next = new Class2 { Bar = "bar" } };
  194. var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
  195. var result = new List<object>();
  196. var sub = target.Subscribe(x => result.Add(x));
  197. var old = data.Next;
  198. data.Next = new Class2 { Bar = "baz" };
  199. data.Next = new Class2 { Bar = null };
  200. Assert.Equal(new[] { "bar", "baz", null }, result);
  201. sub.Dispose();
  202. // Forces WeakEvent compact
  203. Dispatcher.UIThread.RunJobs();
  204. Assert.Equal(0, data.PropertyChangedSubscriptionCount);
  205. Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
  206. Assert.Equal(0, old.PropertyChangedSubscriptionCount);
  207. GC.KeepAlive(data);
  208. }
  209. [Fact]
  210. public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending()
  211. {
  212. var data = new Class1
  213. {
  214. Next = new Class2
  215. {
  216. Next = new Class2
  217. {
  218. Bar = "bar"
  219. }
  220. }
  221. };
  222. var target = ExpressionObserver.Create(data, o => ((o.Next as Class2).Next as Class2).Bar);
  223. var result = new List<object>();
  224. var sub = target.Subscribe(x => result.Add(x));
  225. var old = data.Next;
  226. data.Next = new Class2 { Bar = "baz" };
  227. data.Next = old;
  228. Assert.Equal(
  229. new object[]
  230. {
  231. "bar",
  232. new BindingNotification(
  233. new MarkupBindingChainException("Null value", "o => ((o.Next As Class2).Next As Class2).Bar", "Next.Next"),
  234. BindingErrorType.Error,
  235. AvaloniaProperty.UnsetValue),
  236. "bar"
  237. },
  238. result);
  239. sub.Dispose();
  240. // Forces WeakEvent compact
  241. Dispatcher.UIThread.RunJobs();
  242. Assert.Equal(0, data.PropertyChangedSubscriptionCount);
  243. Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
  244. Assert.Equal(0, old.PropertyChangedSubscriptionCount);
  245. GC.KeepAlive(data);
  246. }
  247. [Fact]
  248. public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending()
  249. {
  250. var data = new Class1 { Next = new Class2 { Bar = "bar" } };
  251. var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
  252. var result = new List<object>();
  253. var sub = target.Subscribe(x => result.Add(x));
  254. var old = data.Next;
  255. var breaking = new WithoutBar();
  256. data.Next = breaking;
  257. data.Next = new Class2 { Bar = "baz" };
  258. Assert.Equal(
  259. new object[]
  260. {
  261. "bar",
  262. new BindingNotification(
  263. new MissingMemberException("Could not find a matching property accessor for 'Bar' on 'Avalonia.Base.UnitTests.Data.Core.ExpressionObserverTests_Property+WithoutBar'"),
  264. BindingErrorType.Error),
  265. "baz",
  266. },
  267. result);
  268. sub.Dispose();
  269. // Forces WeakEvent compact
  270. Dispatcher.UIThread.RunJobs();
  271. Assert.Equal(0, data.PropertyChangedSubscriptionCount);
  272. Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
  273. Assert.Equal(0, breaking.PropertyChangedSubscriptionCount);
  274. Assert.Equal(0, old.PropertyChangedSubscriptionCount);
  275. GC.KeepAlive(data);
  276. }
  277. [Fact]
  278. public void Empty_Expression_Should_Track_Root()
  279. {
  280. var data = new Class1 { Foo = "foo" };
  281. var update = new Subject<Unit>();
  282. var target = ExpressionObserver.Create(() => data.Foo, o => o, update);
  283. var result = new List<object>();
  284. target.Subscribe(x => result.Add(x));
  285. data.Foo = "bar";
  286. update.OnNext(Unit.Default);
  287. Assert.Equal(new[] { "foo", "bar" }, result);
  288. GC.KeepAlive(data);
  289. }
  290. [Fact]
  291. public void Should_Track_Property_Value_From_Observable_Root()
  292. {
  293. var scheduler = new TestScheduler();
  294. var source = scheduler.CreateColdObservable(
  295. OnNext(1, new Class1 { Foo = "foo" }),
  296. OnNext(2, new Class1 { Foo = "bar" }));
  297. var target = ExpressionObserver.Create(source, o => o.Foo);
  298. var result = new List<object>();
  299. using (target.Subscribe(x => result.Add(x)))
  300. {
  301. scheduler.Start();
  302. }
  303. Assert.Equal(new[] { "foo", "bar" }, result);
  304. Assert.All(source.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe));
  305. }
  306. [Fact]
  307. public void Subscribing_Multiple_Times_Should_Return_Values_To_All()
  308. {
  309. var data = new Class1 { Foo = "foo" };
  310. var target = ExpressionObserver.Create(data, o => o.Foo);
  311. var result1 = new List<object>();
  312. var result2 = new List<object>();
  313. var result3 = new List<object>();
  314. target.Subscribe(x => result1.Add(x));
  315. target.Subscribe(x => result2.Add(x));
  316. data.Foo = "bar";
  317. target.Subscribe(x => result3.Add(x));
  318. Assert.Equal(new[] { "foo", "bar" }, result1);
  319. Assert.Equal(new[] { "foo", "bar" }, result2);
  320. Assert.Equal(new[] { "bar" }, result3);
  321. GC.KeepAlive(data);
  322. }
  323. [Fact]
  324. public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once()
  325. {
  326. var data = new Class1 { Foo = "foo" };
  327. var target = ExpressionObserver.Create(data, o => o.Foo);
  328. var sub1 = target.Subscribe(x => { });
  329. var sub2 = target.Subscribe(x => { });
  330. Assert.Equal(1, data.PropertyChangedSubscriptionCount);
  331. sub1.Dispose();
  332. sub2.Dispose();
  333. // Forces WeakEvent compact
  334. Dispatcher.UIThread.RunJobs();
  335. Assert.Equal(0, data.PropertyChangedSubscriptionCount);
  336. GC.KeepAlive(data);
  337. }
  338. [Fact]
  339. public void SetValue_Should_Set_Simple_Property_Value()
  340. {
  341. var data = new Class1 { Foo = "foo" };
  342. var target = ExpressionObserver.Create(data, o => o.Foo);
  343. using (target.Subscribe(_ => { }))
  344. {
  345. Assert.True(target.SetValue("bar"));
  346. }
  347. Assert.Equal("bar", data.Foo);
  348. GC.KeepAlive(data);
  349. }
  350. [Fact]
  351. public void SetValue_Should_Set_Property_At_The_End_Of_Chain()
  352. {
  353. var data = new Class1 { Next = new Class2 { Bar = "bar" } };
  354. var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
  355. using (target.Subscribe(_ => { }))
  356. {
  357. Assert.True(target.SetValue("baz"));
  358. }
  359. Assert.Equal("baz", ((Class2)data.Next).Bar);
  360. GC.KeepAlive(data);
  361. }
  362. [Fact]
  363. public void SetValue_Should_Return_False_For_Missing_Property()
  364. {
  365. var data = new Class1 { Next = new WithoutBar() };
  366. var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
  367. using (target.Subscribe(_ => { }))
  368. {
  369. Assert.False(target.SetValue("baz"));
  370. }
  371. GC.KeepAlive(data);
  372. }
  373. [Fact]
  374. public void SetValue_Should_Notify_New_Value_With_Inpc()
  375. {
  376. var data = new Class1();
  377. var target = ExpressionObserver.Create(data, o => o.Foo);
  378. var result = new List<object>();
  379. target.Subscribe(x => result.Add(x));
  380. target.SetValue("bar");
  381. Assert.Equal(new[] { null, "bar" }, result);
  382. GC.KeepAlive(data);
  383. }
  384. [Fact]
  385. public void SetValue_Should_Notify_New_Value_Without_Inpc()
  386. {
  387. var data = new Class1();
  388. var target = ExpressionObserver.Create(data, o => o.Bar);
  389. var result = new List<object>();
  390. target.Subscribe(x => result.Add(x));
  391. target.SetValue("bar");
  392. Assert.Equal(new[] { null, "bar" }, result);
  393. GC.KeepAlive(data);
  394. }
  395. [Fact]
  396. public void SetValue_Should_Return_False_For_Missing_Object()
  397. {
  398. var data = new Class1();
  399. var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar);
  400. using (target.Subscribe(_ => { }))
  401. {
  402. Assert.False(target.SetValue("baz"));
  403. }
  404. GC.KeepAlive(data);
  405. }
  406. [Fact]
  407. public void Can_Replace_Root()
  408. {
  409. var first = new Class1 { Foo = "foo" };
  410. var second = new Class1 { Foo = "bar" };
  411. var root = first;
  412. var update = new Subject<Unit>();
  413. var target = ExpressionObserver.Create(() => root, o => o.Foo, update);
  414. var result = new List<object>();
  415. var sub = target.Subscribe(x => result.Add(x));
  416. root = second;
  417. update.OnNext(Unit.Default);
  418. root = null;
  419. update.OnNext(Unit.Default);
  420. Assert.Equal(
  421. new object[]
  422. {
  423. "foo",
  424. "bar",
  425. new BindingNotification(
  426. new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty),
  427. BindingErrorType.Error,
  428. AvaloniaProperty.UnsetValue)
  429. },
  430. result);
  431. // Forces WeakEvent compact
  432. Dispatcher.UIThread.RunJobs();
  433. Assert.Equal(0, first.PropertyChangedSubscriptionCount);
  434. Assert.Equal(0, second.PropertyChangedSubscriptionCount);
  435. GC.KeepAlive(first);
  436. GC.KeepAlive(second);
  437. }
  438. [Fact]
  439. public void Should_Not_Keep_Source_Alive()
  440. {
  441. Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
  442. {
  443. var source = new Class1 { Foo = "foo" };
  444. var target = ExpressionObserver.Create(source, o => o.Foo);
  445. return Tuple.Create(target, new WeakReference(source));
  446. };
  447. var result = run();
  448. result.Item1.Subscribe(x => { });
  449. // Mono trickery
  450. GC.Collect(2);
  451. GC.WaitForPendingFinalizers();
  452. GC.WaitForPendingFinalizers();
  453. GC.Collect(2);
  454. Assert.Null(result.Item2.Target);
  455. }
  456. [Fact]
  457. public void Should_Not_Throw_Exception_On_Unsubscribe_When_Already_Unsubscribed()
  458. {
  459. var source = new Class1 { Foo = "foo" };
  460. var target = new PropertyAccessorNode("Foo", false);
  461. Assert.NotNull(target);
  462. target.Target = new WeakReference<object>(source);
  463. target.Subscribe(_ => { });
  464. target.Unsubscribe();
  465. target.Unsubscribe();
  466. Assert.True(true);
  467. }
  468. [Fact]
  469. public void Should_Not_Throw_Exception_When_Enabling_Data_Validation_On_Missing_Member()
  470. {
  471. var source = new Class1();
  472. var target = new PropertyAccessorNode("NotFound", true);
  473. target.Target = new WeakReference<object>(source);
  474. var result = new List<object>();
  475. target.Subscribe(x => result.Add(x));
  476. Assert.Equal(
  477. new object[]
  478. {
  479. new BindingNotification(
  480. new MissingMemberException("Could not find a matching property accessor for 'NotFound' on 'Avalonia.Base.UnitTests.Data.Core.ExpressionObserverTests_Property+Class1'"),
  481. BindingErrorType.Error),
  482. },
  483. result);
  484. }
  485. [Fact]
  486. public void Should_Not_Throw_Exception_On_Duplicate_Properties()
  487. {
  488. // Repro of https://github.com/AvaloniaUI/Avalonia/issues/4733.
  489. var source = new MyViewModel();
  490. var target = new PropertyAccessorNode("Name", false);
  491. target.Target = new WeakReference<object>(source);
  492. var result = new List<object>();
  493. target.Subscribe(x => result.Add(x));
  494. }
  495. public class MyViewModelBase { public object Name => "Name"; }
  496. public class MyViewModel : MyViewModelBase { public new string Name => "NewName"; }
  497. private interface INext
  498. {
  499. int PropertyChangedSubscriptionCount { get; }
  500. }
  501. private class Class1 : NotifyingBase
  502. {
  503. private string _foo;
  504. private INext _next;
  505. public string Foo
  506. {
  507. get { return _foo; }
  508. set
  509. {
  510. _foo = value;
  511. RaisePropertyChanged(nameof(Foo));
  512. }
  513. }
  514. private string _bar;
  515. public string Bar
  516. {
  517. get { return _bar; }
  518. set { _bar = value; }
  519. }
  520. public INext Next
  521. {
  522. get { return _next; }
  523. set
  524. {
  525. _next = value;
  526. RaisePropertyChanged(nameof(Next));
  527. }
  528. }
  529. }
  530. private class Class2 : NotifyingBase, INext
  531. {
  532. private string _bar;
  533. private INext _next;
  534. public string Bar
  535. {
  536. get { return _bar; }
  537. set
  538. {
  539. _bar = value;
  540. RaisePropertyChanged(nameof(Bar));
  541. }
  542. }
  543. public INext Next
  544. {
  545. get { return _next; }
  546. set
  547. {
  548. _next = value;
  549. RaisePropertyChanged(nameof(Next));
  550. }
  551. }
  552. }
  553. private class Class3 : Class1
  554. {
  555. }
  556. private class WithoutBar : NotifyingBase, INext
  557. {
  558. }
  559. private static Recorded<Notification<T>> OnNext<T>(long time, T value)
  560. {
  561. return new Recorded<Notification<T>>(time, Notification.CreateOnNext<T>(value));
  562. }
  563. }
  564. }