Потратив некоторое время на это, я наконец-то нашел следующее решение. Вот несколько ключевых преимуществ этого подхода:
- Реализует собственный MVVM Light
IDialogService
.
- Представлению не нужно добавлять ссылку на MVVM Light.
- ВМ не нужно выполнять какие-либо действия на уровне презентации. Не нуждается даже в справке
PresentationFramework
.
- Использует собственный канал Messenger MVVM Light, поэтому уровни представления и виртуальной машины не связаны.
- Поддерживает диалоги с возвращаемым значением, такие как «Да / Нет вопросов» или «ОК / Отмена».
- Поддерживает пользовательские диалоги.
Вот реализация IDialogService
(входит в ViewModel проект):
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
});
}
}
}
Вот слой представления (входит в 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;
}
}
}