Handling Dialogs in WPF with MVVM

2019-01-01 04:44发布

In the MVVM pattern for WPF, handling dialogs is one of the more complex operations. As your view model does not know anything about the view, dialog communication can be interesting. I can expose an ICommand that when the view invokes it, a dialog can appear.

Does anyone know of a good way to handle results from dialogs? I am speaking about windows dialogs such as MessageBox.

One of the ways we did this was have an event on the viewmodel that the view would subscribe to when a dialog was required.

public event EventHandler<MyDeleteArgs> RequiresDeleteDialog;

This is OK, but it means that the view requires code which is something I would like to stay away from.

23条回答
临风纵饮
2楼-- · 2019-01-01 05:00

Why not just raise an event in the VM and subscribe to the event in the view? This would keep the application logic and the view seperate and still allow you to use a child window for dialogs.

查看更多
刘海飞了
3楼-- · 2019-01-01 05:00

After spending some time with it, I finally came up with the following solution. A few key advantages of this approach are:

  1. Implements MVVM Light's own IDialogService.
  2. View doesn't need to add MVVM Light's reference.
  3. VM doesn't need to do any presentation-level activity. Doesn't even need PresentationFramework reference.
  4. Uses MVVM Light's own Messenger channel, so the presentation and VM layers are decoupled.
  5. Supports dialogs with a return value, such as Yes/No questions or OK/Cancel situations.
  6. Supports custom dialogs.

Here's the implementation of IDialogService (goes into ViewModel project):

using System;
using System.Linq;
using System.Threading.Tasks;

namespace VM
{
  public enum MessageBoxButtonVM
  {
    OK,
    OKCancel,
    YesNo
  }

  public enum MessageBoxImageVM
  {
    None,
    Information,
    Question,
    Error
  }

  public class MessageBoxArgs
  {
    public MessageBoxButtonVM Buttons { get; set; }
    public MessageBoxImageVM Icon { get; set; }
    public string Title { get; set; }
    public string Message { get; set; }
  }

  //For custom dialogs that return a value
  public class MessageBoxNotificationWithAction<T>
  {
    private readonly Action<T> _callback;

    public MessageBoxArgs Notification { get; set; }

    public MessageBoxNotificationWithAction(MessageBoxArgs notification, Action<T> callback)
    {
      Notification = notification;

      CheckCallback(callback);
      _callback = callback;
    }

    public virtual void Execute(T argument)
    {
      _callback.Invoke(argument);
    }

    private static void CheckCallback(Delegate callback)
    {
      if (callback == null)
      {
        throw new ArgumentNullException(nameof(callback), "Callback must not be null");
      }
    }
  }

  /// <summary>
  /// Provides an implementation-agnostic way of communicating with the user through dialog boxes. Clients must register for communication messages using
  /// MVVM Light messaging system.
  /// </summary>
  public class DialogService : GalaSoft.MvvmLight.Views.IDialogService
  {
    private static GalaSoft.MvvmLight.Messaging.IMessenger Messenger = GalaSoft.MvvmLight.Messaging.Messenger.Default;

    private string _ProductName = "";

    public string ProductName
    {
      get
      {
        if (_ProductName == "")
        {
          //The following statement returns the Title attribute of the current assembly, as defined in project properties (Assembly Information dialog).
          var TitleAttrib = System.Reflection.Assembly.GetExecutingAssembly().GetCustomAttributesData().First(x => x.AttributeType.Name == "AssemblyTitleAttribute");

          if (TitleAttrib != null)
          {
            _ProductName = TitleAttrib.ConstructorArguments[0].Value.ToString();
          }
          else
          {
            _ProductName = "Default Application Name";
          }
        }

        return _ProductName;
      }
    }

    public Task ShowError(Exception error, string title, string buttonText, Action afterHideCallback)
    {
      return ShowError(error.Message, title, buttonText, afterHideCallback);
    }

    public Task ShowMessage(string message, string title)
    {
      return Task.Run(() => MessengerSend(message, title, MessageBoxButtonVM.OK, MessageBoxImageVM.Error));
    }

    public Task ShowError(string message, string title, string buttonText, Action afterHideCallback)
    {
      return Task.Run(() =>
      {
        MessengerSend(message, title, MessageBoxButtonVM.OK, MessageBoxImageVM.Error);
        afterHideCallback?.Invoke();
      });
    }

    public Task ShowMessage(string message, string title, string buttonText, Action afterHideCallback)
    {
      return Task.Run(() =>
      {
        MessengerSend(message, title);
        afterHideCallback?.Invoke();
      });
    }

    public Task<bool> ShowMessage(string message, string title, string buttonConfirmText, string buttonCancelText, Action<bool> afterHideCallback)
    {
      if ((buttonConfirmText == "OK" && buttonCancelText == "Cancel") ||
        (buttonConfirmText == "Yes" && buttonCancelText == "No"))
      {
        return Task.Run<bool>(() =>
        {
          MessageBoxButtonVM btn;
          if (buttonConfirmText == "OK")
            btn = MessageBoxButtonVM.OKCancel;
          else
            btn = MessageBoxButtonVM.YesNo;


          bool Response = false;
          Messenger.Send(new MessageBoxNotificationWithAction<bool>(
                                                      new MessageBoxArgs()
                                                      {
                                                        Buttons = btn,
                                                        Icon = MessageBoxImageVM.Question,
                                                        Title = (string.IsNullOrEmpty(title) ? _ProductName : title),
                                                        Message = message
                                                      },
                                                      (result) => Response = result
                                                        ));

          afterHideCallback?.Invoke(Response);

          return Response;
        });
      }
      else
        throw new ArgumentException($"{nameof(buttonConfirmText)} and {nameof(buttonCancelText)} must either be OK/Cancel or Yes/No.");
    }

    /// <summary>
    /// For debugging purpose only
    /// </summary>
    /// <param name="message"></param>
    /// <param name="title"></param>
    /// <returns></returns>
    public Task ShowMessageBox(string message, string title) => ShowMessage(message, title);

    private void MessengerSend(string msg, string title = "", MessageBoxButtonVM btn = MessageBoxButtonVM.OK, MessageBoxImageVM icon = MessageBoxImageVM.Information)
    {
      Messenger.Send(new MessageBoxArgs()
      {
        Buttons = MessageBoxButtonVM.OK,
        Icon = MessageBoxImageVM.Information,
        Title = (string.IsNullOrEmpty(title) ? _ProductName : title),
        Message = msg
      });
    }
  }
}

Here's the presentation layer (goes into View project)

using System.Windows;
using VM;

namespace View
{
  class DialogPresenter
  {
    private Window _Parent;

    public DialogPresenter()
    {
      //For simple information boxes
      GalaSoft.MvvmLight.Messaging.Messenger.Default.Register<MessageBoxArgs>(this, (arg) => ShowDialog(arg));

      //For Yes/No or OK/Cancel dialog boxes.
      GalaSoft.MvvmLight.Messaging.Messenger.Default.Register<MessageBoxNotificationWithAction<bool>>(this, (arg) => arg.Execute(ShowDialog(arg.Notification)));

      //For notifications that require a string response (such as Manual Timeslot Description)
      GalaSoft.MvvmLight.Messaging.Messenger.Default.Register<MessageBoxNotificationWithAction<string>>(this,
        (arg) => arg.Execute(ShowStringInputDialog(arg.Notification.Title, arg.Notification.Message)));
    }

    private bool ShowDialog(MessageBoxArgs arg)
    {
      MessageBoxButton btn = MessageBoxButton.OK;
      MessageBoxImage ico = MessageBoxImage.None;

      switch (arg.Buttons)
      {
        case MessageBoxButtonVM.OK: btn = MessageBoxButton.OK; break;
        case MessageBoxButtonVM.OKCancel: btn = MessageBoxButton.OKCancel; break;
        case MessageBoxButtonVM.YesNo: btn = MessageBoxButton.YesNo; break;
      }

      switch (arg.Icon)
      {
        case MessageBoxImageVM.Error: ico = MessageBoxImage.Error; break;
        case MessageBoxImageVM.Information: ico = MessageBoxImage.Information; break;
        case MessageBoxImageVM.None: ico = MessageBoxImage.None; break;
        case MessageBoxImageVM.Question: ico = MessageBoxImage.Question; break;
      }

      bool Result = false;
      _Parent.Dispatcher.Invoke(() =>
      {
        var Res = MessageBox.Show(arg.Message, arg.Title, btn, ico);
        Result = (Res == MessageBoxResult.OK || Res == MessageBoxResult.Yes);
      });

      return Result;
    }

    private string ShowStringInputDialog(string title, string description, string value = "", int maxLength = 100)
    {
      string Result = null;

      _Parent.Dispatcher.Invoke(() =>
      {
        //InputBox is a WPF Window I created for taking simple
        //string values from the user. This also shows that you can
        //any custom dialog using this approach.

        InputBox input = new InputBox();
        input.Title = title;
        input.Owner = _Parent;
        if (input.ShowDialog(description, value, maxLength).Value)
          Result=input.Value;
        else
          Result=null;
      });

      return Result;
    }

    //Call this somewhere at application startup so that the dialog boxes
    //appear as child windows.
    public void SetParentWindow(Window parent)
    {
      _Parent = parent;
    }
  }
}
查看更多
旧时光的记忆
4楼-- · 2019-01-01 05:01

I struggled with the same problem. I have come up with a way to intercommunicate between the View and the ViewModel. You can initiate sending a message from the ViewModel to the View to tell it to show a messagebox and it will report back with the result. Then the ViewModel can respond to the result returned from the View.

I demonstrate this in my blog:

查看更多
梦醉为红颜
5楼-- · 2019-01-01 05:02

I really struggled with this concept for a while when learning (still learning) MVVM. What I decided, and what I think others already decided but which wasn't clear to me is this:

My original thought was that a ViewModel should not be allowed to call a dialog box directly as it has no business deciding how a dialog should appear. Beacause of this I started thinking about how I could pass messages much like I would have in MVP (i.e. View.ShowSaveFileDialog()). However, I think this is the wrong approach.

It is OK for a ViewModel to call a dialog directly. However, when you are testing a ViewModel , that means that the dialog will either pop up during your test, or fail all together (never really tried this).

So, what needs to happen is while testing is to use a "test" version of your dialog. This means that for ever dialog you have, you need to create an Interface and either mock out the dialog response or create a testing mock that will have a default behaviour.

You should already be using some sort of Service Locator or IoC that you can configure to provide you the correct version depending on the context.

Using this approach, your ViewModel is still testable and depending on how you mock out your dialogs, you can control the behaviour.

Hope this helps.

查看更多
姐姐魅力值爆表
6楼-- · 2019-01-01 05:03

I use this approach for dialogs with MVVM.

All I have to do now is call the following from my view model.

var result = this.uiDialogService.ShowDialog("Dialogwindow title goes here", dialogwindowVM);
查看更多
与君花间醉酒
7楼-- · 2019-01-01 05:03

Karl Shifflett has created a sample application for showing dialog boxes using service approach and Prism InteractionRequest approach.

I like the service approach - It's less flexible so users are less likely to break something :) It's also consistent with the WinForms part of my application (MessageBox.Show) But if you plan to show a lot of different dialogs, then InteractionRequest is a better way to go.

http://karlshifflett.wordpress.com/2010/11/07/in-the-box-ndash-mvvm-training/

查看更多
登录 后发表回答