// --------------------------------------------------------------------------------------------------------------------
// <copyright file="InfoBarMessageControl.cs" company="Catel development team">
// Copyright (c) 2008 - 2011 Catel development team. All rights reserved.
// </copyright>
// <summary>
// Control for displaying messages to the user.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
// Use routed events if you are NOT using MVVM and want to show errors
#define USE_ROUTED_EVENTS
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using Catel.Windows.Properties;
using log4net;
namespace Catel.Windows.Controls
{
/// <summary>
/// Control for displaying messages to the user.
/// </summary>
/// <remarks>
/// A long, long, long time ago, the messages were hold in a dependency property (DP). However, even though DP values are
/// not static, several instances that were open at the same time were still clearing eachother values (thus it seemed the
/// DP behaves like it's a static member). Therefore, the messages are now hold in a field, and all problems are now gone.
/// <para />
/// And the control lived happily ever after.
/// </remarks>
public class InfoBarMessageControl : ContentControl
{
#region Variables
/// <summary>
/// The <see cref="ILog">log</see> object.
/// </summary>
private static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private readonly object _lock = new object();
private readonly List<object> _objectsToIgnore = new List<object>();
private readonly Dictionary<object, List<string>> _warnings = new Dictionary<object, List<string>>();
private readonly Dictionary<object, List<string>> _errors = new Dictionary<object, List<string>>();
private readonly ObservableCollection<string> _warningMessages = new ObservableCollection<string>();
private readonly ObservableCollection<string> _errorMessages = new ObservableCollection<string>();
private bool _subscribedToEvents;
#endregion
#region Constructor & destructor
/// <summary>
/// Initializes static members of the <see cref="InfoBarMessageControl"/> class.
/// </summary>
static InfoBarMessageControl()
{
#if !SILVERLIGHT
DefaultStyleKeyProperty.OverrideMetadata(typeof(InfoBarMessageControl), new FrameworkPropertyMetadata(typeof(InfoBarMessageControl)));
#endif
}
/// <summary>
/// Initializes a new instance of the <see cref="InfoBarMessageControl"/> class.
/// </summary>
public InfoBarMessageControl()
{
#if !SILVERLIGHT
Focusable = false;
#endif
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
#endregion
#region Properties
/// <summary>
/// Info message for the info bar.
/// </summary>
public string InfoMessage
{
get { return (string)GetValue(InfoMessageProperty); }
set { SetValue(InfoMessageProperty, value); }
}
/// <summary>
/// DependencyProperty definition as the backing store for InfoMessage
/// </summary>
public static readonly DependencyProperty InfoMessageProperty =
DependencyProperty.Register("InfoMessage", typeof(string), typeof(InfoBarMessageControl), new PropertyMetadata(string.Empty));
/// <summary>
/// Gets or sets MessageCount.
/// </summary>
/// <remarks>
/// Wrapper for the MessageCount dependency property.
/// </remarks>
public int MessageCount
{
get { return (int)GetValue(MessageCountProperty); }
private set { SetValue(MessageCountProperty, value); }
}
/// <summary>
/// Definition of the dependency property is private.
/// </summary>
public static readonly DependencyProperty MessageCountProperty =
DependencyProperty.Register("MessageCount", typeof(int), typeof(InfoBarMessageControl), new PropertyMetadata(0));
/// <summary>
/// Gets the warning message collection.
/// </summary>
/// <value>The warning message collection.</value>
/// <remarks>
/// This property is not defined as dependency property, since it seems to cause some issues when several windows/controls with
/// this control are open at the same time (dependency properties seem to behave static, but they shouldn't).
/// </remarks>
public ObservableCollection<string> WarningMessageCollection
{
get { return _warningMessages; }
}
/// <summary>
/// Gets the error message collection.
/// </summary>
/// <value>The error message collection.</value>
/// <remarks>
/// This property is not defined as dependency property, since it seems to cause some issues when several windows/controls with
/// this control are open at the same time (dependency properties seem to behave static, but they shouldn't).
/// </remarks>
public ObservableCollection<string> ErrorMessageCollection
{
get { return _errorMessages; }
}
#endregion
#region Methods
/// <summary>
/// Called when the control is loaded.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
private void OnLoaded(object sender, RoutedEventArgs e)
{
SubscribeToEvents();
}
/// <summary>
/// Called when the control is unloaded.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
private void OnUnloaded(object sender, RoutedEventArgs e)
{
UnsubscribeFromEvents();
}
/// <summary>
/// Subscribes to events.
/// </summary>
private void SubscribeToEvents()
{
if (_subscribedToEvents)
{
return;
}
#if !SILVERLIGHT && USE_ROUTED_EVENTS
Validation.AddErrorHandler(this, infoBarMessage_ErrorValidation);
WarningAndErrorValidator.AddValidationHandler(this, infoBarMessage_Validation);
#else
// TODO: search for WarningAndErrorValidator in the tree
#endif
_subscribedToEvents = true;
}
/// <summary>
/// Unsubscribes from events.
/// </summary>
private void UnsubscribeFromEvents()
{
if (!_subscribedToEvents)
{
return;
}
#if !SILVERLIGHT && USE_ROUTED_EVENTS
Validation.RemoveErrorHandler(this, infoBarMessage_ErrorValidation);
WarningAndErrorValidator.RemoveValidationHandler(this, infoBarMessage_Validation);
#else
// TODO: search for WarningAndErrorValidator in the tree
#endif
_subscribedToEvents = false;
}
/// <summary>
/// Clears the object messages for the specified binding object.
/// </summary>
/// <param name="bindingObject">The binding object.</param>
/// <remarks>
/// This method is implemented because of the DataContext issue (DataContext cannot be changed before a
/// user control is loaded, and therefore might be binding to the wrong object).
/// </remarks>
internal void ClearObjectMessages(object bindingObject)
{
#if !SILVERLIGHT && USE_ROUTED_EVENTS
object realBindingObject = GetBindingObject(bindingObject);
#else
object realBindingObject = bindingObject;
#endif
ProcessValidationMessage(realBindingObject, null, ValidationEventAction.ClearAll, ValidationType.Warning);
ProcessValidationMessage(realBindingObject, null, ValidationEventAction.ClearAll, ValidationType.Error);
UpdateMessages();
}
/// <summary>
/// Adds an object to the ignore list so this control does not show messages for the specified object any longer.
/// </summary>
/// <param name="bindingObject">The binding object.</param>
internal void IgnoreObject(object bindingObject)
{
#if !SILVERLIGHT && USE_ROUTED_EVENTS
object realBindingObject = GetBindingObject(bindingObject);
#else
object realBindingObject = bindingObject;
#endif
_objectsToIgnore.Add(realBindingObject);
ClearObjectMessages(bindingObject);
}
#if !SILVERLIGHT && USE_ROUTED_EVENTS
/// <summary>
/// Handling data errors.
/// </summary>
/// <param name="sender">A sender.</param>
/// <param name="e">The event arguments</param>
private void infoBarMessage_ErrorValidation(object sender, ValidationErrorEventArgs e)
{
e.Handled = true;
ValidationEventAction validationEventAction = ValidationEventAction.Added;
if (e.Action == ValidationErrorEventAction.Added)
{
validationEventAction = ValidationEventAction.Added;
}
else if (e.Action == ValidationErrorEventAction.Removed)
{
validationEventAction = ValidationEventAction.Removed;
}
object bindingObject = GetBindingObject(e.Error.BindingInError);
string message = (e.Error != null) ? e.Error.ErrorContent.ToString() : string.Empty;
ProcessValidationMessage(bindingObject, message, validationEventAction, ValidationType.Error);
UpdateMessages();
}
#endif
/// <summary>
/// Handling business data errors.
/// </summary>
/// <param name="sender">A sender.</param>
/// <param name="e">The event arguments</param>
private void infoBarMessage_Validation(object sender, ValidationEventArgs e)
{
#if !SILVERLIGHT && USE_ROUTED_EVENTS
e.Handled = true;
object bindingObject = GetBindingObject(e.Error.BindingInError);
string message = (e.Error != null) ? e.Error.ErrorContent.ToString() : string.Empty;
ProcessValidationMessage(bindingObject, message, e.Action, e.Type);
#else
// TODO: handle
#endif
UpdateMessages();
}
#if !SILVERLIGHT && USE_ROUTED_EVENTS
/// <summary>
/// Gets the binding object.
/// </summary>
/// <param name="bindingObject">The binding object.</param>
/// <returns>object from the binding.</returns>
private static object GetBindingObject(object bindingObject)
{
object result;
// Check whether the data error is throwed on an single binding or a bindinggroup and process the error message
if (bindingObject as BindingExpression != null)
{
// Use data item of binding
result = ((BindingExpression)bindingObject).DataItem;
}
else if (bindingObject as BindingGroup != null)
{
// Use data group (object itself)
// ReSharper disable RedundantCast
result = ((BindingGroup)bindingObject);
// ReSharper restore RedundantCast
}
else
{
// Just use the object
result = bindingObject;
}
return result;
}
#endif
/// <summary>
/// Process an validation message.
/// </summary>
/// <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>
/// <param name="message">The actual warning or error message.</param>
/// <param name="action">An error event action. See <see cref="ValidationErrorEventAction"/>.</param>
/// <param name="type">The validation type.</param>
private void ProcessValidationMessage(object bindingObject, string message, ValidationEventAction action, ValidationType type)
{
if ((action != ValidationEventAction.ClearAll) && (bindingObject == null))
{
Log.Warn(TraceMessages.ProcessValidationCannotHandleNullValues);
return;
}
if (_objectsToIgnore.Contains(bindingObject) && (action != ValidationEventAction.ClearAll))
{
Log.Debug(TraceMessages.ObjectIsInIgnoreListThusMessagesWillNotBeHandled, bindingObject);
return;
}
Dictionary<object, List<string>> messages = (type == ValidationType.Warning) ? _warnings : _errors;
lock (_lock)
{
switch (action)
{
case ValidationEventAction.Added:
if (!messages.ContainsKey(bindingObject))
{
messages.Add(bindingObject, new List<string>());
}
if (!messages[bindingObject].Contains(message))
{
messages[bindingObject].Add(message);
}
break;
case ValidationEventAction.Removed:
if (messages.ContainsKey(bindingObject))
{
messages[bindingObject].Remove(message);
}
break;
case ValidationEventAction.ClearAll:
if (bindingObject != null)
{
messages.Remove(bindingObject);
}
else
{
messages.Clear();
}
break;
}
}
}
/// <summary>
/// Update the content of the control with the found warnings and errors.
/// </summary>
private void UpdateMessages()
{
lock (_lock)
{
UpdatesMessageCollection(_warningMessages, _warnings);
UpdatesMessageCollection(_errorMessages, _errors);
}
MessageCount = _warningMessages.Count + _errorMessages.Count;
InfoMessage = (MessageCount > 0) ? Properties.Resources.InfoBarMessageControlErrorTitle : string.Empty;
}
/// <summary>
/// Updates a message collection by adding new messages and removing old ones that no longer exist.
/// </summary>
/// <param name="messageCollection">The message collection.</param>
/// <param name="messageSource">The message source.</param>
private static void UpdatesMessageCollection(ObservableCollection<string> messageCollection, Dictionary<object, List<string>> messageSource)
{
foreach (List<string> sourceMessageCollection in messageSource.Values)
{
foreach (string message in sourceMessageCollection)
{
if (!messageCollection.Contains(message))
{
messageCollection.Add(message);
}
}
}
for (int i = messageCollection.Count - 1; i >= 0; i--)
{
string message = messageCollection[i];
bool isValid = false;
foreach (List<string> sourceMessageCollection in messageSource.Values)
{
if (sourceMessageCollection.Contains(message))
{
isValid = true;
break;
}
}
if (!isValid)
{
messageCollection.RemoveAt(i);
}
}
}
#endregion
}
}