Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Thomas Lebrun

Tout sur WPF, LINQ, C# et .NET en général !

Actualités

[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:

image

Le résultat est sans appel !

image

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 Smile

 

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 :
Posted: mardi 22 septembre 2009 11:45 par Thomas LEBRUN

Commentaires

romain verdier a dit :

Ce ServiceProvider me fait un peu penser au conteneur IoC du pauvre. Sans l'inversion de contrôle, hé.

# septembre 22, 2009 12:58

tomlev a dit :

Sympa ! Ca faisait longtemps que j'expérimentais diverses solutions pour gérer ça proprement, et celle-ci me plait bien :)

Juste une petite suggestion : pour stocker les objets dans le ServiceProvider, tu pourrais utiliser la classe KeyedByTypeCollection

<TItem>, qui sert à ça (http://msdn.microsoft.com/fr-fr/library/ms404549.aspx)

# septembre 22, 2009 13:54

richardc a dit :

C'est aussi utilisé dans XNA ;-)

# septembre 22, 2009 14:17

Thomas LEBRUN a dit :

Aucune idée, je ne fais pas de XNa.

Mais si tu le dis, je te fais confiance ;)

# septembre 22, 2009 14:24
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- [TechDays2012] Oui j’y serai! par Blog de Jérémy Jeanson le il y a 12 heures et 1 minutes

- TFS Integration Tools – Suivi des synchronisations avec Reporting Services par Vivien Fabing le 02-05-2012, 17:46

- CSS Content State Selectors (Personnal Draft) par Le blog de FremyCompany le 02-04-2012, 15:38

- MBA : Pourquoi faire et comment le choisir ? par Blog Technique de Romelard Fabrice le 02-03-2012, 14:22

- Y'a des erreurs qui peuvent rendre le développeur violent par Aleks's Blog le 02-02-2012, 16:33

- [Hyper-V 3] Présentation des commandlets PowerShell par Blog de SPBrouillet (Pierrick BROUILLET) le 01-31-2012, 16:01

- IIS7 – Compression GZIP par Atteint de JavaScriptite Aiguë [Cyril Durand] le 01-31-2012, 15:52

- SharePoint 15 Technical Preview Managed Object Model Software Development Kit par Matthew le 01-31-2012, 12:34

- Office 15 Technical Preview - Open specification Update par Matthew le 01-31-2012, 10:14

- TFS Integration Tools – Installation par Vivien Fabing le 01-31-2012, 00:06