AvaloniaObjectTests_Binding.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. // Copyright (c) The Avalonia Project. All rights reserved.
  2. // Licensed under the MIT license. See licence.md file in the project root for full license information.
  3. using System;
  4. using System.Collections;
  5. using System.Collections.Generic;
  6. using System.Reactive.Linq;
  7. using System.Reactive.Subjects;
  8. using Microsoft.Reactive.Testing;
  9. using Avalonia.Data;
  10. using Avalonia.Logging;
  11. using Avalonia.UnitTests;
  12. using Xunit;
  13. using System.Threading.Tasks;
  14. using Avalonia.Platform;
  15. using System.Threading;
  16. using Moq;
  17. using System.Reactive.Disposables;
  18. using System.Reactive.Concurrency;
  19. using Avalonia.Threading;
  20. namespace Avalonia.Base.UnitTests
  21. {
  22. public class AvaloniaObjectTests_Binding
  23. {
  24. [Fact]
  25. public void Bind_Sets_Current_Value()
  26. {
  27. Class1 target = new Class1();
  28. Class1 source = new Class1();
  29. source.SetValue(Class1.FooProperty, "initial");
  30. target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty));
  31. Assert.Equal("initial", target.GetValue(Class1.FooProperty));
  32. }
  33. [Fact]
  34. public void Bind_NonGeneric_Sets_Current_Value()
  35. {
  36. Class1 target = new Class1();
  37. Class1 source = new Class1();
  38. source.SetValue(Class1.FooProperty, "initial");
  39. target.Bind((AvaloniaProperty)Class1.FooProperty, source.GetObservable(Class1.FooProperty));
  40. Assert.Equal("initial", target.GetValue(Class1.FooProperty));
  41. }
  42. [Fact]
  43. public void Bind_To_ValueType_Accepts_UnsetValue()
  44. {
  45. var target = new Class1();
  46. var source = new Subject<object>();
  47. target.Bind(Class1.QuxProperty, source);
  48. source.OnNext(6.7);
  49. source.OnNext(AvaloniaProperty.UnsetValue);
  50. Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
  51. Assert.False(target.IsSet(Class1.QuxProperty));
  52. }
  53. [Fact]
  54. public void OneTime_Binding_Ignores_UnsetValue()
  55. {
  56. var target = new Class1();
  57. var source = new Subject<object>();
  58. target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source));
  59. source.OnNext(AvaloniaProperty.UnsetValue);
  60. Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
  61. source.OnNext(6.7);
  62. Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
  63. }
  64. [Fact]
  65. public void OneTime_Binding_Ignores_Binding_Errors()
  66. {
  67. var target = new Class1();
  68. var source = new Subject<object>();
  69. target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source));
  70. source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error));
  71. Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
  72. source.OnNext(6.7);
  73. Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
  74. }
  75. [Fact]
  76. public void Bind_Throws_Exception_For_Unregistered_Property()
  77. {
  78. Class1 target = new Class1();
  79. Assert.Throws<ArgumentException>(() =>
  80. {
  81. target.Bind(Class2.BarProperty, Observable.Return("foo"));
  82. });
  83. }
  84. [Fact]
  85. public void Bind_Sets_Subsequent_Value()
  86. {
  87. Class1 target = new Class1();
  88. Class1 source = new Class1();
  89. source.SetValue(Class1.FooProperty, "initial");
  90. target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty));
  91. source.SetValue(Class1.FooProperty, "subsequent");
  92. Assert.Equal("subsequent", target.GetValue(Class1.FooProperty));
  93. }
  94. [Fact]
  95. public void Bind_Ignores_Invalid_Value_Type()
  96. {
  97. Class1 target = new Class1();
  98. target.Bind((AvaloniaProperty)Class1.FooProperty, Observable.Return((object)123));
  99. Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
  100. }
  101. [Fact]
  102. public void Observable_Is_Unsubscribed_When_Subscription_Disposed()
  103. {
  104. var scheduler = new TestScheduler();
  105. var source = scheduler.CreateColdObservable<object>();
  106. var target = new Class1();
  107. var subscription = target.Bind(Class1.FooProperty, source);
  108. Assert.Equal(1, source.Subscriptions.Count);
  109. Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe);
  110. subscription.Dispose();
  111. Assert.Equal(1, source.Subscriptions.Count);
  112. Assert.Equal(0, source.Subscriptions[0].Unsubscribe);
  113. }
  114. [Fact]
  115. public void Two_Way_Separate_Binding_Works()
  116. {
  117. Class1 obj1 = new Class1();
  118. Class1 obj2 = new Class1();
  119. obj1.SetValue(Class1.FooProperty, "initial1");
  120. obj2.SetValue(Class1.FooProperty, "initial2");
  121. obj1.Bind(Class1.FooProperty, obj2.GetObservable(Class1.FooProperty));
  122. obj2.Bind(Class1.FooProperty, obj1.GetObservable(Class1.FooProperty));
  123. Assert.Equal("initial2", obj1.GetValue(Class1.FooProperty));
  124. Assert.Equal("initial2", obj2.GetValue(Class1.FooProperty));
  125. obj1.SetValue(Class1.FooProperty, "first");
  126. Assert.Equal("first", obj1.GetValue(Class1.FooProperty));
  127. Assert.Equal("first", obj2.GetValue(Class1.FooProperty));
  128. obj2.SetValue(Class1.FooProperty, "second");
  129. Assert.Equal("second", obj1.GetValue(Class1.FooProperty));
  130. Assert.Equal("second", obj2.GetValue(Class1.FooProperty));
  131. obj1.SetValue(Class1.FooProperty, "third");
  132. Assert.Equal("third", obj1.GetValue(Class1.FooProperty));
  133. Assert.Equal("third", obj2.GetValue(Class1.FooProperty));
  134. }
  135. [Fact]
  136. public void Two_Way_Binding_With_Priority_Works()
  137. {
  138. Class1 obj1 = new Class1();
  139. Class1 obj2 = new Class1();
  140. obj1.SetValue(Class1.FooProperty, "initial1", BindingPriority.Style);
  141. obj2.SetValue(Class1.FooProperty, "initial2", BindingPriority.Style);
  142. obj1.Bind(Class1.FooProperty, obj2.GetObservable(Class1.FooProperty), BindingPriority.Style);
  143. obj2.Bind(Class1.FooProperty, obj1.GetObservable(Class1.FooProperty), BindingPriority.Style);
  144. Assert.Equal("initial2", obj1.GetValue(Class1.FooProperty));
  145. Assert.Equal("initial2", obj2.GetValue(Class1.FooProperty));
  146. obj1.SetValue(Class1.FooProperty, "first", BindingPriority.Style);
  147. Assert.Equal("first", obj1.GetValue(Class1.FooProperty));
  148. Assert.Equal("first", obj2.GetValue(Class1.FooProperty));
  149. obj2.SetValue(Class1.FooProperty, "second", BindingPriority.Style);
  150. Assert.Equal("second", obj1.GetValue(Class1.FooProperty));
  151. Assert.Equal("second", obj2.GetValue(Class1.FooProperty));
  152. obj1.SetValue(Class1.FooProperty, "third", BindingPriority.Style);
  153. Assert.Equal("third", obj1.GetValue(Class1.FooProperty));
  154. Assert.Equal("third", obj2.GetValue(Class1.FooProperty));
  155. }
  156. [Fact]
  157. public void Local_Binding_Overwrites_Local_Value()
  158. {
  159. var target = new Class1();
  160. var binding = new Subject<string>();
  161. target.Bind(Class1.FooProperty, binding);
  162. binding.OnNext("first");
  163. Assert.Equal("first", target.GetValue(Class1.FooProperty));
  164. target.SetValue(Class1.FooProperty, "second");
  165. Assert.Equal("second", target.GetValue(Class1.FooProperty));
  166. binding.OnNext("third");
  167. Assert.Equal("third", target.GetValue(Class1.FooProperty));
  168. }
  169. [Fact]
  170. public void StyleBinding_Overrides_Default_Value()
  171. {
  172. Class1 target = new Class1();
  173. target.Bind(Class1.FooProperty, Single("stylevalue"), BindingPriority.Style);
  174. Assert.Equal("stylevalue", target.GetValue(Class1.FooProperty));
  175. }
  176. [Fact]
  177. public void this_Operator_Returns_Value_Property()
  178. {
  179. Class1 target = new Class1();
  180. target.SetValue(Class1.FooProperty, "newvalue");
  181. Assert.Equal("newvalue", target[Class1.FooProperty]);
  182. }
  183. [Fact]
  184. public void this_Operator_Sets_Value_Property()
  185. {
  186. Class1 target = new Class1();
  187. target[Class1.FooProperty] = "newvalue";
  188. Assert.Equal("newvalue", target.GetValue(Class1.FooProperty));
  189. }
  190. [Fact]
  191. public void this_Operator_Doesnt_Accept_Observable()
  192. {
  193. Class1 target = new Class1();
  194. Assert.Throws<ArgumentException>(() =>
  195. {
  196. target[Class1.FooProperty] = Observable.Return("newvalue");
  197. });
  198. }
  199. [Fact]
  200. public void this_Operator_Binds_One_Way()
  201. {
  202. Class1 target1 = new Class1();
  203. Class2 target2 = new Class2();
  204. IndexerDescriptor binding = Class2.BarProperty.Bind().WithMode(BindingMode.OneWay);
  205. target1.SetValue(Class1.FooProperty, "first");
  206. target2[binding] = target1[!Class1.FooProperty];
  207. target1.SetValue(Class1.FooProperty, "second");
  208. Assert.Equal("second", target2.GetValue(Class2.BarProperty));
  209. }
  210. [Fact]
  211. public void this_Operator_Binds_Two_Way()
  212. {
  213. Class1 target1 = new Class1();
  214. Class1 target2 = new Class1();
  215. target1.SetValue(Class1.FooProperty, "first");
  216. target2[!Class1.FooProperty] = target1[!!Class1.FooProperty];
  217. Assert.Equal("first", target2.GetValue(Class1.FooProperty));
  218. target1.SetValue(Class1.FooProperty, "second");
  219. Assert.Equal("second", target2.GetValue(Class1.FooProperty));
  220. target2.SetValue(Class1.FooProperty, "third");
  221. Assert.Equal("third", target1.GetValue(Class1.FooProperty));
  222. }
  223. [Fact]
  224. public void this_Operator_Binds_One_Time()
  225. {
  226. Class1 target1 = new Class1();
  227. Class1 target2 = new Class1();
  228. target1.SetValue(Class1.FooProperty, "first");
  229. target2[!Class1.FooProperty] = target1[Class1.FooProperty.Bind().WithMode(BindingMode.OneTime)];
  230. target1.SetValue(Class1.FooProperty, "second");
  231. Assert.Equal("first", target2.GetValue(Class1.FooProperty));
  232. }
  233. [Fact]
  234. public void BindingError_Does_Not_Cause_Target_Update()
  235. {
  236. var target = new Class1();
  237. var source = new Subject<object>();
  238. target.Bind(Class1.QuxProperty, source);
  239. source.OnNext(6.7);
  240. source.OnNext(new BindingNotification(
  241. new InvalidOperationException("Foo"),
  242. BindingErrorType.Error));
  243. Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
  244. }
  245. [Fact]
  246. public void BindingNotification_With_FallbackValue_Causes_Target_Update()
  247. {
  248. var target = new Class1();
  249. var source = new Subject<object>();
  250. target.Bind(Class1.QuxProperty, source);
  251. source.OnNext(6.7);
  252. source.OnNext(new BindingNotification(
  253. new InvalidOperationException("Foo"),
  254. BindingErrorType.Error,
  255. 8.9));
  256. Assert.Equal(8.9, target.GetValue(Class1.QuxProperty));
  257. }
  258. [Fact]
  259. public void Bind_Logs_Binding_Error()
  260. {
  261. var target = new Class1();
  262. var source = new Subject<object>();
  263. var called = false;
  264. var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}";
  265. LogCallback checkLogMessage = (level, area, src, mt, pv) =>
  266. {
  267. if (level == LogEventLevel.Error &&
  268. area == LogArea.Binding &&
  269. mt == expectedMessageTemplate)
  270. {
  271. called = true;
  272. }
  273. };
  274. using (TestLogSink.Start(checkLogMessage))
  275. {
  276. target.Bind(Class1.QuxProperty, source);
  277. source.OnNext(6.7);
  278. source.OnNext(new BindingNotification(
  279. new InvalidOperationException("Foo"),
  280. BindingErrorType.Error));
  281. Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
  282. Assert.True(called);
  283. }
  284. }
  285. [Fact]
  286. public async void Bind_With_Scheduler_Executes_On_Scheduler()
  287. {
  288. var target = new Class1();
  289. var source = new Subject<object>();
  290. var currentThreadId = Thread.CurrentThread.ManagedThreadId;
  291. var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
  292. threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
  293. .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
  294. using (AvaloniaLocator.EnterScope())
  295. {
  296. AvaloniaLocator.CurrentMutable.Bind<IPlatformThreadingInterface>().ToConstant(threadingInterfaceMock.Object);
  297. AvaloniaLocator.CurrentMutable.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance);
  298. target.Bind(Class1.QuxProperty, source);
  299. await Task.Run(() => source.OnNext(6.7));
  300. }
  301. }
  302. /// <summary>
  303. /// Returns an observable that returns a single value but does not complete.
  304. /// </summary>
  305. /// <typeparam name="T">The type of the observable.</typeparam>
  306. /// <param name="value">The value.</param>
  307. /// <returns>The observable.</returns>
  308. private IObservable<T> Single<T>(T value)
  309. {
  310. return Observable.Never<T>().StartWith(value);
  311. }
  312. private class Class1 : AvaloniaObject
  313. {
  314. public static readonly StyledProperty<string> FooProperty =
  315. AvaloniaProperty.Register<Class1, string>("Foo", "foodefault");
  316. public static readonly StyledProperty<double> QuxProperty =
  317. AvaloniaProperty.Register<Class1, double>("Qux", 5.6);
  318. }
  319. private class Class2 : Class1
  320. {
  321. public static readonly StyledProperty<string> BarProperty =
  322. AvaloniaProperty.Register<Class2, string>("Bar", "bardefault");
  323. }
  324. private class TestOneTimeBinding : IBinding
  325. {
  326. private IObservable<object> _source;
  327. public TestOneTimeBinding(IObservable<object> source)
  328. {
  329. _source = source;
  330. }
  331. public InstancedBinding Initiate(
  332. IAvaloniaObject target,
  333. AvaloniaProperty targetProperty,
  334. object anchor = null,
  335. bool enableDataValidation = false)
  336. {
  337. return new InstancedBinding(_source, BindingMode.OneTime);
  338. }
  339. }
  340. }
  341. }