AvaloniaObjectTests_Binding.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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.ComponentModel;
  5. using System.Reactive.Concurrency;
  6. using System.Reactive.Linq;
  7. using System.Reactive.Subjects;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. using Avalonia.Data;
  11. using Avalonia.Logging;
  12. using Avalonia.Markup.Xaml.Data;
  13. using Avalonia.Platform;
  14. using Avalonia.Threading;
  15. using Avalonia.UnitTests;
  16. using Microsoft.Reactive.Testing;
  17. using Moq;
  18. using Xunit;
  19. namespace Avalonia.Base.UnitTests
  20. {
  21. public class AvaloniaObjectTests_Binding
  22. {
  23. [Fact]
  24. public void Bind_Sets_Current_Value()
  25. {
  26. Class1 target = new Class1();
  27. Class1 source = new Class1();
  28. source.SetValue(Class1.FooProperty, "initial");
  29. target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty));
  30. Assert.Equal("initial", target.GetValue(Class1.FooProperty));
  31. }
  32. [Fact]
  33. public void Bind_NonGeneric_Sets_Current_Value()
  34. {
  35. Class1 target = new Class1();
  36. Class1 source = new Class1();
  37. source.SetValue(Class1.FooProperty, "initial");
  38. target.Bind((AvaloniaProperty)Class1.FooProperty, source.GetObservable(Class1.FooProperty));
  39. Assert.Equal("initial", target.GetValue(Class1.FooProperty));
  40. }
  41. [Fact]
  42. public void Bind_To_ValueType_Accepts_UnsetValue()
  43. {
  44. var target = new Class1();
  45. var source = new Subject<object>();
  46. target.Bind(Class1.QuxProperty, source);
  47. source.OnNext(6.7);
  48. source.OnNext(AvaloniaProperty.UnsetValue);
  49. Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
  50. Assert.False(target.IsSet(Class1.QuxProperty));
  51. }
  52. [Fact]
  53. public void OneTime_Binding_Ignores_UnsetValue()
  54. {
  55. var target = new Class1();
  56. var source = new Subject<object>();
  57. target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source));
  58. source.OnNext(AvaloniaProperty.UnsetValue);
  59. Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
  60. source.OnNext(6.7);
  61. Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
  62. }
  63. [Fact]
  64. public void OneTime_Binding_Ignores_Binding_Errors()
  65. {
  66. var target = new Class1();
  67. var source = new Subject<object>();
  68. target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source));
  69. source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error));
  70. Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
  71. source.OnNext(6.7);
  72. Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
  73. }
  74. [Fact]
  75. public void Bind_Throws_Exception_For_Unregistered_Property()
  76. {
  77. Class1 target = new Class1();
  78. Assert.Throws<ArgumentException>(() =>
  79. {
  80. target.Bind(Class2.BarProperty, Observable.Return("foo"));
  81. });
  82. }
  83. [Fact]
  84. public void Bind_Sets_Subsequent_Value()
  85. {
  86. Class1 target = new Class1();
  87. Class1 source = new Class1();
  88. source.SetValue(Class1.FooProperty, "initial");
  89. target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty));
  90. source.SetValue(Class1.FooProperty, "subsequent");
  91. Assert.Equal("subsequent", target.GetValue(Class1.FooProperty));
  92. }
  93. [Fact]
  94. public void Bind_Ignores_Invalid_Value_Type()
  95. {
  96. Class1 target = new Class1();
  97. target.Bind((AvaloniaProperty)Class1.FooProperty, Observable.Return((object)123));
  98. Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
  99. }
  100. [Fact]
  101. public void Observable_Is_Unsubscribed_When_Subscription_Disposed()
  102. {
  103. var scheduler = new TestScheduler();
  104. var source = scheduler.CreateColdObservable<object>();
  105. var target = new Class1();
  106. var subscription = target.Bind(Class1.FooProperty, source);
  107. Assert.Equal(1, source.Subscriptions.Count);
  108. Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe);
  109. subscription.Dispose();
  110. Assert.Equal(1, source.Subscriptions.Count);
  111. Assert.Equal(0, source.Subscriptions[0].Unsubscribe);
  112. }
  113. [Fact]
  114. public void Two_Way_Separate_Binding_Works()
  115. {
  116. Class1 obj1 = new Class1();
  117. Class1 obj2 = new Class1();
  118. obj1.SetValue(Class1.FooProperty, "initial1");
  119. obj2.SetValue(Class1.FooProperty, "initial2");
  120. obj1.Bind(Class1.FooProperty, obj2.GetObservable(Class1.FooProperty));
  121. obj2.Bind(Class1.FooProperty, obj1.GetObservable(Class1.FooProperty));
  122. Assert.Equal("initial2", obj1.GetValue(Class1.FooProperty));
  123. Assert.Equal("initial2", obj2.GetValue(Class1.FooProperty));
  124. obj1.SetValue(Class1.FooProperty, "first");
  125. Assert.Equal("first", obj1.GetValue(Class1.FooProperty));
  126. Assert.Equal("first", obj2.GetValue(Class1.FooProperty));
  127. obj2.SetValue(Class1.FooProperty, "second");
  128. Assert.Equal("second", obj1.GetValue(Class1.FooProperty));
  129. Assert.Equal("second", obj2.GetValue(Class1.FooProperty));
  130. obj1.SetValue(Class1.FooProperty, "third");
  131. Assert.Equal("third", obj1.GetValue(Class1.FooProperty));
  132. Assert.Equal("third", obj2.GetValue(Class1.FooProperty));
  133. }
  134. [Fact]
  135. public void Two_Way_Binding_With_Priority_Works()
  136. {
  137. Class1 obj1 = new Class1();
  138. Class1 obj2 = new Class1();
  139. obj1.SetValue(Class1.FooProperty, "initial1", BindingPriority.Style);
  140. obj2.SetValue(Class1.FooProperty, "initial2", BindingPriority.Style);
  141. obj1.Bind(Class1.FooProperty, obj2.GetObservable(Class1.FooProperty), BindingPriority.Style);
  142. obj2.Bind(Class1.FooProperty, obj1.GetObservable(Class1.FooProperty), BindingPriority.Style);
  143. Assert.Equal("initial2", obj1.GetValue(Class1.FooProperty));
  144. Assert.Equal("initial2", obj2.GetValue(Class1.FooProperty));
  145. obj1.SetValue(Class1.FooProperty, "first", BindingPriority.Style);
  146. Assert.Equal("first", obj1.GetValue(Class1.FooProperty));
  147. Assert.Equal("first", obj2.GetValue(Class1.FooProperty));
  148. obj2.SetValue(Class1.FooProperty, "second", BindingPriority.Style);
  149. Assert.Equal("second", obj1.GetValue(Class1.FooProperty));
  150. Assert.Equal("second", obj2.GetValue(Class1.FooProperty));
  151. obj1.SetValue(Class1.FooProperty, "third", BindingPriority.Style);
  152. Assert.Equal("third", obj1.GetValue(Class1.FooProperty));
  153. Assert.Equal("third", obj2.GetValue(Class1.FooProperty));
  154. }
  155. [Fact]
  156. public void Local_Binding_Overwrites_Local_Value()
  157. {
  158. var target = new Class1();
  159. var binding = new Subject<string>();
  160. target.Bind(Class1.FooProperty, binding);
  161. binding.OnNext("first");
  162. Assert.Equal("first", target.GetValue(Class1.FooProperty));
  163. target.SetValue(Class1.FooProperty, "second");
  164. Assert.Equal("second", target.GetValue(Class1.FooProperty));
  165. binding.OnNext("third");
  166. Assert.Equal("third", target.GetValue(Class1.FooProperty));
  167. }
  168. [Fact]
  169. public void StyleBinding_Overrides_Default_Value()
  170. {
  171. Class1 target = new Class1();
  172. target.Bind(Class1.FooProperty, Single("stylevalue"), BindingPriority.Style);
  173. Assert.Equal("stylevalue", target.GetValue(Class1.FooProperty));
  174. }
  175. [Fact]
  176. public void this_Operator_Returns_Value_Property()
  177. {
  178. Class1 target = new Class1();
  179. target.SetValue(Class1.FooProperty, "newvalue");
  180. Assert.Equal("newvalue", target[Class1.FooProperty]);
  181. }
  182. [Fact]
  183. public void this_Operator_Sets_Value_Property()
  184. {
  185. Class1 target = new Class1();
  186. target[Class1.FooProperty] = "newvalue";
  187. Assert.Equal("newvalue", target.GetValue(Class1.FooProperty));
  188. }
  189. [Fact]
  190. public void this_Operator_Doesnt_Accept_Observable()
  191. {
  192. Class1 target = new Class1();
  193. Assert.Throws<ArgumentException>(() =>
  194. {
  195. target[Class1.FooProperty] = Observable.Return("newvalue");
  196. });
  197. }
  198. [Fact]
  199. public void this_Operator_Binds_One_Way()
  200. {
  201. Class1 target1 = new Class1();
  202. Class2 target2 = new Class2();
  203. IndexerDescriptor binding = Class2.BarProperty.Bind().WithMode(BindingMode.OneWay);
  204. target1.SetValue(Class1.FooProperty, "first");
  205. target2[binding] = target1[!Class1.FooProperty];
  206. target1.SetValue(Class1.FooProperty, "second");
  207. Assert.Equal("second", target2.GetValue(Class2.BarProperty));
  208. }
  209. [Fact]
  210. public void this_Operator_Binds_Two_Way()
  211. {
  212. Class1 target1 = new Class1();
  213. Class1 target2 = new Class1();
  214. target1.SetValue(Class1.FooProperty, "first");
  215. target2[!Class1.FooProperty] = target1[!!Class1.FooProperty];
  216. Assert.Equal("first", target2.GetValue(Class1.FooProperty));
  217. target1.SetValue(Class1.FooProperty, "second");
  218. Assert.Equal("second", target2.GetValue(Class1.FooProperty));
  219. target2.SetValue(Class1.FooProperty, "third");
  220. Assert.Equal("third", target1.GetValue(Class1.FooProperty));
  221. }
  222. [Fact]
  223. public void this_Operator_Binds_One_Time()
  224. {
  225. Class1 target1 = new Class1();
  226. Class1 target2 = new Class1();
  227. target1.SetValue(Class1.FooProperty, "first");
  228. target2[!Class1.FooProperty] = target1[Class1.FooProperty.Bind().WithMode(BindingMode.OneTime)];
  229. target1.SetValue(Class1.FooProperty, "second");
  230. Assert.Equal("first", target2.GetValue(Class1.FooProperty));
  231. }
  232. [Fact]
  233. public void BindingError_Does_Not_Cause_Target_Update()
  234. {
  235. var target = new Class1();
  236. var source = new Subject<object>();
  237. target.Bind(Class1.QuxProperty, source);
  238. source.OnNext(6.7);
  239. source.OnNext(new BindingNotification(
  240. new InvalidOperationException("Foo"),
  241. BindingErrorType.Error));
  242. Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
  243. }
  244. [Fact]
  245. public void BindingNotification_With_FallbackValue_Causes_Target_Update()
  246. {
  247. var target = new Class1();
  248. var source = new Subject<object>();
  249. target.Bind(Class1.QuxProperty, source);
  250. source.OnNext(6.7);
  251. source.OnNext(new BindingNotification(
  252. new InvalidOperationException("Foo"),
  253. BindingErrorType.Error,
  254. 8.9));
  255. Assert.Equal(8.9, target.GetValue(Class1.QuxProperty));
  256. }
  257. [Fact]
  258. public void Bind_Logs_Binding_Error()
  259. {
  260. var target = new Class1();
  261. var source = new Subject<object>();
  262. var called = false;
  263. var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}";
  264. LogCallback checkLogMessage = (level, area, src, mt, pv) =>
  265. {
  266. if (level == LogEventLevel.Error &&
  267. area == LogArea.Binding &&
  268. mt == expectedMessageTemplate)
  269. {
  270. called = true;
  271. }
  272. };
  273. using (TestLogSink.Start(checkLogMessage))
  274. {
  275. target.Bind(Class1.QuxProperty, source);
  276. source.OnNext(6.7);
  277. source.OnNext(new BindingNotification(
  278. new InvalidOperationException("Foo"),
  279. BindingErrorType.Error));
  280. Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
  281. Assert.True(called);
  282. }
  283. }
  284. [Fact]
  285. public async void Bind_With_Scheduler_Executes_On_Scheduler()
  286. {
  287. var target = new Class1();
  288. var source = new Subject<object>();
  289. var currentThreadId = Thread.CurrentThread.ManagedThreadId;
  290. var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
  291. threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
  292. .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
  293. using (AvaloniaLocator.EnterScope())
  294. {
  295. AvaloniaLocator.CurrentMutable.Bind<IPlatformThreadingInterface>().ToConstant(threadingInterfaceMock.Object);
  296. AvaloniaLocator.CurrentMutable.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance);
  297. target.Bind(Class1.QuxProperty, source);
  298. await Task.Run(() => source.OnNext(6.7));
  299. }
  300. }
  301. [Theory]
  302. [InlineData(true)]
  303. [InlineData(false)]
  304. public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values(bool useXamlBinding)
  305. {
  306. var viewModel = new TestStackOverflowViewModel()
  307. {
  308. Value = 50
  309. };
  310. var target = new Class1();
  311. //note: if the initialization of the child binding is here target/child binding work fine!!!
  312. //var child = new Class1()
  313. //{
  314. // [~~Class1.DoubleValueProperty] = target[~~Class1.DoubleValueProperty]
  315. //};
  316. target.Bind(Class1.DoubleValueProperty,
  317. new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel });
  318. var child = new Class1();
  319. if (useXamlBinding)
  320. {
  321. child.Bind(Class1.DoubleValueProperty,
  322. new Binding("DoubleValue")
  323. {
  324. Mode = BindingMode.TwoWay,
  325. Source = target
  326. });
  327. }
  328. else
  329. {
  330. child[!!Class1.DoubleValueProperty] = target[!!Class1.DoubleValueProperty];
  331. }
  332. Assert.Equal(1, viewModel.SetterInvokedCount);
  333. //here in real life stack overflow exception is thrown issue #855 and #824
  334. target.DoubleValue = 51.001;
  335. Assert.Equal(2, viewModel.SetterInvokedCount);
  336. double expected = 51;
  337. Assert.Equal(expected, viewModel.Value);
  338. Assert.Equal(expected, target.DoubleValue);
  339. Assert.Equal(expected, child.DoubleValue);
  340. }
  341. /// <summary>
  342. /// Returns an observable that returns a single value but does not complete.
  343. /// </summary>
  344. /// <typeparam name="T">The type of the observable.</typeparam>
  345. /// <param name="value">The value.</param>
  346. /// <returns>The observable.</returns>
  347. private IObservable<T> Single<T>(T value)
  348. {
  349. return Observable.Never<T>().StartWith(value);
  350. }
  351. private class Class1 : AvaloniaObject
  352. {
  353. public static readonly StyledProperty<string> FooProperty =
  354. AvaloniaProperty.Register<Class1, string>("Foo", "foodefault");
  355. public static readonly StyledProperty<double> QuxProperty =
  356. AvaloniaProperty.Register<Class1, double>("Qux", 5.6);
  357. public static readonly StyledProperty<double> DoubleValueProperty =
  358. AvaloniaProperty.Register<Class1, double>(nameof(DoubleValue));
  359. public double DoubleValue
  360. {
  361. get { return GetValue(DoubleValueProperty); }
  362. set { SetValue(DoubleValueProperty, value); }
  363. }
  364. }
  365. private class Class2 : Class1
  366. {
  367. public static readonly StyledProperty<string> BarProperty =
  368. AvaloniaProperty.Register<Class2, string>("Bar", "bardefault");
  369. }
  370. private class TestOneTimeBinding : IBinding
  371. {
  372. private IObservable<object> _source;
  373. public TestOneTimeBinding(IObservable<object> source)
  374. {
  375. _source = source;
  376. }
  377. public InstancedBinding Initiate(
  378. IAvaloniaObject target,
  379. AvaloniaProperty targetProperty,
  380. object anchor = null,
  381. bool enableDataValidation = false)
  382. {
  383. return new InstancedBinding(_source, BindingMode.OneTime);
  384. }
  385. }
  386. private class TestStackOverflowViewModel : INotifyPropertyChanged
  387. {
  388. public int SetterInvokedCount { get; private set; }
  389. public const int MaxInvokedCount = 1000;
  390. private double _value;
  391. public event PropertyChangedEventHandler PropertyChanged;
  392. public double Value
  393. {
  394. get { return _value; }
  395. set
  396. {
  397. if (_value != value)
  398. {
  399. SetterInvokedCount++;
  400. if (SetterInvokedCount < MaxInvokedCount)
  401. {
  402. _value = (int)value;
  403. if (_value > 75) _value = 75;
  404. if (_value < 25) _value = 25;
  405. }
  406. else
  407. {
  408. _value = value;
  409. }
  410. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
  411. }
  412. }
  413. }
  414. }
  415. }
  416. }