InputBaseTest.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. // Copyright (c) .NET Foundation. All rights reserved.
  2. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Globalization;
  6. using System.Linq;
  7. using System.Threading.Tasks;
  8. using Microsoft.AspNetCore.Components.Test.Helpers;
  9. using Xunit;
  10. namespace Microsoft.AspNetCore.Components.Forms
  11. {
  12. public class InputBaseTest
  13. {
  14. [Fact]
  15. public async Task ThrowsOnFirstRenderIfNoEditContextIsSupplied()
  16. {
  17. // Arrange
  18. var inputComponent = new TestInputComponent<string>();
  19. var testRenderer = new TestRenderer();
  20. var componentId = testRenderer.AssignRootComponentId(inputComponent);
  21. // Act/Assert
  22. var ex = await Assert.ThrowsAsync<InvalidOperationException>(
  23. () => testRenderer.RenderRootComponentAsync(componentId));
  24. Assert.StartsWith($"{typeof(TestInputComponent<string>)} requires a cascading parameter of type {nameof(EditContext)}", ex.Message);
  25. }
  26. [Fact]
  27. public async Task ThrowsIfEditContextChanges()
  28. {
  29. // Arrange
  30. var model = new TestModel();
  31. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model), ValueExpression = () => model.StringProperty };
  32. await InputRenderer.RenderAndGetComponent(rootComponent);
  33. // Act/Assert
  34. rootComponent.EditContext = new EditContext(model);
  35. var ex = Assert.Throws<InvalidOperationException>(() => rootComponent.TriggerRender());
  36. Assert.StartsWith($"{typeof(TestInputComponent<string>)} does not support changing the EditContext dynamically", ex.Message);
  37. }
  38. [Fact]
  39. public async Task ThrowsIfNoValueExpressionIsSupplied()
  40. {
  41. // Arrange
  42. var model = new TestModel();
  43. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model) };
  44. // Act/Assert
  45. var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => InputRenderer.RenderAndGetComponent(rootComponent));
  46. Assert.Contains($"{typeof(TestInputComponent<string>)} requires a value for the 'ValueExpression' parameter. Normally this is provided automatically when using 'bind-Value'.", ex.Message);
  47. }
  48. [Fact]
  49. public async Task GetsCurrentValueFromValueParameter()
  50. {
  51. // Arrange
  52. var model = new TestModel();
  53. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  54. {
  55. EditContext = new EditContext(model),
  56. Value = "some value",
  57. ValueExpression = () => model.StringProperty
  58. };
  59. // Act
  60. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  61. // Assert
  62. Assert.Equal("some value", inputComponent.CurrentValue);
  63. }
  64. [Fact]
  65. public async Task ExposesEditContextToSubclass()
  66. {
  67. // Arrange
  68. var model = new TestModel();
  69. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  70. {
  71. EditContext = new EditContext(model),
  72. Value = "some value",
  73. ValueExpression = () => model.StringProperty
  74. };
  75. // Act
  76. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  77. // Assert
  78. Assert.Same(rootComponent.EditContext, inputComponent.EditContext);
  79. }
  80. [Fact]
  81. public async Task ExposesFieldIdentifierToSubclass()
  82. {
  83. // Arrange
  84. var model = new TestModel();
  85. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  86. {
  87. EditContext = new EditContext(model),
  88. Value = "some value",
  89. ValueExpression = () => model.StringProperty
  90. };
  91. // Act
  92. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  93. // Assert
  94. Assert.Equal(FieldIdentifier.Create(() => model.StringProperty), inputComponent.FieldIdentifier);
  95. }
  96. [Fact]
  97. public async Task CanReadBackChangesToCurrentValue()
  98. {
  99. // Arrange
  100. var model = new TestModel();
  101. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  102. {
  103. EditContext = new EditContext(model),
  104. Value = "initial value",
  105. ValueExpression = () => model.StringProperty
  106. };
  107. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  108. Assert.Equal("initial value", inputComponent.CurrentValue);
  109. // Act
  110. inputComponent.CurrentValue = "new value";
  111. // Assert
  112. Assert.Equal("new value", inputComponent.CurrentValue);
  113. }
  114. [Fact]
  115. public async Task WritingToCurrentValueInvokesValueChangedIfDifferent()
  116. {
  117. // Arrange
  118. var model = new TestModel();
  119. var valueChangedCallLog = new List<string>();
  120. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  121. {
  122. EditContext = new EditContext(model),
  123. Value = "initial value",
  124. ValueChanged = val => valueChangedCallLog.Add(val),
  125. ValueExpression = () => model.StringProperty
  126. };
  127. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  128. Assert.Empty(valueChangedCallLog);
  129. // Act
  130. inputComponent.CurrentValue = "new value";
  131. // Assert
  132. Assert.Single(valueChangedCallLog, "new value");
  133. }
  134. [Fact]
  135. public async Task WritingToCurrentValueDoesNotInvokeValueChangedIfUnchanged()
  136. {
  137. // Arrange
  138. var model = new TestModel();
  139. var valueChangedCallLog = new List<string>();
  140. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  141. {
  142. EditContext = new EditContext(model),
  143. Value = "initial value",
  144. ValueChanged = val => valueChangedCallLog.Add(val),
  145. ValueExpression = () => model.StringProperty
  146. };
  147. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  148. Assert.Empty(valueChangedCallLog);
  149. // Act
  150. inputComponent.CurrentValue = "initial value";
  151. // Assert
  152. Assert.Empty(valueChangedCallLog);
  153. }
  154. [Fact]
  155. public async Task WritingToCurrentValueNotifiesEditContext()
  156. {
  157. // Arrange
  158. var model = new TestModel();
  159. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  160. {
  161. EditContext = new EditContext(model),
  162. Value = "initial value",
  163. ValueExpression = () => model.StringProperty
  164. };
  165. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  166. Assert.False(rootComponent.EditContext.IsModified(() => model.StringProperty));
  167. // Act
  168. inputComponent.CurrentValue = "new value";
  169. // Assert
  170. Assert.True(rootComponent.EditContext.IsModified(() => model.StringProperty));
  171. }
  172. [Fact]
  173. public async Task SuppliesFieldClassCorrespondingToFieldState()
  174. {
  175. // Arrange
  176. var model = new TestModel();
  177. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  178. {
  179. EditContext = new EditContext(model),
  180. ValueExpression = () => model.StringProperty
  181. };
  182. var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
  183. // Act/Assert: Initially, it's valid and unmodified
  184. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  185. Assert.Equal("valid", inputComponent.CssClass); // no Class was specified
  186. // Act/Assert: Modify the field
  187. rootComponent.EditContext.NotifyFieldChanged(fieldIdentifier);
  188. Assert.Equal("modified valid", inputComponent.CssClass);
  189. // Act/Assert: Make it invalid
  190. var messages = new ValidationMessageStore(rootComponent.EditContext);
  191. messages.Add(fieldIdentifier, "I do not like this value");
  192. Assert.Equal("modified invalid", inputComponent.CssClass);
  193. // Act/Assert: Clear the modification flag
  194. rootComponent.EditContext.MarkAsUnmodified(fieldIdentifier);
  195. Assert.Equal("invalid", inputComponent.CssClass);
  196. // Act/Assert: Make it valid
  197. messages.Clear();
  198. Assert.Equal("valid", inputComponent.CssClass);
  199. }
  200. [Fact]
  201. public async Task CssClassCombinesClassWithFieldClass()
  202. {
  203. // Arrange
  204. var model = new TestModel();
  205. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  206. {
  207. AdditionalAttributes = new Dictionary<string, object>()
  208. {
  209. { "class", "my-class other-class" },
  210. },
  211. EditContext = new EditContext(model),
  212. ValueExpression = () => model.StringProperty
  213. };
  214. var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
  215. // Act/Assert
  216. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  217. Assert.Equal("my-class other-class valid", inputComponent.CssClass);
  218. // Act/Assert: Retains custom class when changing field class
  219. rootComponent.EditContext.NotifyFieldChanged(fieldIdentifier);
  220. Assert.Equal("my-class other-class modified valid", inputComponent.CssClass);
  221. }
  222. [Fact]
  223. public async Task SuppliesCurrentValueAsStringWithFormatting()
  224. {
  225. // Arrange
  226. var model = new TestModel();
  227. var rootComponent = new TestInputHostComponent<DateTime, TestDateInputComponent>
  228. {
  229. EditContext = new EditContext(model),
  230. Value = new DateTime(1915, 3, 2),
  231. ValueExpression = () => model.DateProperty
  232. };
  233. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  234. // Act/Assert
  235. Assert.Equal("1915/03/02", inputComponent.CurrentValueAsString);
  236. }
  237. [Fact]
  238. public async Task ParsesCurrentValueAsStringWhenChanged_Valid()
  239. {
  240. // Arrange
  241. var model = new TestModel();
  242. var valueChangedArgs = new List<DateTime>();
  243. var rootComponent = new TestInputHostComponent<DateTime, TestDateInputComponent>
  244. {
  245. EditContext = new EditContext(model),
  246. ValueChanged = valueChangedArgs.Add,
  247. ValueExpression = () => model.DateProperty
  248. };
  249. var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
  250. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  251. var numValidationStateChanges = 0;
  252. rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
  253. // Act
  254. await inputComponent.SetCurrentValueAsStringAsync("1991/11/20");
  255. // Assert
  256. var receivedParsedValue = valueChangedArgs.Single();
  257. Assert.Equal(1991, receivedParsedValue.Year);
  258. Assert.Equal(11, receivedParsedValue.Month);
  259. Assert.Equal(20, receivedParsedValue.Day);
  260. Assert.True(rootComponent.EditContext.IsModified(fieldIdentifier));
  261. Assert.Empty(rootComponent.EditContext.GetValidationMessages(fieldIdentifier));
  262. Assert.Equal(0, numValidationStateChanges);
  263. }
  264. [Fact]
  265. public async Task ParsesCurrentValueAsStringWhenChanged_Invalid()
  266. {
  267. // Arrange
  268. var model = new TestModel();
  269. var valueChangedArgs = new List<DateTime>();
  270. var rootComponent = new TestInputHostComponent<DateTime, TestDateInputComponent>
  271. {
  272. EditContext = new EditContext(model),
  273. ValueChanged = valueChangedArgs.Add,
  274. ValueExpression = () => model.DateProperty
  275. };
  276. var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
  277. var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
  278. var numValidationStateChanges = 0;
  279. rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
  280. // Act/Assert 1: Transition to invalid
  281. await inputComponent.SetCurrentValueAsStringAsync("1991/11/40");
  282. Assert.Empty(valueChangedArgs);
  283. Assert.True(rootComponent.EditContext.IsModified(fieldIdentifier));
  284. Assert.Equal(new[] { "Bad date value" }, rootComponent.EditContext.GetValidationMessages(fieldIdentifier));
  285. Assert.Equal(1, numValidationStateChanges);
  286. // Act/Assert 2: Transition to valid
  287. await inputComponent.SetCurrentValueAsStringAsync("1991/11/20");
  288. var receivedParsedValue = valueChangedArgs.Single();
  289. Assert.Equal(1991, receivedParsedValue.Year);
  290. Assert.Equal(11, receivedParsedValue.Month);
  291. Assert.Equal(20, receivedParsedValue.Day);
  292. Assert.True(rootComponent.EditContext.IsModified(fieldIdentifier));
  293. Assert.Empty(rootComponent.EditContext.GetValidationMessages(fieldIdentifier));
  294. Assert.Equal(2, numValidationStateChanges);
  295. }
  296. [Fact]
  297. public async Task RespondsToValidationStateChangeNotifications()
  298. {
  299. // Arrange
  300. var model = new TestModel();
  301. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  302. {
  303. EditContext = new EditContext(model),
  304. ValueExpression = () => model.StringProperty
  305. };
  306. var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
  307. var renderer = new TestRenderer();
  308. var rootComponentId = renderer.AssignRootComponentId(rootComponent);
  309. await renderer.RenderRootComponentAsync(rootComponentId);
  310. // Initally, it rendered one batch and is valid
  311. var batch1 = renderer.Batches.Single();
  312. var componentFrame1 = batch1.GetComponentFrames<TestInputComponent<string>>().Single();
  313. var inputComponentId = componentFrame1.ComponentId;
  314. var component = (TestInputComponent<string>)componentFrame1.Component;
  315. Assert.Equal("valid", component.CssClass);
  316. Assert.Null(component.AdditionalAttributes);
  317. // Act: update the field state in the EditContext and notify
  318. var messageStore = new ValidationMessageStore(rootComponent.EditContext);
  319. messageStore.Add(fieldIdentifier, "Some message");
  320. await renderer.Dispatcher.InvokeAsync(rootComponent.EditContext.NotifyValidationStateChanged);
  321. // Assert: The input component rendered itself again and now has the new class
  322. var batch2 = renderer.Batches.Skip(1).Single();
  323. Assert.Equal(inputComponentId, batch2.DiffsByComponentId.Keys.Single());
  324. Assert.Equal("invalid", component.CssClass);
  325. Assert.NotNull(component.AdditionalAttributes);
  326. Assert.True(component.AdditionalAttributes.ContainsKey("aria-invalid"));
  327. }
  328. [Fact]
  329. public async Task UnsubscribesFromValidationStateChangeNotifications()
  330. {
  331. // Arrange
  332. var model = new TestModel();
  333. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  334. {
  335. EditContext = new EditContext(model),
  336. ValueExpression = () => model.StringProperty
  337. };
  338. var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
  339. var renderer = new TestRenderer();
  340. var rootComponentId = renderer.AssignRootComponentId(rootComponent);
  341. await renderer.RenderRootComponentAsync(rootComponentId);
  342. var component = renderer.Batches.Single().GetComponentFrames<TestInputComponent<string>>().Single().Component;
  343. // Act: dispose, then update the field state in the EditContext and notify
  344. ((IDisposable)component).Dispose();
  345. var messageStore = new ValidationMessageStore(rootComponent.EditContext);
  346. messageStore.Add(fieldIdentifier, "Some message");
  347. await renderer.Dispatcher.InvokeAsync(rootComponent.EditContext.NotifyValidationStateChanged);
  348. // Assert: No additional render
  349. Assert.Empty(renderer.Batches.Skip(1));
  350. }
  351. [Fact]
  352. public async Task AriaAttributeIsRenderedWhenTheValidationStateIsInvalidOnFirstRender()
  353. {
  354. // Arrange// Arrange
  355. var model = new TestModel();
  356. var invalidContext = new EditContext(model);
  357. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  358. {
  359. EditContext = invalidContext,
  360. ValueExpression = () => model.StringProperty
  361. };
  362. var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
  363. var messageStore = new ValidationMessageStore(invalidContext);
  364. messageStore.Add(fieldIdentifier, "Test error message");
  365. var renderer = new TestRenderer();
  366. var rootComponentId = renderer.AssignRootComponentId(rootComponent);
  367. await renderer.RenderRootComponentAsync(rootComponentId);
  368. // Initally, it rendered one batch and is valid
  369. var batch1 = renderer.Batches.Single();
  370. var componentFrame1 = batch1.GetComponentFrames<TestInputComponent<string>>().Single();
  371. var inputComponentId = componentFrame1.ComponentId;
  372. var component = (TestInputComponent<string>)componentFrame1.Component;
  373. Assert.Equal("invalid", component.CssClass);
  374. Assert.NotNull(component.AdditionalAttributes);
  375. Assert.Equal(1, component.AdditionalAttributes.Count);
  376. Assert.True((bool)component.AdditionalAttributes["aria-invalid"]);
  377. }
  378. [Fact]
  379. public async Task UserSpecifiedAriaValueIsNotChangedIfInvalid()
  380. {
  381. // Arrange// Arrange
  382. var model = new TestModel();
  383. var invalidContext = new EditContext(model);
  384. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  385. {
  386. EditContext = invalidContext,
  387. ValueExpression = () => model.StringProperty
  388. };
  389. rootComponent.AdditionalAttributes = new Dictionary<string, object>();
  390. rootComponent.AdditionalAttributes["aria-invalid"] = "userSpecifiedValue";
  391. var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
  392. var messageStore = new ValidationMessageStore(invalidContext);
  393. messageStore.Add(fieldIdentifier, "Test error message");
  394. var renderer = new TestRenderer();
  395. var rootComponentId = renderer.AssignRootComponentId(rootComponent);
  396. await renderer.RenderRootComponentAsync(rootComponentId);
  397. // Initally, it rendered one batch and is valid
  398. var batch1 = renderer.Batches.Single();
  399. var componentFrame1 = batch1.GetComponentFrames<TestInputComponent<string>>().Single();
  400. var inputComponentId = componentFrame1.ComponentId;
  401. var component = (TestInputComponent<string>)componentFrame1.Component;
  402. Assert.Equal("invalid", component.CssClass);
  403. Assert.NotNull(component.AdditionalAttributes);
  404. Assert.Equal(1, component.AdditionalAttributes.Count);
  405. Assert.Equal("userSpecifiedValue", component.AdditionalAttributes["aria-invalid"]);
  406. }
  407. [Fact]
  408. public async Task AriaAttributeRemovedWhenStateChangesToValidFromInvalid()
  409. {
  410. // Arrange
  411. var model = new TestModel();
  412. var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
  413. {
  414. EditContext = new EditContext(model),
  415. ValueExpression = () => model.StringProperty
  416. };
  417. var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
  418. var renderer = new TestRenderer();
  419. var messageStore = new ValidationMessageStore(rootComponent.EditContext);
  420. messageStore.Add(fieldIdentifier, "Artificial error message");
  421. var rootComponentId = renderer.AssignRootComponentId(rootComponent);
  422. await renderer.RenderRootComponentAsync(rootComponentId);
  423. // Initally, it rendered one batch and is invalid
  424. var batch1 = renderer.Batches.Single();
  425. var componentFrame1 = batch1.GetComponentFrames<TestInputComponent<string>>().Single();
  426. var inputComponentId = componentFrame1.ComponentId;
  427. var component = (TestInputComponent<string>)componentFrame1.Component;
  428. Assert.Equal("invalid", component.CssClass);
  429. Assert.NotNull(component.AdditionalAttributes);
  430. Assert.True(component.AdditionalAttributes.ContainsKey("aria-invalid"));
  431. // Act: update the field state in the EditContext and notify
  432. messageStore.Clear(fieldIdentifier);
  433. await renderer.Dispatcher.InvokeAsync(rootComponent.EditContext.NotifyValidationStateChanged);
  434. // Assert: The input component rendered itself again and now has the new class
  435. var batch2 = renderer.Batches.Skip(1).Single();
  436. Assert.Equal(inputComponentId, batch2.DiffsByComponentId.Keys.Single());
  437. Assert.Equal("valid", component.CssClass);
  438. Assert.Null(component.AdditionalAttributes);
  439. }
  440. class TestModel
  441. {
  442. public string StringProperty { get; set; }
  443. public DateTime DateProperty { get; set; }
  444. }
  445. class TestInputComponent<T> : InputBase<T>
  446. {
  447. // Expose protected members publicly for tests
  448. public new T CurrentValue
  449. {
  450. get => base.CurrentValue;
  451. set { base.CurrentValue = value; }
  452. }
  453. public new string CurrentValueAsString
  454. {
  455. get => base.CurrentValueAsString;
  456. }
  457. public new IReadOnlyDictionary<string, object> AdditionalAttributes => base.AdditionalAttributes;
  458. public new string CssClass => base.CssClass;
  459. public new EditContext EditContext => base.EditContext;
  460. public new FieldIdentifier FieldIdentifier => base.FieldIdentifier;
  461. protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage)
  462. {
  463. throw new NotImplementedException();
  464. }
  465. public async Task SetCurrentValueAsStringAsync(string value)
  466. {
  467. // This is equivalent to the subclass writing to CurrentValueAsString
  468. // (e.g., from @bind), except to simplify the test code there's an InvokeAsync
  469. // here. In production code it wouldn't normally be required because @bind
  470. // calls run on the sync context anyway.
  471. await InvokeAsync(() => { base.CurrentValueAsString = value; });
  472. }
  473. }
  474. private class TestDateInputComponent : TestInputComponent<DateTime>
  475. {
  476. protected override string FormatValueAsString(DateTime value)
  477. => value.ToString("yyyy/MM/dd", CultureInfo.InvariantCulture);
  478. protected override bool TryParseValueFromString(string value, out DateTime result, out string validationErrorMessage)
  479. {
  480. if (DateTime.TryParse(value, out result))
  481. {
  482. validationErrorMessage = null;
  483. return true;
  484. }
  485. else
  486. {
  487. validationErrorMessage = "Bad date value";
  488. return false;
  489. }
  490. }
  491. }
  492. }
  493. }