标准方法
在花了几年时间在WPF中处理这个问题之后,我终于找到了在WPF中实现对话框的标准方法。以下是这种方法的优点:
清洁
不违反MVVM设计模式
ViewModal从不引用任何UI库(WindowBase, PresentationFramework等)
非常适合自动化测试
对话框可以很容易地被替换。
那么关键是什么呢?是DI + IoC。
下面是它的工作原理。我正在使用MVVM Light,但这种方法也可以扩展到其他框架:
Add a WPF Application project to your solution. Call it App.
Add a ViewModal Class Library. Call it VM.
App references VM project. VM project doesn't know anything about App.
Add NuGet reference to MVVM Light to both projects. I'm using MVVM Light Standard these days, but you are okay with the full Framework version too.
Add an interface IDialogService to VM project:
public interface IDialogService
{
void ShowMessage(string msg, bool isError);
bool AskBooleanQuestion(string msg);
string AskStringQuestion(string msg, string default_value);
string ShowOpen(string filter, string initDir = "", string title = "");
string ShowSave(string filter, string initDir = "", string title = "", string fileName = "");
string ShowFolder(string initDir = "");
bool ShowSettings();
}
Expose a public static property of IDialogService type in your ViewModelLocator, but leave registration part for the View layer to perform. This is the key.:
public static IDialogService DialogService => SimpleIoc.Default.GetInstance<IDialogService>();
Add an implementation of this interface in the App project.
public class DialogPresenter : IDialogService
{
private static OpenFileDialog dlgOpen = new OpenFileDialog();
private static SaveFileDialog dlgSave = new SaveFileDialog();
private static FolderBrowserDialog dlgFolder = new FolderBrowserDialog();
/// <summary>
/// Displays a simple Information or Error message to the user.
/// </summary>
/// <param name="msg">String text that is to be displayed in the MessageBox</param>
/// <param name="isError">If true, Error icon is displayed. If false, Information icon is displayed.</param>
public void ShowMessage(string msg, bool isError)
{
if(isError)
System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.OK, MessageBoxImage.Error);
else
System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.OK, MessageBoxImage.Information);
}
/// <summary>
/// Displays a Yes/No MessageBox.Returns true if user clicks Yes, otherwise false.
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
public bool AskBooleanQuestion(string msg)
{
var Result = System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
return Result;
}
/// <summary>
/// Displays Save dialog. User can specify file filter, initial directory and dialog title. Returns full path of the selected file if
/// user clicks Save button. Returns null if user clicks Cancel button.
/// </summary>
/// <param name="filter"></param>
/// <param name="initDir"></param>
/// <param name="title"></param>
/// <param name="fileName"></param>
/// <returns></returns>
public string ShowSave(string filter, string initDir = "", string title = "", string fileName = "")
{
if (!string.IsNullOrEmpty(title))
dlgSave.Title = title;
else
dlgSave.Title = "Save";
if (!string.IsNullOrEmpty(fileName))
dlgSave.FileName = fileName;
else
dlgSave.FileName = "";
dlgSave.Filter = filter;
if (!string.IsNullOrEmpty(initDir))
dlgSave.InitialDirectory = initDir;
if (dlgSave.ShowDialog() == DialogResult.OK)
return dlgSave.FileName;
else
return null;
}
public string ShowFolder(string initDir = "")
{
if (!string.IsNullOrEmpty(initDir))
dlgFolder.SelectedPath = initDir;
if (dlgFolder.ShowDialog() == DialogResult.OK)
return dlgFolder.SelectedPath;
else
return null;
}
/// <summary>
/// Displays Open dialog. User can specify file filter, initial directory and dialog title. Returns full path of the selected file if
/// user clicks Open button. Returns null if user clicks Cancel button.
/// </summary>
/// <param name="filter"></param>
/// <param name="initDir"></param>
/// <param name="title"></param>
/// <returns></returns>
public string ShowOpen(string filter, string initDir = "", string title = "")
{
if (!string.IsNullOrEmpty(title))
dlgOpen.Title = title;
else
dlgOpen.Title = "Open";
dlgOpen.Multiselect = false;
dlgOpen.Filter = filter;
if (!string.IsNullOrEmpty(initDir))
dlgOpen.InitialDirectory = initDir;
if (dlgOpen.ShowDialog() == DialogResult.OK)
return dlgOpen.FileName;
else
return null;
}
/// <summary>
/// Shows Settings dialog.
/// </summary>
/// <returns>true if User clicks OK button, otherwise false.</returns>
public bool ShowSettings()
{
var w = new SettingsWindow();
MakeChild(w); //Show this dialog as child of Microsoft Word window.
var Result = w.ShowDialog().Value;
return Result;
}
/// <summary>
/// Prompts user for a single value input. First parameter specifies the message to be displayed in the dialog
/// and the second string specifies the default value to be displayed in the input box.
/// </summary>
/// <param name="m"></param>
public string AskStringQuestion(string msg, string default_value)
{
string Result = null;
InputBox w = new InputBox();
MakeChild(w);
if (w.ShowDialog(msg, default_value).Value)
Result = w.Value;
return Result;
}
/// <summary>
/// Sets Word window as parent of the specified window.
/// </summary>
/// <param name="w"></param>
private static void MakeChild(System.Windows.Window w)
{
IntPtr HWND = Process.GetCurrentProcess().MainWindowHandle;
var helper = new WindowInteropHelper(w) { Owner = HWND };
}
}
While some of these functions are generic (ShowMessage, AskBooleanQuestion etc.), others are specific to this project and use custom Windows. You can add more custom windows in the same fashion. The key is to keep UI-specific elements in the View layer and just expose the returned data using POCOs in the VM layer.
Perform IoC Registration your interface in the View layer using this class. You can do this in your main view's constructor (after InitializeComponent() call):
SimpleIoc.Default.Register<IDialogService, DialogPresenter>();
There you go. You now have access to all your dialog functionality at both VM and View layers. Your VM layer can call these functions like this:
var NoTrump = ViewModelLocator.DialogService.AskBooleanQuestion("Really stop the trade war???", "");
So clean you see. The VM layer doesn't know nothing about how a Yes/No question will be presented to the user by the UI layer and can still successfully work with the returned result from the dialog.
其他免费福利
For writing unit test, you can provide a custom implementation of IDialogService in your Test project and register that class in IoC in the constructor your test class.
You'll need to import some namespaces such as Microsoft.Win32 to access Open and Save dialogs. I have left them out because there is also a WinForms version of these dialogs available, plus someone might want to create their own version. Also note that some of the identifier used in DialogPresenter are names of my own windows (e.g. SettingsWindow). You'll need to either remove them from both the interface and implementation or provide your own windows.
If your VM performs multi-threading, call MVVM Light's DispatcherHelper.Initialize() early in your application's life cycle.
Except for DialogPresenter which is injected in the View layer, other ViewModals should be registered in ViewModelLocator and then a public static property of that type should be exposed for the View layer to consume. Something like this:
public static SettingsVM Settings => SimpleIoc.Default.GetInstance<SettingsVM>();
For the most part, your dialogs should not have any code-behind for stuff like binding or setting DataContext etc. You shouldn't even pass things as constructor parameters. XAML can do that all for you, like this:
<Window x:Class="YourViewNamespace.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="clr-namespace:YourViewProject"
xmlns:vm="clr-namespace:YourVMProject;assembly=YourVMProject"
DataContext="{x:Static vm:ViewModelLocator.Settings}"
d:DataContext="{d:DesignInstance Type=vm:SettingsVM}" />
Setting DataContext this way gives you all kinds of design-time benefits such as Intellisense and auto-completion.
希望这对大家都有帮助。