[MVVM] Comment afficher une boite de dialogue (afficher le détail d’une exception, poser une question, etc.) ?
La majorité des applications que l’on développe au jour le jour doivent forcément, à un moment ou à un autre, afficher des boites de dialogue aux utilisateurs. Que ce soit pour afficher une confirmation, poser une question ou même informer l’utilisateur qu’une exception est survenue: une boite de dialogue est nécessaire.
Dans le cas où l’on utilise le pattern MVVM pour développer ses applications WPF/Silverlight, cette question d’affichage des boites de dialogue est très importante car on souhaite éviter, le plus possible, d’avoir du code spécifique dans les ViewModels de l’application.
Jusqu’a présent, pour réaliser ce genre d’opération, je faisais quelque chose de très simple: j’utilisais une interface (IActions) que j’implémentais au niveau des différentes vues (donc dans le code behind). Puis, au moment d’assigner le DataContext, je passais en paramètre l’instance de la classe qui implémente l’interface (authrement this). Coté ViewModel, je récupérais un objet de type IActions et il ne me restait plus qu’a appeler ces méthodes. Bien que cela fonctionne et ne casse pas le pattern MVVM (je rappelle l’objectif de ce pattern qui n’est pas de n’avoir aucun code behind mais qui est d’avoir le minimum de code beind, celui-ci devant/pouvant être facilement testable), cela ne me plaisait pas vraiment car j’était obligé de modifier chacun des constructeurs de mes ViewModels pour y rajouter un paramètre, etc.
Puis, j’ai (enfin) eu l’occasion de jeter un oeil à Cinch (http://www.codeproject.com/KB/WPF/Cinch.aspx), un framework permettant de simplifier le développement d’applications en utilisant le pattern MVVM. Dans ce framework, l’auteur utilise une technique empruntée à Mark Smith (Mark Smith) pour utiliser un fournisseur de service permettant d’accéder à un ou plusieurs services.
Concrètement, voila comment cela se passe. On comment par créer un ServiceProvider, autrement dit une classe qui va implémenter l’interface IServiceProvider:
/// <summary>
/// This class acts as a resolver for typed services
/// (interfaces and implementations).
/// </summary>
public class ServiceProvider : IServiceProvider
{
#region Data
private readonly Dictionary<Type, object> _services = new Dictionary<Type, object>();
#endregion
#region Public Methods
/// <summary>
/// Clears all services from the resolver list
/// </summary>
public void Clear()
{
if (_services != null && _services.Count > 0)
_services.Clear();
}
/// <summary>
/// Adds a new service to the resolver list
/// </summary>
/// <param name="type">Service Type (typically an interface)</param>
/// <param name="value">Object that implements service</param>
public void Add(Type type, object value)
{
if (type == null)
throw new ArgumentNullException("type");
if (value == null)
throw new ArgumentNullException("value");
lock (_services)
{
// Replacing existing service
if (_services.ContainsKey(type))
_services[type] = value;
// Adding new service
else
_services.Add(type, value);
}
}
/// <summary>
/// Remove a service
/// </summary>
/// <param name="type">Type to remove</param>
public void Remove(Type type)
{
if (type == null)
throw new ArgumentNullException("type");
lock (_services)
{
_services.Remove(type);
}
}
/// <summary>
/// This resolves a service type and returns the implementation. Note that this
/// assumes the key used to register the object is of the appropriate type or
/// this method will throw an InvalidCastException!
/// </summary>
/// <typeparam name="T">Type to resolve</typeparam>
/// <returns>Implementation</returns>
public T Resolve<T>()
{
return (T)GetService(typeof(T));
}
/// <summary>
/// Implementation of IServiceProvider
/// </summary>
/// <param name="serviceType">Service Type</param>
/// <returns>Object implementing service</returns>
public object GetService(Type serviceType)
{
lock (_services)
{
object value;
return _services.TryGetValue(serviceType, out value) ? value : null;
}
}
#endregion
}
A présent, il faut écrire l’interface qui sera exposée et qui va contenir les différentes méthodes utilisables:
/// <summary>
/// Available Button options.
/// Abstracted to allow some level of UI Agnosticness
/// </summary>
public enum CustomDialogButtons
{
Ok,
OkCancel,
YesNo,
YesNoCancel
}
/// <summary>
/// Available Icon options.
/// Abstracted to allow some level of UI Agnosticness
/// </summary>
public enum CustomDialogIcons
{
None,
Information,
Question,
Exclamation,
Stop,
Warning
}
/// <summary>
/// Available DialogResults options.
/// Abstracted to allow some level of UI Agnosticness
/// </summary>
public enum CustomDialogResults
{
None,
Ok,
Cancel,
Yes,
No
}
/// <summary>
/// This interface defines a interface that will allow
/// a ViewModel to show a messagebox
/// </summary>
public interface IMessageBoxService
{
/// <summary>
/// Shows an error message
/// </summary>
/// <param name="message">The error message</param>
void ShowError(string message);
/// <summary>
/// Shows an information message
/// </summary>
/// <param name="message">The information message</param>
void ShowInformation(string message);
/// <summary>
/// Shows an warning message
/// </summary>
/// <param name="message">The warning message</param>
void ShowWarning(string message);
/// <summary>
/// Displays a Yes/No dialog and returns the user input.
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
/// <returns>User selection.</returns>
CustomDialogResults ShowYesNo(string message, CustomDialogIcons icon);
/// <summary>
/// Displays a Yes/No/Cancel dialog and returns the user input.
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
/// <returns>User selection.</returns>
CustomDialogResults ShowYesNoCancel(string message, CustomDialogIcons icon);
/// <summary>
/// Displays a OK/Cancel dialog and returns the user input.
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
/// <returns>User selection.</returns>
CustomDialogResults ShowOkCancel(string message, CustomDialogIcons icon);
}
A noter que si vous désirez empoyer cette technique avec Silverlight, vous devrez adapter le code ci-dessus car les boites de dialogues Silverlight ne permettent pas d’afficher des images ou ne renvoit, comme valeur de retour, que OK et Cancel.
Maintenant que l’interface est définie, il reste à l’implémenter, ce que nous allons faire dans la classe suivante:
/// <summary>
/// This class implements the IMessageBoxService for WPF purposes.
/// </summary>
public class MessageBoxService : IMessageBoxService
{
#region IMessageBoxService Members
/// <summary>
/// Displays an error dialog with a given message.
/// </summary>
/// <param name="message">The message to be displayed.</param>
public void ShowError(string message)
{
ShowMessage(message, "Error", CustomDialogIcons.Stop);
}
/// <summary>
/// Displays an error dialog with a given message.
/// </summary>
/// <param name="message">The message to be displayed.</param>
public void ShowInformation(string message)
{
ShowMessage(message, "Information", CustomDialogIcons.Information);
}
/// <summary>
/// Displays an error dialog with a given message.
/// </summary>
/// <param name="message">The message to be displayed.</param>
public void ShowWarning(string message)
{
ShowMessage(message, "Warning", CustomDialogIcons.Warning);
}
/// <summary>
/// Displays a Yes/No dialog and returns the user input.
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
/// <returns>User selection.</returns>
public CustomDialogResults ShowYesNo(string message, CustomDialogIcons icon)
{
return ShowQuestionWithButton(message, icon, CustomDialogButtons.YesNo);
}
/// <summary>
/// Displays a Yes/No/Cancel dialog and returns the user input.
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
/// <returns>User selection.</returns>
public CustomDialogResults ShowYesNoCancel(string message, CustomDialogIcons icon)
{
return ShowQuestionWithButton(message, icon, CustomDialogButtons.YesNoCancel);
}
/// <summary>
/// Displays a OK/Cancel dialog and returns the user input.
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
/// <returns>User selection.</returns>
public CustomDialogResults ShowOkCancel(string message, CustomDialogIcons icon)
{
return ShowQuestionWithButton(message, icon, CustomDialogButtons.OkCancel);
}
#endregion
#region Private Methods
/// <summary>
/// Shows a standard System.Windows.MessageBox using the parameters requested
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="heading">The heading to be displayed</param>
/// <param name="icon">The icon to be displayed.</param>
private static void ShowMessage(string message, string heading, CustomDialogIcons icon)
{
MessageBox.Show(message, heading, MessageBoxButton.OK, GetImage(icon));
}
/// <summary>
/// Shows a standard System.Windows.MessageBox using the parameters requested
/// but will return a translated result to enable adhere to the IMessageBoxService
/// implementation required.
///
/// This abstraction allows for different frameworks to use the same ViewModels but supply
/// alternative implementations of core service interfaces
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
/// <param name="button"></param>
/// <returns>CustomDialogResults results to use</returns>
private static CustomDialogResults ShowQuestionWithButton(string message, CustomDialogIcons icon, CustomDialogButtons button)
{
MessageBoxResult result = MessageBox.Show(message, "Please confirm...", GetButton(button), GetImage(icon));
return GetResult(result);
}
/// <summary>
/// Translates a CustomDialogIcons into a standard WPF System.Windows.MessageBox MessageBoxImage.
/// This abstraction allows for different frameworks to use the same ViewModels but supply
/// alternative implementations of core service interfaces
/// </summary>
/// <param name="icon">The icon to be displayed.</param>
/// <returns>A standard WPF System.Windows.MessageBox MessageBoxImage</returns>
private static MessageBoxImage GetImage(CustomDialogIcons icon)
{
MessageBoxImage image = MessageBoxImage.None;
switch (icon)
{
case CustomDialogIcons.Information:
image = MessageBoxImage.Information;
break;
case CustomDialogIcons.Question:
image = MessageBoxImage.Question;
break;
case CustomDialogIcons.Exclamation:
image = MessageBoxImage.Exclamation;
break;
case CustomDialogIcons.Stop:
image = MessageBoxImage.Stop;
break;
case CustomDialogIcons.Warning:
image = MessageBoxImage.Warning;
break;
}
return image;
}
/// <summary>
/// Translates a CustomDialogButtons into a standard WPF System.Windows.MessageBox MessageBoxButton.
/// This abstraction allows for different frameworks to use the same ViewModels but supply
/// alternative implementations of core service interfaces
/// </summary>
/// <param name="btn">The button type to be displayed.</param>
/// <returns>A standard WPF System.Windows.MessageBox MessageBoxButton</returns>
private static MessageBoxButton GetButton(CustomDialogButtons btn)
{
MessageBoxButton button = MessageBoxButton.OK;
switch (btn)
{
case CustomDialogButtons.Ok:
button = MessageBoxButton.OK;
break;
case CustomDialogButtons.OkCancel:
button = MessageBoxButton.OKCancel;
break;
case CustomDialogButtons.YesNo:
button = MessageBoxButton.YesNo;
break;
case CustomDialogButtons.YesNoCancel:
button = MessageBoxButton.YesNoCancel;
break;
}
return button;
}
/// <summary>
/// Translates a standard WPF System.Windows.MessageBox MessageBoxResult into a
/// CustomDialogIcons.
/// This abstraction allows for different frameworks to use the same ViewModels but supply
/// alternative implementations of core service interfaces
/// </summary>
/// <param name="result">The standard WPF System.Windows.MessageBox MessageBoxResult</param>
/// <returns>CustomDialogResults results to use</returns>
private static CustomDialogResults GetResult(MessageBoxResult result)
{
CustomDialogResults customDialogResults = CustomDialogResults.None;
switch (result)
{
case MessageBoxResult.Cancel:
customDialogResults = CustomDialogResults.Cancel;
break;
case MessageBoxResult.No:
customDialogResults = CustomDialogResults.No;
break;
case MessageBoxResult.None:
customDialogResults = CustomDialogResults.None;
break;
case MessageBoxResult.OK:
customDialogResults = CustomDialogResults.Ok;
break;
case MessageBoxResult.Yes:
customDialogResults = CustomDialogResults.Yes;
break;
}
return customDialogResults;
}
#endregion
}
Et voila, le plus dur est fait ! En effet, maintenant, il suffit simplement d’utiliser tout cela. Ainsi, dans votre ViewModel de base, créer 2 champs privés et 1 propriété:
public static readonly ServiceProvider ServiceProvider = new ServiceProvider();
private IMessageBoxService m_Prompt;
public IMessageBoxService Prompt
{
get { return m_Prompt; }
}
Puis, rajoutez 2 constructeurs (dont un statique) qui vont se charger:
- d’ajouter, au ServiceProvider, une instance de classe qui implémente le service de MessageBox
- d’initialiser la propriété servant à appeler des boites de dialogues
static ViewModelBase()
{
// Regiser defaults
RegisterDefaultServices();
}
protected ViewModelBase()
{
// Fetch sefault services
FetchDefaultServices();
}
private static void RegisterDefaultServices()
{
// Add the service to the service provider
ServiceProvider.Add(typeof(IMessageBoxService), new MessageBoxService());
}
private void FetchDefaultServices()
{
// Retrieve the service from the service provider
m_Prompt = this.Resolve<IMessageBoxService>();
}
/// <summary>
/// This resolves a service type and returns the implementation.
/// </summary>
/// <typeparam name="T">Type to resolve</typeparam>
/// <returns>Implementation</returns>
protected T Resolve<T>()
{
return ServiceProvider.Resolve<T>();
}
Dès lors, vous n’avez plus qu’à utiliser la propriété Prompt, dans chacun de vos ViewModel héritant de ViewModelBase, pour pouvoir affichier des boites de dialogues:
Le résultat est sans appel !
L’avantage de cette technique, c’est qu’elle respecte à 100% le pattern MVVM mais surtout, que vous pouvez l’étendre comme bon vous semble pour rajouter vos propres services (comme un service de Log):
- on créé l’interface
- on développe la classe qui implémente l’interface
- on enregistre le service (de log) dans le service provider
- dans un ViewModel (de base ou non), on appele la méthode Resolve pour récupérer l’instance du service que l’on vient d’enregistrer !
C’est simple, rapide et très efficace !
A vos claviers 
A+
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :