PageRenderTime 49ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Catel.Windows35/Windows/Controls/InfoBarMessageControl/InfoBarMessageControl.cs

#
C# | 561 lines | 329 code | 75 blank | 157 comment | 48 complexity | 87b76a7de38542d1c68d3ccb1149dcd1 MD5 | raw file
Possible License(s): CC-BY-SA-3.0
  1. // --------------------------------------------------------------------------------------------------------------------
  2. // <copyright file="InfoBarMessageControl.cs" company="Catel development team">
  3. // Copyright (c) 2008 - 2011 Catel development team. All rights reserved.
  4. // </copyright>
  5. // <summary>
  6. // Control for displaying messages to the user.
  7. // </summary>
  8. // --------------------------------------------------------------------------------------------------------------------
  9. namespace Catel.Windows.Controls
  10. {
  11. using System.Collections.Generic;
  12. using System.Collections.ObjectModel;
  13. using System.Windows;
  14. using System.Windows.Controls;
  15. using Logging;
  16. #if !SILVERLIGHT
  17. using Properties;
  18. using System;
  19. using System.ComponentModel;
  20. using System.Windows.Data;
  21. #endif
  22. /// <summary>
  23. /// The display mode for the <see cref="InfoBarMessageControl"/>.
  24. /// </summary>
  25. public enum InfoBarMessageControlMode
  26. {
  27. /// <summary>
  28. /// Displays the control inline, which means all controls below are moved down a bit when the
  29. /// control becomes visible.
  30. /// </summary>
  31. Inline,
  32. /// <summary>
  33. /// Displays the control as an overlay, which might lead to overlapping of existing controls.
  34. /// </summary>
  35. Overlay
  36. }
  37. /// <summary>
  38. /// Control for displaying messages to the user.
  39. /// </summary>
  40. /// <remarks>
  41. /// A long, long, long time ago, the messages were hold in a dependency property (DP). However, even though DP values are
  42. /// not static, several instances that were open at the same time were still clearing eachother values (thus it seemed the
  43. /// DP behaves like it's a static member). Therefore, the messages are now hold in a field, and all problems are now gone.
  44. /// <para />
  45. /// And the control lived happily ever after.
  46. /// </remarks>
  47. [TemplatePart(Name = ElementMessageBar, Type = typeof(FrameworkElement))]
  48. public class InfoBarMessageControl : ContentControl
  49. {
  50. #region Constants
  51. /// <summary>
  52. /// The bar that will show the initial message bar.
  53. /// </summary>
  54. private const string ElementMessageBar = "PART_MessageBar";
  55. #endregion
  56. #region Variables
  57. /// <summary>
  58. /// The log.
  59. /// </summary>
  60. private static readonly ILog Log = LogManager.GetCurrentClassLogger();
  61. private readonly object _lock = new object();
  62. private readonly List<object> _objectsToIgnore = new List<object>();
  63. private readonly Dictionary<object, List<string>> _warnings = new Dictionary<object, List<string>>();
  64. private readonly Dictionary<object, List<string>> _errors = new Dictionary<object, List<string>>();
  65. private readonly ObservableCollection<string> _warningMessages = new ObservableCollection<string>();
  66. private readonly ObservableCollection<string> _errorMessages = new ObservableCollection<string>();
  67. #if !USE_ROUTED_EVENTS
  68. private WarningAndErrorValidator _parentWarningAndErrorValidator;
  69. #endif
  70. private bool _subscribedToEvents;
  71. #endregion
  72. #region Constructor & destructor
  73. /// <summary>
  74. /// Initializes static members of the <see cref="InfoBarMessageControl"/> class.
  75. /// </summary>
  76. static InfoBarMessageControl()
  77. {
  78. #if !SILVERLIGHT
  79. DefaultStyleKeyProperty.OverrideMetadata(typeof(InfoBarMessageControl), new FrameworkPropertyMetadata(typeof(InfoBarMessageControl)));
  80. #endif
  81. }
  82. /// <summary>
  83. /// Initializes a new instance of the <see cref="InfoBarMessageControl"/> class.
  84. /// </summary>
  85. public InfoBarMessageControl()
  86. {
  87. Text = Properties.Resources.InfoBarMessageControlErrorTitle;
  88. #if !SILVERLIGHT
  89. Focusable = false;
  90. #endif
  91. if (Catel.Environment.IsInDesignMode)
  92. {
  93. return;
  94. }
  95. Loaded += OnLoaded;
  96. Unloaded += OnUnloaded;
  97. }
  98. #endregion
  99. #region Properties
  100. /// <summary>
  101. /// Gets or sets the mode in which the control is displayed.
  102. /// </summary>
  103. /// <value>The mode in which the control is displayed.</value>
  104. public InfoBarMessageControlMode Mode
  105. {
  106. get { return (InfoBarMessageControlMode)GetValue(ModeProperty); }
  107. set { SetValue(ModeProperty, value); }
  108. }
  109. /// <summary>
  110. /// DependencyProperty definition as the backing store for Mode.
  111. /// </summary>
  112. public static readonly DependencyProperty ModeProperty =
  113. DependencyProperty.Register("Mode", typeof(InfoBarMessageControlMode), typeof(InfoBarMessageControl),
  114. new PropertyMetadata(InfoBarMessageControlMode.Inline, (sender, e) => ((InfoBarMessageControl)sender).OnModeChanged()));
  115. /// <summary>
  116. /// Gets or sets the text to display when there are warnings and/or messages.
  117. /// </summary>
  118. /// <value>The text.</value>
  119. public string Text
  120. {
  121. get { return (string)GetValue(TextProperty); }
  122. set { SetValue(TextProperty, value); }
  123. }
  124. /// <summary>
  125. /// DependencyProperty definition as the backing store for Text.
  126. /// </summary>
  127. public static readonly DependencyProperty TextProperty =
  128. DependencyProperty.Register("Text", typeof(string), typeof(InfoBarMessageControl), new PropertyMetadata(Catel.Environment.DefaultMultiLingualDependencyPropertyValue));
  129. /// <summary>
  130. /// Info message for the info bar.
  131. /// </summary>
  132. public string InfoMessage
  133. {
  134. get { return (string)GetValue(InfoMessageProperty); }
  135. set { SetValue(InfoMessageProperty, value); }
  136. }
  137. /// <summary>
  138. /// DependencyProperty definition as the backing store for InfoMessage.
  139. /// </summary>
  140. public static readonly DependencyProperty InfoMessageProperty =
  141. DependencyProperty.Register("InfoMessage", typeof(string), typeof(InfoBarMessageControl), new PropertyMetadata(string.Empty));
  142. /// <summary>
  143. /// Gets or sets MessageCount.
  144. /// </summary>
  145. /// <remarks>
  146. /// Wrapper for the MessageCount dependency property.
  147. /// </remarks>
  148. public int MessageCount
  149. {
  150. get { return (int)GetValue(MessageCountProperty); }
  151. private set { SetValue(MessageCountProperty, value); }
  152. }
  153. /// <summary>
  154. /// Definition of the dependency property is private.
  155. /// </summary>
  156. public static readonly DependencyProperty MessageCountProperty =
  157. DependencyProperty.Register("MessageCount", typeof(int), typeof(InfoBarMessageControl), new PropertyMetadata(0));
  158. /// <summary>
  159. /// Gets the warning message collection.
  160. /// </summary>
  161. /// <value>The warning message collection.</value>
  162. /// <remarks>
  163. /// This property is not defined as dependency property, since it seems to cause some issues when several windows/controls with
  164. /// this control are open at the same time (dependency properties seem to behave static, but they shouldn't).
  165. /// </remarks>
  166. public ObservableCollection<string> WarningMessageCollection
  167. {
  168. get { return _warningMessages; }
  169. }
  170. /// <summary>
  171. /// Gets the error message collection.
  172. /// </summary>
  173. /// <value>The error message collection.</value>
  174. /// <remarks>
  175. /// This property is not defined as dependency property, since it seems to cause some issues when several windows/controls with
  176. /// this control are open at the same time (dependency properties seem to behave static, but they shouldn't).
  177. /// </remarks>
  178. public ObservableCollection<string> ErrorMessageCollection
  179. {
  180. get { return _errorMessages; }
  181. }
  182. #endregion
  183. #region Methods
  184. /// <summary>
  185. /// Called when the control is loaded.
  186. /// </summary>
  187. /// <param name="sender">The sender.</param>
  188. /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
  189. private void OnLoaded(object sender, RoutedEventArgs e)
  190. {
  191. SubscribeToEvents();
  192. }
  193. /// <summary>
  194. /// Called when the control is unloaded.
  195. /// </summary>
  196. /// <param name="sender">The sender.</param>
  197. /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
  198. private void OnUnloaded(object sender, RoutedEventArgs e)
  199. {
  200. UnsubscribeFromEvents();
  201. }
  202. /// <summary>
  203. /// When overridden in a derived class, is invoked whenever application code or internal processes call <see cref="M:System.Windows.FrameworkElement.ApplyTemplate"/>.
  204. /// </summary>
  205. public override void OnApplyTemplate()
  206. {
  207. base.OnApplyTemplate();
  208. #if !SILVERLIGHT
  209. if (GetTemplateChild(ElementMessageBar) == null)
  210. {
  211. throw new NotSupportedException(string.Format(Exceptions.ControlTemplateMustContainPart, ElementMessageBar));
  212. }
  213. #endif
  214. OnModeChanged();
  215. }
  216. /// <summary>
  217. /// Called when the <see cref="Mode"/> property has changed.
  218. /// </summary>
  219. private void OnModeChanged()
  220. {
  221. FrameworkElement messageBar = GetTemplateChild(ElementMessageBar) as FrameworkElement;
  222. if (messageBar != null)
  223. {
  224. int gridRow = 0;
  225. switch (Mode)
  226. {
  227. case InfoBarMessageControlMode.Inline:
  228. gridRow = 0;
  229. break;
  230. case InfoBarMessageControlMode.Overlay:
  231. gridRow = 1;
  232. break;
  233. }
  234. messageBar.SetValue(Grid.RowProperty, gridRow);
  235. }
  236. }
  237. /// <summary>
  238. /// Subscribes to events.
  239. /// </summary>
  240. private void SubscribeToEvents()
  241. {
  242. if (_subscribedToEvents)
  243. {
  244. return;
  245. }
  246. #if !SILVERLIGHT
  247. Validation.AddErrorHandler(this, OnInfoBarMessageErrorValidation);
  248. #endif
  249. #if !SILVERLIGHT && USE_ROUTED_EVENTS
  250. WarningAndErrorValidator.RemoveValidationHandler(this, OnInfoBarMessageValidation);
  251. #else
  252. WarningAndErrorValidator warningAndErrorValidator = this.FindVisualDescendantByType<WarningAndErrorValidator>();
  253. if (warningAndErrorValidator != null)
  254. {
  255. _parentWarningAndErrorValidator = warningAndErrorValidator;
  256. _parentWarningAndErrorValidator.Validation += OnInfoBarMessageValidation;
  257. }
  258. #endif
  259. _subscribedToEvents = true;
  260. }
  261. /// <summary>
  262. /// Unsubscribes from events.
  263. /// </summary>
  264. private void UnsubscribeFromEvents()
  265. {
  266. if (!_subscribedToEvents)
  267. {
  268. return;
  269. }
  270. #if !SILVERLIGHT
  271. Validation.RemoveErrorHandler(this, OnInfoBarMessageErrorValidation);
  272. #endif
  273. if (_parentWarningAndErrorValidator != null)
  274. {
  275. _parentWarningAndErrorValidator.Validation -= OnInfoBarMessageValidation;
  276. _parentWarningAndErrorValidator = null;
  277. }
  278. _subscribedToEvents = false;
  279. }
  280. /// <summary>
  281. /// Clears the object messages for the specified binding object.
  282. /// </summary>
  283. /// <param name="bindingObject">The binding object.</param>
  284. /// <remarks>
  285. /// This method is implemented because of the DataContext issue (DataContext cannot be changed before a
  286. /// user control is loaded, and therefore might be binding to the wrong object).
  287. /// </remarks>
  288. internal void ClearObjectMessages(object bindingObject)
  289. {
  290. object realBindingObject = bindingObject;
  291. ProcessValidationMessage(realBindingObject, null, ValidationEventAction.ClearAll, ValidationType.Warning);
  292. ProcessValidationMessage(realBindingObject, null, ValidationEventAction.ClearAll, ValidationType.Error);
  293. UpdateMessages();
  294. }
  295. /// <summary>
  296. /// Adds an object to the ignore list so this control does not show messages for the specified object any longer.
  297. /// </summary>
  298. /// <param name="bindingObject">The binding object.</param>
  299. internal void IgnoreObject(object bindingObject)
  300. {
  301. object realBindingObject = bindingObject;
  302. _objectsToIgnore.Add(realBindingObject);
  303. ClearObjectMessages(bindingObject);
  304. }
  305. #if !SILVERLIGHT
  306. /// <summary>
  307. /// Handling data errors.
  308. /// </summary>
  309. /// <param name="sender">A sender.</param>
  310. /// <param name="e">The event arguments</param>
  311. private void OnInfoBarMessageErrorValidation(object sender, ValidationErrorEventArgs e)
  312. {
  313. e.Handled = true;
  314. ValidationEventAction validationEventAction = ValidationEventAction.Added;
  315. if (e.Action == ValidationErrorEventAction.Added)
  316. {
  317. validationEventAction = ValidationEventAction.Added;
  318. }
  319. else if (e.Action == ValidationErrorEventAction.Removed)
  320. {
  321. validationEventAction = ValidationEventAction.Removed;
  322. }
  323. object bindingObject = GetBindingObject(e.Error.BindingInError);
  324. string message = (e.Error != null) ? e.Error.ErrorContent.ToString() : string.Empty;
  325. // There seems to be an issue where validations are removed, even when
  326. // ((IDataErrorInfo)bindingObject)["property"] has a value, so check for that
  327. var bindingObjectAsIDataErrorInfo = bindingObject as IDataErrorInfo;
  328. var bindingInErrorAsBindingExpression = e.Error.BindingInError as BindingExpression;
  329. if ((validationEventAction == ValidationEventAction.Removed) && (bindingObjectAsIDataErrorInfo != null) &&
  330. (bindingInErrorAsBindingExpression != null))
  331. {
  332. if (!string.IsNullOrEmpty(bindingObjectAsIDataErrorInfo[bindingInErrorAsBindingExpression.ParentBinding.Path.Path]))
  333. {
  334. Log.Debug("Received 'Remove' action for error '{0}', but it is invalid because the error still exists on the object", message);
  335. return;
  336. }
  337. }
  338. ProcessValidationMessage(bindingObject, message, validationEventAction, ValidationType.Error);
  339. UpdateMessages();
  340. }
  341. #endif
  342. /// <summary>
  343. /// Handling business data errors.
  344. /// </summary>
  345. /// <param name="sender">A sender.</param>
  346. /// <param name="e">The event arguments</param>
  347. private void OnInfoBarMessageValidation(object sender, ValidationEventArgs e)
  348. {
  349. ProcessValidationMessage(e.Value, e.Message, e.Action, e.Type);
  350. UpdateMessages();
  351. }
  352. #if !SILVERLIGHT
  353. /// <summary>
  354. /// Gets the binding object.
  355. /// </summary>
  356. /// <param name="bindingObject">The binding object.</param>
  357. /// <returns>object from the binding.</returns>
  358. private static object GetBindingObject(object bindingObject)
  359. {
  360. object result;
  361. // Check whether the data error is throwed on an single binding or a bindinggroup and process the error message
  362. if (bindingObject as BindingExpression != null)
  363. {
  364. // Use data item of binding
  365. result = ((BindingExpression)bindingObject).DataItem;
  366. }
  367. else if (bindingObject as BindingGroup != null)
  368. {
  369. // Use data group (object itself)
  370. // ReSharper disable RedundantCast
  371. result = ((BindingGroup)bindingObject);
  372. // ReSharper restore RedundantCast
  373. }
  374. else
  375. {
  376. // Just use the object
  377. result = bindingObject;
  378. }
  379. return result;
  380. }
  381. #endif
  382. /// <summary>
  383. /// Process an validation message.
  384. /// </summary>
  385. /// <param name="bindingObject">The binding object which will be used as key in dictionary with error messages. Allowed to be <c>null</c> if <see cref="ValidationEventAction.ClearAll"/>.</param>
  386. /// <param name="message">The actual warning or error message.</param>
  387. /// <param name="action">An error event action. See <see cref="ValidationErrorEventAction"/>.</param>
  388. /// <param name="type">The validation type.</param>
  389. private void ProcessValidationMessage(object bindingObject, string message, ValidationEventAction action, ValidationType type)
  390. {
  391. if ((action != ValidationEventAction.ClearAll) && (bindingObject == null))
  392. {
  393. Log.Warning("Null-values are not allowed when not using ValidationEventAction.ClearAll");
  394. return;
  395. }
  396. if (_objectsToIgnore.Contains(bindingObject) && (action != ValidationEventAction.ClearAll))
  397. {
  398. Log.Debug("Object '{0}' is in the ignore list, thus messages will not be handled", bindingObject);
  399. return;
  400. }
  401. Dictionary<object, List<string>> messages = (type == ValidationType.Warning) ? _warnings : _errors;
  402. lock (_lock)
  403. {
  404. switch (action)
  405. {
  406. case ValidationEventAction.Added:
  407. case ValidationEventAction.Refresh:
  408. if (!messages.ContainsKey(bindingObject))
  409. {
  410. messages.Add(bindingObject, new List<string>());
  411. }
  412. if (!messages[bindingObject].Contains(message))
  413. {
  414. messages[bindingObject].Add(message);
  415. }
  416. break;
  417. case ValidationEventAction.Removed:
  418. if (messages.ContainsKey(bindingObject))
  419. {
  420. messages[bindingObject].Remove(message);
  421. }
  422. break;
  423. case ValidationEventAction.ClearAll:
  424. if (bindingObject != null)
  425. {
  426. messages.Remove(bindingObject);
  427. }
  428. else
  429. {
  430. messages.Clear();
  431. }
  432. break;
  433. }
  434. }
  435. }
  436. /// <summary>
  437. /// Update the content of the control with the found warnings and errors.
  438. /// </summary>
  439. private void UpdateMessages()
  440. {
  441. lock (_lock)
  442. {
  443. UpdatesMessageCollection(_warningMessages, _warnings);
  444. UpdatesMessageCollection(_errorMessages, _errors);
  445. }
  446. MessageCount = _warningMessages.Count + _errorMessages.Count;
  447. InfoMessage = (MessageCount > 0) ? Text : string.Empty;
  448. }
  449. /// <summary>
  450. /// Updates a message collection by adding new messages and removing old ones that no longer exist.
  451. /// </summary>
  452. /// <param name="messageCollection">The message collection.</param>
  453. /// <param name="messageSource">The message source.</param>
  454. private static void UpdatesMessageCollection(ObservableCollection<string> messageCollection, Dictionary<object, List<string>> messageSource)
  455. {
  456. foreach (List<string> sourceMessageCollection in messageSource.Values)
  457. {
  458. foreach (string message in sourceMessageCollection)
  459. {
  460. if (!messageCollection.Contains(message))
  461. {
  462. messageCollection.Add(message);
  463. }
  464. }
  465. }
  466. for (int i = messageCollection.Count - 1; i >= 0; i--)
  467. {
  468. string message = messageCollection[i];
  469. bool isValid = false;
  470. foreach (List<string> sourceMessageCollection in messageSource.Values)
  471. {
  472. if (sourceMessageCollection.Contains(message))
  473. {
  474. isValid = true;
  475. break;
  476. }
  477. }
  478. if (!isValid)
  479. {
  480. messageCollection.RemoveAt(i);
  481. }
  482. }
  483. }
  484. #endregion
  485. }
  486. }