BindingTests_DataValidation.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. using System;
  2. using System.Collections;
  3. using System.ComponentModel;
  4. using Avalonia.Controls;
  5. using Avalonia.Data;
  6. using Avalonia.Styling;
  7. using Avalonia.UnitTests;
  8. using Xunit;
  9. #nullable enable
  10. namespace Avalonia.Markup.UnitTests.Data
  11. {
  12. public class BindingTests_DataValidation
  13. {
  14. public abstract class TestBase<T> : ScopedTestBase
  15. where T : AvaloniaProperty<int>
  16. {
  17. [Fact]
  18. public void Setter_Exception_Causes_DataValidation_Error()
  19. {
  20. var (target, property) = CreateTarget();
  21. var binding = new Binding(nameof(ExceptionValidatingModel.Value))
  22. {
  23. Mode = BindingMode.TwoWay
  24. };
  25. target.DataContext = new ExceptionValidatingModel();
  26. target.Bind(property, binding);
  27. Assert.Equal(20, target.GetValue(property));
  28. target.SetValue(property, 200);
  29. Assert.Equal(200, target.GetValue(property));
  30. Assert.IsType<ArgumentOutOfRangeException>(target.DataValidationError);
  31. target.SetValue(property, 10);
  32. Assert.Equal(10, target.GetValue(property));
  33. Assert.Null(target.DataValidationError);
  34. }
  35. [Fact]
  36. public void Indei_Error_Causes_DataValidation_Error()
  37. {
  38. var (target, property) = CreateTarget();
  39. var binding = new Binding(nameof(IndeiValidatingModel.Value))
  40. {
  41. Mode = BindingMode.TwoWay
  42. };
  43. target.DataContext = new IndeiValidatingModel();
  44. target.Bind(property, binding);
  45. Assert.Equal(20, target.GetValue(property));
  46. target.SetValue(property, 200);
  47. Assert.Equal(200, target.GetValue(property));
  48. Assert.IsType<DataValidationException>(target.DataValidationError);
  49. Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
  50. target.SetValue(property, 10);
  51. Assert.Equal(10, target.GetValue(property));
  52. Assert.Null(target.DataValidationError);
  53. }
  54. [Fact]
  55. public void Disposing_Binding_Subscription_Clears_DataValidation()
  56. {
  57. var (target, property) = CreateTarget();
  58. var binding = new Binding(nameof(ExceptionValidatingModel.Value))
  59. {
  60. Mode = BindingMode.TwoWay
  61. };
  62. target.DataContext = new IndeiValidatingModel
  63. {
  64. Value = 200,
  65. };
  66. var sub = target.Bind(property, binding);
  67. Assert.Equal(200, target.GetValue(property));
  68. Assert.IsType<DataValidationException>(target.DataValidationError);
  69. sub.Dispose();
  70. Assert.Null(target.DataValidationError);
  71. }
  72. private protected abstract (DataValidationTestControl, T) CreateTarget();
  73. }
  74. public class DirectPropertyTests : TestBase<DirectPropertyBase<int>>
  75. {
  76. private protected override (DataValidationTestControl, DirectPropertyBase<int>) CreateTarget()
  77. {
  78. return (new ValidatedDirectPropertyClass(), ValidatedDirectPropertyClass.ValueProperty);
  79. }
  80. }
  81. public class StyledPropertyTests : TestBase<StyledProperty<int>>
  82. {
  83. [Fact]
  84. public void Style_Binding_Supports_Data_Validation()
  85. {
  86. var (target, property) = CreateTarget();
  87. var binding = new Binding(nameof(IndeiValidatingModel.Value))
  88. {
  89. Mode = BindingMode.TwoWay
  90. };
  91. var model = new IndeiValidatingModel();
  92. var root = new TestRoot
  93. {
  94. DataContext = model,
  95. Styles =
  96. {
  97. new Style(x => x.Is<DataValidationTestControl>())
  98. {
  99. Setters =
  100. {
  101. new Setter(property, binding)
  102. }
  103. }
  104. },
  105. Child = target,
  106. };
  107. root.LayoutManager.ExecuteInitialLayoutPass();
  108. Assert.Equal(20, target.GetValue(property));
  109. model.Value = 200;
  110. Assert.Equal(200, target.GetValue(property));
  111. Assert.IsType<DataValidationException>(target.DataValidationError);
  112. Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
  113. model.Value = 10;
  114. Assert.Equal(10, target.GetValue(property));
  115. Assert.Null(target.DataValidationError);
  116. }
  117. [Fact]
  118. public void Style_With_Activator_Binding_Supports_Data_Validation()
  119. {
  120. var (target, property) = CreateTarget();
  121. var binding = new Binding(nameof(IndeiValidatingModel.Value))
  122. {
  123. Mode = BindingMode.TwoWay
  124. };
  125. var model = new IndeiValidatingModel
  126. {
  127. Value = 200,
  128. };
  129. var root = new TestRoot
  130. {
  131. DataContext = model,
  132. Styles =
  133. {
  134. new Style(x => x.Is<DataValidationTestControl>().Class("foo"))
  135. {
  136. Setters =
  137. {
  138. new Setter(property, binding)
  139. }
  140. }
  141. },
  142. Child = target,
  143. };
  144. root.LayoutManager.ExecuteInitialLayoutPass();
  145. target.Classes.Add("foo");
  146. Assert.Equal(200, target.GetValue(property));
  147. Assert.IsType<DataValidationException>(target.DataValidationError);
  148. Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
  149. target.Classes.Remove("foo");
  150. Assert.Equal(0, target.GetValue(property));
  151. Assert.Null(target.DataValidationError);
  152. target.Classes.Add("foo");
  153. Assert.IsType<DataValidationException>(target.DataValidationError);
  154. Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
  155. model.Value = 10;
  156. Assert.Equal(10, target.GetValue(property));
  157. Assert.Null(target.DataValidationError);
  158. }
  159. [Fact]
  160. public void Data_Validation_Can_Switch_Between_Style_And_LocalValue_Binding()
  161. {
  162. var (target, property) = CreateTarget();
  163. var model1 = new IndeiValidatingModel { Value = 200 };
  164. var model2 = new IndeiValidatingModel { Value = 300 };
  165. var binding1 = new Binding(nameof(IndeiValidatingModel.Value));
  166. var binding2 = new Binding(nameof(IndeiValidatingModel.Value)) { Source = model2 };
  167. var root = new TestRoot
  168. {
  169. DataContext = model1,
  170. Styles =
  171. {
  172. new Style(x => x.Is<DataValidationTestControl>())
  173. {
  174. Setters =
  175. {
  176. new Setter(property, binding1)
  177. }
  178. }
  179. },
  180. Child = target,
  181. };
  182. root.LayoutManager.ExecuteInitialLayoutPass();
  183. Assert.Equal(200, target.GetValue(property));
  184. Assert.IsType<DataValidationException>(target.DataValidationError);
  185. Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
  186. var sub = target.Bind(property, binding2);
  187. Assert.Equal(300, target.GetValue(property));
  188. Assert.Equal("Invalid value: 300.", target.DataValidationError?.Message);
  189. sub.Dispose();
  190. Assert.Equal(200, target.GetValue(property));
  191. Assert.IsType<DataValidationException>(target.DataValidationError);
  192. Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
  193. }
  194. [Fact]
  195. public void Data_Validation_Can_Switch_Between_Style_And_StyleTrigger_Binding()
  196. {
  197. var (target, property) = CreateTarget();
  198. var model1 = new IndeiValidatingModel { Value = 200 };
  199. var model2 = new IndeiValidatingModel { Value = 300 };
  200. var binding1 = new Binding(nameof(IndeiValidatingModel.Value));
  201. var binding2 = new Binding(nameof(IndeiValidatingModel.Value)) { Source = model2 };
  202. var root = new TestRoot
  203. {
  204. DataContext = model1,
  205. Styles =
  206. {
  207. new Style(x => x.Is<DataValidationTestControl>())
  208. {
  209. Setters =
  210. {
  211. new Setter(property, binding1)
  212. }
  213. },
  214. new Style(x => x.Is<DataValidationTestControl>().Class("foo"))
  215. {
  216. Setters =
  217. {
  218. new Setter(property, binding2)
  219. }
  220. },
  221. },
  222. Child = target,
  223. };
  224. root.LayoutManager.ExecuteInitialLayoutPass();
  225. Assert.Equal(200, target.GetValue(property));
  226. Assert.IsType<DataValidationException>(target.DataValidationError);
  227. Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
  228. target.Classes.Add("foo");
  229. Assert.Equal(300, target.GetValue(property));
  230. Assert.Equal("Invalid value: 300.", target.DataValidationError?.Message);
  231. target.Classes.Remove("foo");
  232. Assert.Equal(200, target.GetValue(property));
  233. Assert.IsType<DataValidationException>(target.DataValidationError);
  234. Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
  235. }
  236. private protected override (DataValidationTestControl, StyledProperty<int>) CreateTarget()
  237. {
  238. return (new ValidatedStyledPropertyClass(), ValidatedStyledPropertyClass.ValueProperty);
  239. }
  240. }
  241. internal class DataValidationTestControl : Control
  242. {
  243. public Exception? DataValidationError { get; protected set; }
  244. }
  245. private class ValidatedStyledPropertyClass : DataValidationTestControl
  246. {
  247. public static readonly StyledProperty<int> ValueProperty =
  248. AvaloniaProperty.Register<ValidatedStyledPropertyClass, int>(
  249. "Value",
  250. enableDataValidation: true);
  251. public int Value
  252. {
  253. get => GetValue(ValueProperty);
  254. set => SetValue(ValueProperty, value);
  255. }
  256. protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
  257. {
  258. if (property == ValueProperty)
  259. {
  260. DataValidationError = state.HasAnyFlag(BindingValueType.DataValidationError) ? error : null;
  261. }
  262. }
  263. }
  264. private class ValidatedDirectPropertyClass : DataValidationTestControl
  265. {
  266. public static readonly DirectProperty<ValidatedDirectPropertyClass, int> ValueProperty =
  267. AvaloniaProperty.RegisterDirect<ValidatedDirectPropertyClass, int>(
  268. "Value",
  269. o => o.Value,
  270. (o, v) => o.Value = v,
  271. enableDataValidation: true);
  272. private int _value;
  273. public int Value
  274. {
  275. get => _value;
  276. set => SetAndRaise(ValueProperty, ref _value, value);
  277. }
  278. protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
  279. {
  280. if (property == ValueProperty)
  281. {
  282. DataValidationError = state.HasAnyFlag(BindingValueType.DataValidationError) ? error : null;
  283. }
  284. }
  285. }
  286. private class ExceptionValidatingModel
  287. {
  288. public const int MaxValue = 100;
  289. private int _value = 20;
  290. public int Value
  291. {
  292. get => _value;
  293. set
  294. {
  295. if (value > MaxValue)
  296. throw new ArgumentOutOfRangeException(nameof(value));
  297. _value = value;
  298. }
  299. }
  300. }
  301. private class IndeiValidatingModel : INotifyDataErrorInfo
  302. {
  303. public const int MaxValue = 100;
  304. private bool _hasErrors;
  305. private int _value = 20;
  306. public int Value
  307. {
  308. get => _value;
  309. set
  310. {
  311. _value = value;
  312. HasErrors = value > MaxValue;
  313. }
  314. }
  315. public bool HasErrors
  316. {
  317. get => _hasErrors;
  318. private set
  319. {
  320. if (_hasErrors != value)
  321. {
  322. _hasErrors = value;
  323. ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
  324. }
  325. }
  326. }
  327. public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
  328. public IEnumerable GetErrors(string? propertyName)
  329. {
  330. if (propertyName == nameof(Value) && _value > MaxValue)
  331. yield return $"Invalid value: {_value}.";
  332. }
  333. }
  334. }
  335. }