PageRenderTime 30ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/Trunk/Source/Core/Core/ComponentModel/InputValidation/DataErrorNotifier.cs

#
C# | 453 lines | 291 code | 53 blank | 109 comment | 44 complexity | 5157c8ff40baf2aa8da9de125724b777 MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, LGPL-2.1, LGPL-2.0
  1. #region File and License Information
  2. /*
  3. <File>
  4. <License Type="BSD">
  5. Copyright © 2009 - 2012, Outcoder. All rights reserved.
  6. This file is part of Calcium (http://calciumsdk.net).
  7. Redistribution and use in source and binary forms, with or without
  8. modification, are permitted provided that the following conditions are met:
  9. * Redistributions of source code must retain the above copyright
  10. notice, this list of conditions and the following disclaimer.
  11. * Redistributions in binary form must reproduce the above copyright
  12. notice, this list of conditions and the following disclaimer in the
  13. documentation and/or other materials provided with the distribution.
  14. * Neither the name of the <organization> nor the
  15. names of its contributors may be used to endorse or promote products
  16. derived from this software without specific prior written permission.
  17. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  18. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  19. WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  20. DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
  21. DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  22. (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  23. LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  24. ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  25. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  26. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27. </License>
  28. <Owner Name="Daniel Vaughan" Email="danielvaughan@outcoder.com"/>
  29. <CreationDate>2010-09-29 16:32:45Z</CreationDate>
  30. </File>
  31. */
  32. #endregion
  33. using System;
  34. using System.Collections;
  35. using System.Collections.Generic;
  36. using System.ComponentModel;
  37. using System.Diagnostics;
  38. using System.Linq;
  39. using System.Linq.Expressions;
  40. using System.Reflection;
  41. using System.Threading.Tasks;
  42. using Outcoder.Reflection;
  43. namespace Outcoder.ComponentModel.InputValidation
  44. {
  45. public class DataErrorNotifier : INotifyDataErrorInfo
  46. {
  47. readonly IValidateData validator;
  48. readonly object errorsLock = new object();
  49. Dictionary<string, List<DataValidationError>> errorsField;
  50. readonly object propertyDictionaryLock = new object();
  51. readonly IDictionary<string, Func<object>> propertyDictionary
  52. = new Dictionary<string, Func<object>>();
  53. readonly INotifyPropertyChanged owner;
  54. /// <summary>
  55. /// Initializes a new instance
  56. /// of the <see cref="DataErrorNotifier"/> class.
  57. /// </summary>
  58. /// <param name="owner">The instance for which validation
  59. /// is being provided.</param>
  60. /// <param name="validator">The validator.</param>
  61. public DataErrorNotifier(INotifyPropertyChanged owner, IValidateData validator)
  62. {
  63. this.validator = ArgumentValidator.AssertNotNull(validator, "validator");
  64. this.owner = ArgumentValidator.AssertNotNull(owner, "owner");
  65. owner.PropertyChanged += HandleOwnerPropertyChanged;
  66. ReadValidationAttributes();
  67. }
  68. void ReadValidationAttributes()
  69. {
  70. #if NETFX_CORE
  71. var properties = owner.GetType().GetTypeInfo().DeclaredProperties;
  72. #else
  73. PropertyInfo[] properties = owner.GetType().GetProperties();
  74. #endif
  75. foreach (PropertyInfo propertyInfo in properties)
  76. {
  77. var attributes = propertyInfo.GetCustomAttributes(
  78. typeof(ValidateAttribute), true);
  79. if (!attributes.Any())
  80. {
  81. continue;
  82. }
  83. if (!propertyInfo.CanRead)
  84. {
  85. throw new InvalidOperationException(string.Format(
  86. "Property {0} must have a getter to be validated.",
  87. propertyInfo.Name));
  88. }
  89. /* Prevents access to internal closure warning. */
  90. PropertyInfo info = propertyInfo;
  91. AddValidationProperty(
  92. propertyInfo.Name, () => info.GetValue(owner, null));
  93. }
  94. }
  95. async void HandleOwnerPropertyChanged(object sender, PropertyChangedEventArgs e)
  96. {
  97. if (e == null || e.PropertyName == null)
  98. {
  99. return;
  100. }
  101. await BeginGetPropertyErrorsFromValidator(e.PropertyName);
  102. }
  103. async Task<ValidationCompleteEventArgs> BeginGetPropertyErrorsFromValidator(string propertyName)
  104. {
  105. Func<object> propertyFunc;
  106. lock (propertyDictionaryLock)
  107. {
  108. if (!propertyDictionary.TryGetValue(propertyName, out propertyFunc))
  109. {
  110. /* No property registered with that name. */
  111. return new ValidationCompleteEventArgs(propertyName);
  112. }
  113. }
  114. var result = await validator.ValidateAsync(propertyName, propertyFunc());
  115. ProcessValidationComplete(result);
  116. return result;
  117. }
  118. void ProcessValidationComplete(ValidationCompleteEventArgs e)
  119. {
  120. try
  121. {
  122. if (e.Exception == null)
  123. {
  124. SetPropertyErrors(e.PropertyName, e.Errors);
  125. }
  126. }
  127. catch (Exception ex)
  128. {
  129. Debug.WriteLine("Unable to set property error." + ex);
  130. }
  131. }
  132. /// <summary>
  133. /// Adds the property to the list of known class properties,
  134. /// which is used, for example, when performing validation
  135. /// of the whole class instance.
  136. /// </summary>
  137. /// <param name="name">The name of the property.</param>
  138. /// <param name="property">The <c>Func</c> to
  139. /// retrieve the property.</param>
  140. public void AddValidationProperty(string name, Func<object> property)
  141. {
  142. lock (propertyDictionaryLock)
  143. {
  144. propertyDictionary[name] = property;
  145. }
  146. }
  147. public async Task ValidateAllAsync()
  148. {
  149. if (propertyDictionary == null)
  150. {
  151. return;
  152. }
  153. bool raiseEvent = false;
  154. foreach (KeyValuePair<string, Func<object>> pair in propertyDictionary)
  155. {
  156. string propertyName = pair.Key;
  157. var validateResult = await validator.ValidateAsync(propertyName, pair.Value());
  158. ProcessValidationComplete(validateResult);
  159. }
  160. }
  161. /// <summary>
  162. /// Gets the validation errors for all properties.
  163. /// </summary>
  164. /// <value>The errors.</value>
  165. Dictionary<string, List<DataValidationError>> Errors
  166. {
  167. get
  168. {
  169. /* Lazy instantiation. */
  170. if (errorsField == null)
  171. {
  172. lock (errorsLock)
  173. {
  174. if (errorsField == null)
  175. {
  176. errorsField = new Dictionary<string, List<DataValidationError>>();
  177. }
  178. }
  179. }
  180. return errorsField;
  181. }
  182. }
  183. #region INotifyDataErrorInfo implementation
  184. public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
  185. /// <summary>
  186. /// Gets the validation errors for a specified property
  187. /// or for the entire object.
  188. /// </summary>
  189. /// <param name="propertyName">The name of the property
  190. /// to retrieve validation errors for,
  191. /// or null or <see cref="F:System.String.Empty"/>
  192. /// to retrieve errors for the entire object.</param>
  193. /// <returns>
  194. /// The validation errors for the property or object.
  195. /// </returns>
  196. public IEnumerable GetErrors(string propertyName)
  197. {
  198. return GetDataValidationErrors(propertyName);
  199. }
  200. /// <summary>
  201. /// Gets a value that indicates whether the object has validation errors.
  202. /// </summary>
  203. /// <value></value>
  204. /// <returns><c>true</c> if the object currently has validation errors;
  205. /// otherwise, <c>false</c>.</returns>
  206. public bool HasErrors
  207. {
  208. get
  209. {
  210. lock (errorsLock)
  211. {
  212. return errorsField != null && Errors.Count > 0;
  213. }
  214. }
  215. }
  216. #endregion
  217. /// <summary>
  218. /// Gets the validation errors for a specified property
  219. /// or for the entire object.
  220. /// </summary>
  221. /// <param name="propertyName">The name of the property
  222. /// to retrieve validation errors for,
  223. /// or null or <see cref="F:System.String.Empty"/>
  224. /// to retrieve errors for the entire object.</param>
  225. /// <returns>
  226. /// The validation errors for the property or object.
  227. /// </returns>
  228. public IEnumerable<DataValidationError> GetDataValidationErrors(
  229. string propertyName)
  230. {
  231. if (string.IsNullOrEmpty(propertyName))
  232. {
  233. lock (errorsLock)
  234. {
  235. if (errorsField == null)
  236. {
  237. return new List<DataValidationError>();
  238. }
  239. var result = from list in errorsField.Values
  240. from errorInfo in list
  241. select errorInfo;
  242. return result;
  243. }
  244. }
  245. lock (errorsLock)
  246. {
  247. List<DataValidationError> propertyErrors;
  248. if (errorsField == null
  249. || !errorsField.TryGetValue(propertyName, out propertyErrors))
  250. {
  251. return new List<DataValidationError>();
  252. }
  253. return propertyErrors.ToList();
  254. }
  255. }
  256. /// <summary>
  257. /// Raises the <see cref="ErrorsChanged"/> event.
  258. /// </summary>
  259. /// <param name="property">The property
  260. /// for which the list of errors changed.</param>
  261. protected virtual void OnErrorsChanged(string property)
  262. {
  263. UISynchronizationContext.Instance.InvokeIfRequired(
  264. delegate
  265. {
  266. ErrorsChanged?.Invoke(this,
  267. new DataErrorsChangedEventArgs(property));
  268. });
  269. }
  270. /// <summary>
  271. /// Raises the ErrorsChanged event.
  272. /// </summary>
  273. public void RaiseErrorsChanged()
  274. {
  275. OnErrorsChanged(string.Empty);
  276. }
  277. /// <summary>
  278. /// Removes the property error from the properties list
  279. /// of validation errors.
  280. /// </summary>
  281. /// <param name="propertyName">Name of the property.</param>
  282. /// <param name="errorCode">The error code.</param>
  283. public void RemovePropertyError(string propertyName, int errorCode)
  284. {
  285. bool raiseEvent = false;
  286. lock (errorsLock)
  287. {
  288. if (errorsField == null)
  289. {
  290. return;
  291. }
  292. List<DataValidationError> list = errorsField[propertyName];
  293. DataValidationError dataValidationError
  294. = list.SingleOrDefault(e => e.Id == errorCode);
  295. if (dataValidationError != null)
  296. {
  297. list.Remove(dataValidationError);
  298. raiseEvent = true;
  299. }
  300. }
  301. if (raiseEvent)
  302. {
  303. OnErrorsChanged(propertyName);
  304. }
  305. }
  306. /// <summary>
  307. /// Adds a property error for the specified propertyName.
  308. /// This may produce a prompt in the UI to correct the error before proceeding.
  309. /// </summary>
  310. /// <param name="propertyName">Name of the property.</param>
  311. /// <param name="dataValidationError">The data validation error.</param>
  312. public void AddPropertyError(
  313. string propertyName, DataValidationError dataValidationError)
  314. {
  315. SetPropertyErrors(propertyName,
  316. new List<DataValidationError> { dataValidationError });
  317. }
  318. public void AddValidationProperty(Expression<Func<object>> expression)
  319. {
  320. PropertyInfo propertyInfo = PropertyUtility.GetPropertyInfo(expression);
  321. string name = propertyInfo.Name;
  322. #if NETFX_CORE
  323. MethodInfo getMethodInfo = propertyInfo.GetMethod;
  324. Func<object> getter = (Func<object>)getMethodInfo.CreateDelegate(
  325. typeof(Func<object>),
  326. this);
  327. #else
  328. Func<object> getter = (Func<object>)Delegate.CreateDelegate(
  329. typeof(Func<object>),
  330. this,
  331. propertyInfo.GetGetMethod());
  332. #endif
  333. AddValidationProperty(name, getter);
  334. }
  335. /// <summary>
  336. /// Sets the known validation errors for a property.
  337. /// This may produce a prompt in the UI to correct the error before proceeding.
  338. /// </summary>
  339. /// <param name="propertyName">Name of the property.</param>
  340. /// <param name="dataErrors">The data errors.</param>
  341. public void SetPropertyErrors(
  342. string propertyName, IEnumerable<DataValidationError> dataErrors)
  343. {
  344. ArgumentValidator.AssertNotNullOrEmpty(propertyName, "propertyName");
  345. List<DataValidationError> list;
  346. bool raiseEvent = false;
  347. lock (errorsLock)
  348. {
  349. bool created = false;
  350. int paramErrorCount = dataErrors == null ? 0 : dataErrors.Count();
  351. if ((errorsField == null || errorsField.Count < 1)
  352. && paramErrorCount < 1)
  353. {
  354. return;
  355. }
  356. if (errorsField == null)
  357. {
  358. errorsField = new Dictionary<string, List<DataValidationError>>();
  359. created = true;
  360. }
  361. bool listFound = false;
  362. if (created ||
  363. !(listFound = errorsField.TryGetValue(propertyName, out list)))
  364. {
  365. list = new List<DataValidationError>();
  366. }
  367. if (paramErrorCount < 1)
  368. {
  369. if (listFound)
  370. {
  371. errorsField.Remove(propertyName);
  372. raiseEvent = true;
  373. }
  374. }
  375. else
  376. {
  377. var tempList = new List<DataValidationError>();
  378. foreach (var dataError in dataErrors)
  379. {
  380. if (created || list.SingleOrDefault(
  381. e => e.Id == dataError.Id) == null)
  382. {
  383. tempList.Add(dataError);
  384. raiseEvent = true;
  385. }
  386. }
  387. list.AddRange(tempList);
  388. errorsField[propertyName] = list;
  389. }
  390. }
  391. if (raiseEvent)
  392. {
  393. OnErrorsChanged(propertyName);
  394. }
  395. }
  396. }
  397. }