BindingTests.cs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. // Copyright (c) The Perspex 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.Reactive.Linq;
  5. using System.Reactive.Subjects;
  6. using Moq;
  7. using Perspex.Controls;
  8. using Perspex.Markup.Data;
  9. using Perspex.Markup.Xaml.Data;
  10. using Xunit;
  11. namespace Perspex.Markup.Xaml.UnitTests.Data
  12. {
  13. public class BindingTests
  14. {
  15. [Fact]
  16. public void OneWay_Binding_Should_Be_Set_Up()
  17. {
  18. var target = CreateTarget();
  19. var binding = new Binding
  20. {
  21. SourcePropertyPath = "Foo",
  22. Mode = BindingMode.OneWay,
  23. };
  24. binding.Bind(target.Object, TextBox.TextProperty);
  25. target.Verify(x => x.Bind(
  26. TextBox.TextProperty,
  27. It.IsAny<IObservable<object>>(),
  28. BindingPriority.LocalValue));
  29. }
  30. [Fact]
  31. public void TwoWay_Binding_Should_Be_Set_Up()
  32. {
  33. var target = CreateTarget();
  34. var binding = new Binding
  35. {
  36. SourcePropertyPath = "Foo",
  37. Mode = BindingMode.TwoWay,
  38. };
  39. binding.Bind(target.Object, TextBox.TextProperty);
  40. target.Verify(x => x.BindTwoWay(
  41. TextBox.TextProperty,
  42. It.IsAny<ISubject<object>>(),
  43. BindingPriority.LocalValue));
  44. }
  45. [Fact]
  46. public void OneTime_Binding_Should_Be_Set_Up()
  47. {
  48. var dataContext = new BehaviorSubject<object>(null);
  49. var expression = new BehaviorSubject<object>(null);
  50. var target = CreateTarget(dataContext: dataContext);
  51. var binding = new Binding
  52. {
  53. SourcePropertyPath = "Foo",
  54. Mode = BindingMode.OneTime,
  55. };
  56. binding.Bind(target.Object, TextBox.TextProperty, expression);
  57. target.Verify(x => x.SetValue(
  58. (PerspexProperty)TextBox.TextProperty,
  59. null,
  60. BindingPriority.LocalValue));
  61. target.ResetCalls();
  62. expression.OnNext("foo");
  63. dataContext.OnNext(1);
  64. target.Verify(x => x.SetValue(
  65. (PerspexProperty)TextBox.TextProperty,
  66. "foo",
  67. BindingPriority.LocalValue));
  68. }
  69. [Fact]
  70. public void OneWayToSource_Binding_Should_Be_Set_Up()
  71. {
  72. var textObservable = new Mock<IObservable<string>>();
  73. var expression = new Mock<ISubject<object>>();
  74. var target = CreateTarget(text: textObservable.Object);
  75. var binding = new Binding
  76. {
  77. SourcePropertyPath = "Foo",
  78. Mode = BindingMode.OneWayToSource,
  79. };
  80. binding.Bind(target.Object, TextBox.TextProperty, expression.Object);
  81. textObservable.Verify(x => x.Subscribe(expression.Object));
  82. }
  83. [Fact]
  84. public void Default_BindingMode_Should_Be_Used()
  85. {
  86. var target = CreateTarget(null);
  87. var binding = new Binding
  88. {
  89. SourcePropertyPath = "Foo",
  90. };
  91. binding.Bind(target.Object, TextBox.TextProperty);
  92. // Default for TextBox.Text is two-way.
  93. target.Verify(x => x.BindTwoWay(
  94. TextBox.TextProperty,
  95. It.IsAny<ISubject<object>>(),
  96. BindingPriority.LocalValue));
  97. }
  98. [Fact]
  99. public void DataContext_Binding_Should_Use_Parent_DataContext()
  100. {
  101. var parentDataContext = Mock.Of<IHeadered>(x => x.Header == (object)"Foo");
  102. var parent = new Decorator
  103. {
  104. Child = new Control(),
  105. DataContext = parentDataContext,
  106. };
  107. var binding = new Binding
  108. {
  109. SourcePropertyPath = "Header",
  110. };
  111. binding.Bind(parent.Child, Control.DataContextProperty);
  112. Assert.Equal("Foo", parent.Child.DataContext);
  113. parentDataContext = Mock.Of<IHeadered>(x => x.Header == (object)"Bar");
  114. parent.DataContext = parentDataContext;
  115. Assert.Equal("Bar", parent.Child.DataContext);
  116. }
  117. [Fact]
  118. public void Should_Use_DefaultValueConverter_When_No_Converter_Specified()
  119. {
  120. var target = CreateTarget(null);
  121. var binding = new Binding
  122. {
  123. SourcePropertyPath = "Foo",
  124. };
  125. var result = binding.CreateExpressionSubject(target.Object, TextBox.TextProperty);
  126. Assert.IsType<DefaultValueConverter>(((ExpressionSubject)result).Converter);
  127. }
  128. [Fact]
  129. public void Should_Use_Supplied_Converter()
  130. {
  131. var target = CreateTarget(null);
  132. var converter = new Mock<IValueConverter>();
  133. var binding = new Binding
  134. {
  135. Converter = converter.Object,
  136. SourcePropertyPath = "Foo",
  137. };
  138. var result = binding.CreateExpressionSubject(target.Object, TextBox.TextProperty);
  139. Assert.Same(converter.Object, ((ExpressionSubject)result).Converter);
  140. }
  141. /// <summary>
  142. /// Tests a problem discovered with ListBox with selection.
  143. /// </summary>
  144. /// <remarks>
  145. /// - Items is bound to DataContext first, followed by say SelectedIndex
  146. /// - When the ListBox is removed from the visual tree, DataContext becomes null (as it's
  147. /// inherited)
  148. /// - This changes Items to null, which changes SelectedIndex to null as there are no
  149. /// longer any items
  150. /// - However, the news that DataContext is now null hasn't yet reached the SelectedIndex
  151. /// binding and so the unselection is sent back to the ViewModel
  152. /// </remarks>
  153. [Fact]
  154. public void Should_Not_Write_To_Old_DataContext()
  155. {
  156. var vm = new OldDataContextViewModel();
  157. var target = new OldDataContextTest();
  158. var fooBinding = new Binding
  159. {
  160. SourcePropertyPath = "Foo",
  161. Mode = BindingMode.TwoWay,
  162. };
  163. var barBinding = new Binding
  164. {
  165. SourcePropertyPath = "Bar",
  166. Mode = BindingMode.TwoWay,
  167. };
  168. // Bind Foo and Bar to the VM.
  169. fooBinding.Bind(target, OldDataContextTest.FooProperty);
  170. barBinding.Bind(target, OldDataContextTest.BarProperty);
  171. target.DataContext = vm;
  172. // Make sure the control's Foo and Bar properties are read from the VM
  173. Assert.Equal(1, target.GetValue(OldDataContextTest.FooProperty));
  174. Assert.Equal(2, target.GetValue(OldDataContextTest.BarProperty));
  175. // Set DataContext to null.
  176. target.DataContext = null;
  177. // Foo and Bar are no longer bound so they return 0, their default value.
  178. Assert.Equal(0, target.GetValue(OldDataContextTest.FooProperty));
  179. Assert.Equal(0, target.GetValue(OldDataContextTest.BarProperty));
  180. // The problem was here - DataContext is now null, setting Foo to 0. Bar is bound to
  181. // Foo so Bar also gets set to 0. However the Bar binding still had a reference to
  182. // the VM and so vm.Bar was set to 0 erroneously.
  183. Assert.Equal(1, vm.Foo);
  184. Assert.Equal(2, vm.Bar);
  185. }
  186. private Mock<IObservablePropertyBag> CreateTarget(object dataContext)
  187. {
  188. return CreateTarget(dataContext: Observable.Never<object>().StartWith(dataContext));
  189. }
  190. private Mock<IObservablePropertyBag> CreateTarget(
  191. IObservable<object> dataContext = null,
  192. IObservable<string> text = null)
  193. {
  194. var result = new Mock<IObservablePropertyBag>();
  195. dataContext = dataContext ?? Observable.Never<object>().StartWith((object)null);
  196. text = text ?? Observable.Never<string>().StartWith((string)null);
  197. result.Setup(x => x.GetObservable(Control.DataContextProperty)).Returns(dataContext);
  198. result.Setup(x => x.GetObservable((PerspexProperty)Control.DataContextProperty)).Returns(dataContext);
  199. result.Setup(x => x.GetObservable((PerspexProperty)TextBox.TextProperty)).Returns(text);
  200. return result;
  201. }
  202. private class OldDataContextViewModel
  203. {
  204. public int Foo { get; set; } = 1;
  205. public int Bar { get; set; } = 2;
  206. }
  207. private class OldDataContextTest : Control
  208. {
  209. public static readonly PerspexProperty<int> FooProperty =
  210. PerspexProperty.Register<OldDataContextTest, int>("Foo");
  211. public static readonly PerspexProperty<int> BarProperty =
  212. PerspexProperty.Register<OldDataContextTest, int>("Bar");
  213. public OldDataContextTest()
  214. {
  215. Bind(BarProperty, GetObservable(FooProperty));
  216. }
  217. }
  218. }
  219. }