BindingExpressionTests.DataValidation.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.ComponentModel.DataAnnotations;
  5. using Avalonia.Data;
  6. using Avalonia.UnitTests;
  7. using Xunit;
  8. #nullable enable
  9. namespace Avalonia.Base.UnitTests.Data.Core;
  10. public partial class BindingExpressionTests
  11. {
  12. [Fact]
  13. public void Root_Null_Should_Update_Data_Validation()
  14. {
  15. var target = CreateTargetWithSource<ViewModel?, string?>(
  16. null,
  17. o => o!.StringValue,
  18. enableDataValidation: true);
  19. AssertBindingError(
  20. target,
  21. TargetClass.StringProperty,
  22. new BindingChainException("Binding Source is null.", "StringValue", "(source)"),
  23. BindingErrorType.Error);
  24. }
  25. [Fact]
  26. public void Null_Value_In_Path_Should_Update_Data_Validation()
  27. {
  28. var data = new { Foo = default(ViewModel) };
  29. var target = CreateTargetWithSource(
  30. data,
  31. o => o.Foo!.StringValue!.Length,
  32. enableDataValidation: true);
  33. AssertBindingError(
  34. target,
  35. TargetClass.IntProperty,
  36. new BindingChainException("Value is null.", "Foo.StringValue.Length", "Foo"),
  37. BindingErrorType.Error);
  38. GC.KeepAlive(data);
  39. }
  40. [Fact]
  41. public void Invalid_Double_String_Should_Update_Data_Validation()
  42. {
  43. var data = new ViewModel { StringValue = "foo" };
  44. var target = CreateTargetWithSource(
  45. data,
  46. o => o.StringValue,
  47. enableDataValidation: true,
  48. targetProperty: TargetClass.DoubleProperty);
  49. AssertBindingError(
  50. target,
  51. TargetClass.DoubleProperty,
  52. new InvalidCastException("Could not convert 'foo' (System.String) to 'System.Double'."),
  53. BindingErrorType.Error);
  54. GC.KeepAlive(data);
  55. }
  56. [Fact]
  57. public void Invalid_Double_String_Should_Revert_To_FallbackValue()
  58. {
  59. var data = new ViewModel { StringValue = "foo" };
  60. var target = CreateTargetWithSource(
  61. data,
  62. o => o.StringValue,
  63. enableDataValidation: true,
  64. fallbackValue: 42.0,
  65. targetProperty: TargetClass.DoubleProperty);
  66. Assert.Equal(42.0, target.Double);
  67. AssertBindingError(
  68. target,
  69. TargetClass.DoubleProperty,
  70. new InvalidCastException("Could not convert 'foo' (System.String) to 'System.Double'."),
  71. BindingErrorType.Error);
  72. GC.KeepAlive(data);
  73. }
  74. [Fact]
  75. public void Setter_Exception_Does_Not_Cause_DataValidationError_When_Data_Validation_Not_Enabled()
  76. {
  77. var data = new ExceptionViewModel { MustBePositive = 5 };
  78. var target = CreateTargetWithSource(
  79. data,
  80. o => o.MustBePositive,
  81. enableDataValidation: false,
  82. mode: BindingMode.TwoWay);
  83. target.Int = -5;
  84. // TODO: Should this be 5?
  85. Assert.Equal(-5, target.Int);
  86. Assert.Equal(5, data.MustBePositive);
  87. AssertNoError(target, TargetClass.IntProperty);
  88. GC.KeepAlive(data);
  89. }
  90. [Fact]
  91. public void Setter_Exception_Updates_Data_Validation()
  92. {
  93. var data = new ExceptionViewModel { MustBePositive = 5 };
  94. var target = CreateTargetWithSource(
  95. data,
  96. o => o.MustBePositive,
  97. enableDataValidation: true,
  98. mode: BindingMode.TwoWay);
  99. target.Int = -5;
  100. // TODO: Should this be 5?
  101. Assert.Equal(-5, target.Int);
  102. Assert.Equal(5, data.MustBePositive);
  103. AssertBindingError(
  104. target,
  105. TargetClass.IntProperty,
  106. new ArgumentOutOfRangeException("value"),
  107. BindingErrorType.DataValidationError);
  108. GC.KeepAlive(data);
  109. }
  110. [Fact]
  111. public void Indei_Validation_Does_Not_Subscribe_When_DataValidation_Not_Enabled()
  112. {
  113. var data = new IndeiViewModel { MustBePositive = 5 };
  114. var target = CreateTargetWithSource(
  115. data,
  116. o => o.MustBePositive,
  117. enableDataValidation: false,
  118. mode: BindingMode.TwoWay);
  119. Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
  120. }
  121. [Fact]
  122. public void Indei_Validation_Subscribes_And_Unsubscribes()
  123. {
  124. var data = new IndeiViewModel { MustBePositive = 5 };
  125. var (target, expression) = CreateTargetAndExpression<IndeiViewModel, int>(
  126. o => o.MustBePositive,
  127. enableDataValidation: true,
  128. mode: BindingMode.TwoWay,
  129. source: data);
  130. Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
  131. expression.Dispose();
  132. Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
  133. }
  134. [Fact]
  135. public void Conversion_Errors_Update_Data_Validation_When_Writing_To_Source()
  136. {
  137. var data = new ViewModel { DoubleValue = 5.6 };
  138. var target = CreateTargetWithSource(
  139. data,
  140. o => o.DoubleValue,
  141. enableDataValidation: true,
  142. mode: BindingMode.TwoWay,
  143. targetProperty: TargetClass.TagProperty);
  144. // Can write a double value.
  145. target.Tag = 1.2;
  146. Assert.Equal(1.2, data.DoubleValue);
  147. AssertNoError(target, TargetClass.StringProperty);
  148. // Can write a string value and it gets converted to double.
  149. target.Tag = "3.4";
  150. Assert.Equal(3.4, data.DoubleValue);
  151. AssertNoError(target, TargetClass.StringProperty);
  152. // An invalid string value should result in an error. Not sure why this is considered
  153. // a data validation error rather than a binding error, but preserving semantics.
  154. target.Tag = "bar";
  155. Assert.Equal(3.4, data.DoubleValue);
  156. AssertBindingError(
  157. target,
  158. TargetClass.TagProperty,
  159. new InvalidCastException("Could not convert 'bar' (System.String) to System.Double."),
  160. BindingErrorType.DataValidationError);
  161. GC.KeepAlive(data);
  162. }
  163. [Fact]
  164. public void Indei_Validation_Updates_Data_Validation_When_Writing_To_Source()
  165. {
  166. var data = new IndeiViewModel();
  167. var target = CreateTargetWithSource(
  168. data,
  169. o => o.MustBePositive,
  170. enableDataValidation: true,
  171. mode: BindingMode.TwoWay);
  172. Assert.Equal(0, target.Int);
  173. Assert.Equal(0, data.MustBePositive);
  174. AssertNoError(target, TargetClass.IntProperty);
  175. target.Int = 5;
  176. Assert.Equal(5, target.Int);
  177. Assert.Equal(5, data.MustBePositive);
  178. AssertNoError(target, TargetClass.IntProperty);
  179. target.Int = -5;
  180. Assert.Equal(-5, target.Int);
  181. Assert.Equal(-5, data.MustBePositive);
  182. AssertBindingError(target, TargetClass.IntProperty, new DataValidationException("Must be positive"), BindingErrorType.DataValidationError);
  183. target.Int = 5;
  184. Assert.Equal(5, target.Int);
  185. Assert.Equal(5, data.MustBePositive);
  186. AssertNoError(target, TargetClass.IntProperty);
  187. GC.KeepAlive(data);
  188. }
  189. [Fact]
  190. public void Does_Not_Subscribe_To_Indei_Of_Intermediate_Object_In_Chain()
  191. {
  192. var data = new IndeiContainerViewModel { Inner = new() };
  193. var target = CreateTargetWithSource(
  194. data,
  195. o => o.Inner!.MustBePositive,
  196. enableDataValidation: true,
  197. mode: BindingMode.TwoWay);
  198. // We may want to change this but I've never seen an example of data validation on an
  199. // intermediate object in a chain so for the moment I'm not sure what the result of
  200. // validating such a thing should look like.
  201. Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
  202. Assert.Equal(1, data.Inner.ErrorsChangedSubscriptionCount);
  203. }
  204. [Fact]
  205. public void Updates_Data_Validation_For_Null_Value_In_Property_Chain()
  206. {
  207. var data = new IndeiContainerViewModel();
  208. var target = CreateTargetWithSource(
  209. data,
  210. o => o.Inner!.MustBePositive,
  211. enableDataValidation: true,
  212. mode: BindingMode.TwoWay);
  213. AssertBindingError(
  214. target,
  215. TargetClass.IntProperty,
  216. new BindingChainException("Value is null.", "Inner.MustBePositive", "Inner"),
  217. BindingErrorType.Error);
  218. GC.KeepAlive(data);
  219. }
  220. [Fact]
  221. public void Updates_Data_Validation_For_Required_DataAnnotation()
  222. {
  223. var data = new DataAnnotationsViewModel();
  224. var target = CreateTargetWithSource(
  225. data,
  226. o => o.RequiredString,
  227. enableDataValidation: true);
  228. AssertBindingError(
  229. target,
  230. TargetClass.StringProperty,
  231. new DataValidationException("String is required!"),
  232. BindingErrorType.DataValidationError);
  233. }
  234. [Fact]
  235. public void Handles_Indei_And_DataAnnotations_On_Same_Class()
  236. {
  237. // Issue #15201
  238. var data = new IndeiDataAnnotationsViewModel();
  239. var target = CreateTargetWithSource(
  240. data,
  241. o => o.RequiredString,
  242. enableDataValidation: true);
  243. AssertBindingError(
  244. target,
  245. TargetClass.StringProperty,
  246. new DataValidationException("String is required!"),
  247. BindingErrorType.DataValidationError);
  248. }
  249. [Fact]
  250. public void Setting_Valid_Value_Should_Clear_Binding_Error()
  251. {
  252. var data = new ViewModel { DoubleValue = 5.6 };
  253. var target = CreateTargetWithSource(
  254. data,
  255. o => o.DoubleValue,
  256. enableDataValidation: true,
  257. mode: BindingMode.TwoWay,
  258. targetProperty: TargetClass.StringProperty);
  259. target.String = "5.6";
  260. target.String = "5.6a";
  261. target.String = "5.6";
  262. AssertNoError(target, TargetClass.StringProperty);
  263. GC.KeepAlive(data);
  264. }
  265. public class ExceptionViewModel : NotifyingBase
  266. {
  267. private int _mustBePositive;
  268. public int MustBePositive
  269. {
  270. get { return _mustBePositive; }
  271. set
  272. {
  273. if (value <= 0)
  274. {
  275. throw new ArgumentOutOfRangeException(nameof(value));
  276. }
  277. _mustBePositive = value;
  278. RaisePropertyChanged();
  279. }
  280. }
  281. }
  282. private class IndeiViewModel : IndeiBase
  283. {
  284. private int _mustBePositive;
  285. private Dictionary<string, IList<string>> _errors = new Dictionary<string, IList<string>>();
  286. public int MustBePositive
  287. {
  288. get { return _mustBePositive; }
  289. set
  290. {
  291. _mustBePositive = value;
  292. RaisePropertyChanged();
  293. if (value >= 0)
  294. {
  295. _errors.Remove(nameof(MustBePositive));
  296. RaiseErrorsChanged(nameof(MustBePositive));
  297. }
  298. else
  299. {
  300. _errors[nameof(MustBePositive)] = new[] { "Must be positive" };
  301. RaiseErrorsChanged(nameof(MustBePositive));
  302. }
  303. }
  304. }
  305. public override bool HasErrors => _mustBePositive >= 0;
  306. public override IEnumerable? GetErrors(string propertyName)
  307. {
  308. IList<string>? result;
  309. _errors.TryGetValue(propertyName, out result);
  310. return result;
  311. }
  312. }
  313. private class IndeiContainerViewModel : IndeiBase
  314. {
  315. private IndeiViewModel? _inner;
  316. public IndeiViewModel? Inner
  317. {
  318. get { return _inner; }
  319. set { _inner = value; RaisePropertyChanged(); }
  320. }
  321. public override bool HasErrors => false;
  322. public override IEnumerable? GetErrors(string propertyName) => null;
  323. }
  324. private class DataAnnotationsViewModel : NotifyingBase
  325. {
  326. private string? _requiredString;
  327. [Required(ErrorMessage = "String is required!")]
  328. public string? RequiredString
  329. {
  330. get { return _requiredString; }
  331. set { _requiredString = value; RaisePropertyChanged(); }
  332. }
  333. }
  334. private class IndeiDataAnnotationsViewModel : IndeiBase
  335. {
  336. private string? _requiredString;
  337. [Required(ErrorMessage = "String is required!")]
  338. public string? RequiredString
  339. {
  340. get { return _requiredString; }
  341. set { _requiredString = value; RaisePropertyChanged(); }
  342. }
  343. public override bool HasErrors => RequiredString is null;
  344. public override IEnumerable? GetErrors(string propertyName)
  345. {
  346. if (propertyName == nameof(RequiredString) && RequiredString is null)
  347. {
  348. return new[] { "String is required!" };
  349. }
  350. return null;
  351. }
  352. }
  353. private static void AssertNoError(TargetClass target, AvaloniaProperty property)
  354. {
  355. Assert.False(target.BindingNotifications.TryGetValue(property, out var notification));
  356. }
  357. private static void AssertBindingError(
  358. TargetClass target,
  359. AvaloniaProperty property,
  360. Exception expectedException,
  361. BindingErrorType errorType)
  362. {
  363. Assert.True(target.BindingNotifications.TryGetValue(property, out var notification));
  364. Assert.Equal(errorType, notification.ErrorType);
  365. Assert.NotNull(notification.Error);
  366. Assert.IsType(expectedException.GetType(), notification.Error);
  367. Assert.Equal(expectedException.Message, notification.Error.Message);
  368. }
  369. }