在 WPF 或 UWP 中使用 MVVM 模式显示对话框






4.84/5 (76投票s)
一个框架,用于解决在 WPF 或 UWP 中使用 MVVM 模式时,从 ViewModel 打开对话框的问题。
目录
引言
本文将解决您在使用 MVVM 模式时可能遇到的一个问题,即从 ViewModel 打开对话框。预期读者具备 MVVM 模式的基础知识。Josh Smith 在 MSDN Magazine 上撰写了一篇精彩的文章,可作为不熟悉该模式的读者的起点。
已经存在许多 MVVM 框架,对于那些正在寻找更完整的 MVVM 解决方案的人,我建议您看看以下框架。
该框架并非一个完整的一体化 MVVM 框架。它旨在简化在 WPF 或 UWP 中使用 MVVM 时,从 ViewModel 打开对话框的概念。它在这方面做得相当不错,但仅限于此。它不包含任何花哨的 ViewModel 基类,也没有事件代理或服务定位器。您获得的唯一额外好处是能够以与其他类编写单元测试相同的方式,轻松地为您的 ViewModel 编写单元测试。这一点您将获得。
该框架内置支持打开以下对话框:
- 模态对话框
- 非模态对话框
- 消息框
- 打开文件对话框
- 保存文件对话框
- 文件夹浏览器对话框
WPF 用法
比框架的实现更有趣的是它的用法,所以我们先从这里开始。本章将演示显示支持的 WPF 对话框所需的代码。
显示对话框
对话框可以显示为模态或非模态。模态对话框会暂停代码执行并等待对话框结果,而非模态对话框会继续代码执行,而不等待任何对话框结果。显示对话框可以通过两种方式之一执行,即通过显式指定对话框类型,或通过隐式使用对话框类型定位器。这两种概念及其使用上的区别将在接下来的章节中进行描述。
显式对话框类型语法
最直接的语法是显式语法,其中泛型方法 IDialogService.ShowDialog<T>
和 IDialogService.Show<T>
分别显示类型为 T
的模态和非模态对话框。MVVM 纯粹主义者肯定会因为在 ViewModel 中定义了视图类型而感到震惊。对他们来说,存在隐式语法和对话框类型定位器。
隐式对话框类型语法和对话框类型定位器
在 ViewModel 中指定对话框类型在某些情况下可能是不受欢迎的或不可能的,因此该框架支持在不指定对话框类型的情况下打开对话框。IDialogService.ShowDialog
和 IDialogService.Show
是非泛型方法,在方法调用中未指定对话框类型。但是,IDialogService
仍然需要知道对话框类型才能创建和打开对话框。这就是对话框类型定位器概念发挥作用的地方。
对话框类型定位器是类型为 Func<INotifyPropertyChanged, Type>
的函数,它能够根据指定的 ViewModel 解析对话框类型。DialogService
的实现带有一个默认的对话框类型定位器,它使用在许多关于 MVVM 模式的文章和代码示例中使用的常见命名约定。该约定规定,如果 ViewModel 的名称是 MyNamespace.ViewModels.MyDialogViewModel
,那么对话框的名称是 MyNamespace.Views.MyDialog
。如果此约定不适合您的代码结构,可以通过在 DialogService
的构造函数中指定自己的实现来覆盖默认定位器。
使用显式对话框类型语法显示模态对话框
要使用显式对话框类型语法显示模态对话框,首先通过使用附加属性 DialogServiceViews.IsRegistered
装饰 XAML 来注册视图。
<UserControl x:Class="DemoApplication.Features.Dialog.Modal.Views.ModalDialogTabContent" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:md="https://github.com/fantasticfiasco/mvvm-dialogs" md:DialogServiceViews.IsRegistered="True"> </UserControl>
在 ViewModel 中,通过调用 IDialogService.ShowDialog<T>
来打开对话框。
public class ModalDialogTabContentViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public ModalDialogTabContentViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private void ShowDialog() { var dialogViewModel = new AddTextDialogViewModel(); bool? success = dialogService.ShowDialog<AddTextDialog>(this, dialogViewModel)); if (success == true) { Texts.Add(dialogViewModel.Text); } } }
使用隐式对话框类型语法显示模态对话框
要使用隐式对话框类型语法显示模态对话框,首先通过使用附加属性 DialogServiceViews.IsRegistered
装饰 XAML 来注册视图。
<UserControl x:Class="DemoApplication.Features.Dialog.Modal.Views.ModalDialogTabContent" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:md="https://github.com/fantasticfiasco/mvvm-dialogs" md:DialogServiceViews.IsRegistered="True"> </UserControl>
确保对话框类型定位器可以找到对话框类型,然后让 ViewModel 通过调用 IDialogService.ShowDialog
来打开对话框。
public class ModalDialogTabContentViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public ModalDialogTabContentViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private void ShowDialog() { var dialogViewModel = new AddTextDialogViewModel(); bool? success = dialogService.ShowDialog(this, dialogViewModel)); if (success == true) { Texts.Add(dialogViewModel.Text); } } }
使用显式对话框类型语法显示非模态对话框
要使用显式对话框类型语法显示非模态对话框,首先通过使用附加属性 DialogServiceViews.IsRegistered
装饰 XAML 来注册视图。
<UserControl x:Class="DemoApplication.Features.Dialog.NonModal.Views.NonModalDialogTabContent" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:md="https://github.com/fantasticfiasco/mvvm-dialogs" md:DialogServiceViews.IsRegistered="True"> </UserControl>
在 ViewModel 中,通过调用 IDialogService.Show<T>
来打开对话框。
public class NonModalDialogTabContentViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public NonModalDialogTabContentViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private void Show() { var dialogViewModel = new CurrentTimeDialogViewModel(); dialogService.Show<CurrentTimeDialog>(this, dialogViewModel)); } }
使用隐式对话框类型语法显示非模态对话框
要使用隐式对话框类型语法显示非模态对话框,首先通过使用附加属性 DialogServiceViews.IsRegistered
装饰 XAML 来注册视图。
<UserControl x:Class="DemoApplication.Features.Dialog.NonModal.Views.NonModalDialogTabContent" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:md="https://github.com/fantasticfiasco/mvvm-dialogs" md:DialogServiceViews.IsRegistered="True"> </UserControl>
确保对话框类型定位器可以找到对话框类型,然后让 ViewModel 通过调用 IDialogService.Show
来打开对话框。
public class NonModalDialogTabContentViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public NonModalDialogTabContentViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private void Show() { var dialogViewModel = new CurrentTimeDialogViewModel(); dialogService.Show(this, dialogViewModel)); } }
显示消息框
要显示消息框,首先通过使用附加属性 DialogServiceViews.IsRegistered
装饰 XAML 来注册视图。
<UserControl x:Class="DemoApplication.Features.MessageBox.Views.MessageBoxTabContent" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:md="https://github.com/fantasticfiasco/mvvm-dialogs" md:DialogServiceViews.IsRegistered="True"> </UserControl>
在 ViewModel 中,通过调用 IDialogService.ShowMessageBox
来打开对话框。
public class MessageBoxTabContentViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public MessageBoxTabContentViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private void ShowMessageBox() { dialogService.ShowMessageBox( this, "This is the text.", "This Is The Caption", MessageBoxButton.OKCancel, MessageBoxImage.Information); } }
显示打开文件对话框
要显示打开文件对话框,首先通过使用附加属性 DialogServiceViews.IsRegistered
装饰 XAML 来注册视图。
<UserControl x:Class="DemoApplication.Features.OpenFileDialog.Views.OpenFileTabContent" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:md="https://github.com/fantasticfiasco/mvvm-dialogs" md:DialogServiceViews.IsRegistered="True"> </UserControl>
在 ViewModel 中,通过调用 IDialogService.ShowOpenFileDialog
来打开对话框。
public class OpenFileTabContentViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public OpenFileTabContentViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private void OpenFile() { var settings = new OpenFileDialogSettings { Title = "This Is The Title", InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), Filter = "Text Documents (*.txt)|*.txt|All Files (*.*)|*.*" }; bool? success = dialogService.ShowOpenFileDialog(this, settings); if (success == true) { Path = settings.FileName; } }
显示保存文件对话框
要显示保存文件对话框,首先通过使用附加属性 DialogServiceViews.IsRegistered
装饰 XAML 来注册视图。
<UserControl x:Class="DemoApplication.Features.SaveFileDialog.Views.SaveFileTabContent" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:md="https://github.com/fantasticfiasco/mvvm-dialogs" md:DialogServiceViews.IsRegistered="True"> </UserControl>
在 ViewModel 中,通过调用 IDialogService.ShowSaveFileDialog
来打开对话框。
public class SaveFileTabContentViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public SaveFileTabContentViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private void SaveFile() { var settings = new SaveFileDialogSettings { Title = "This Is The Title", InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), Filter = "Text Documents (*.txt)|*.txt|All Files (*.*)|*.*", CheckFileExists = false }; bool? success = dialogService.ShowSaveFileDialog(this, settings); if (success == true) { Path = settings.FileName; } } }
显示文件夹浏览器对话框
要显示文件夹浏览器对话框,首先通过使用附加属性 DialogServiceViews.IsRegistered
装饰 XAML 来注册视图。
<UserControl x:Class="DemoApplication.Features.FolderBrowserDialog.Views.FolderBrowserTabContent" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:md="https://github.com/fantasticfiasco/mvvm-dialogs" md:DialogServiceViews.IsRegistered="True"> </UserControl>
在 ViewModel 中,通过调用 IDialogService.ShowFolderBrowserDialog
来打开对话框。
public class FolderBrowserTabContentViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public FolderBrowserTabContentViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private void BrowseFolder() { var settings = new FolderBrowserDialogSettings { Description = "This is a description" }; bool? success = dialogService.ShowFolderBrowserDialog(this, settings); if (success == true) { Path = settings.SelectedPath; } } }
UWP 用法
令人难以置信的是,这个框架可以在 UWP 上运行,换句话说,可以在 Raspberry PI 或任何其他支持 Windows 10 IoT 的设备上运行。本章将演示显示支持的 UWP 对话框所需的代码。
显示内容对话框
显示内容对话框可以通过两种方式之一执行,即通过显式指定对话框类型,或通过隐式使用对话框类型定位器。这两种概念及其使用上的区别将在下面进行描述。
显式对话框类型语法
最直接的语法是显式语法,其中泛型方法ShowContentDialogAsync<T>
显示类型为T
的内容对话框。MVVM 纯粹主义者肯定会因为在 ViewModel 中定义了视图类型而感到震惊。对他们来说,存在隐式语法和对话框类型定位器。
隐式对话框类型语法和对话框类型定位器
在 ViewModel 中指定对话框类型在某些情况下可能是不受欢迎的或不可能的,因此该框架支持在不指定对话框类型的情况下打开内容对话框。IDialogService.ShowContentDialogAsync
是非泛型方法,在方法调用中未指定对话框类型。但是,IDialogService
仍然需要知道对话框类型才能创建和打开对话框。这就是对话框类型定位器概念发挥作用的地方。
对话框类型定位器是类型为Func<INotifyPropertyChanged, Type>
的函数,它能够根据指定的 ViewModel 解析对话框类型。DialogService
的实现带有一个默认的对话框类型定位器,它使用在许多关于 MVVM 模式的文章和代码示例中使用的常见命名约定。该约定规定,如果 ViewModel 的名称是MyNamespace.ViewModels.MyDialogViewModel
,那么内容对话框的名称是MyNamespace.Views.MyDialog
。如果此约定不适合您的代码结构,可以通过在DialogService
的构造函数中指定自己的实现来覆盖默认定位器。
使用显式对话框类型语法显示内容对话框
要使用显式对话框类型语法显示内容对话框,请从 ViewModel 调用IDialogService.ShowContentDialogAsync<T>
。
public class MainPageViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public MainPageViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private async void ShowContentDialog() { var viewModel = new AddTextContentDialogViewModel(); ContentDialogResult result = await dialogService.ShowContentDialogAsync<AddTextContentDialog>(viewModel) if (result == ContentDialogResult.Primary) { Texts.Add(dialogViewModel.Text); } } }
使用隐式对话框类型语法显示内容对话框
要使用隐式对话框类型语法显示内容对话框,请从 ViewModel 调用IDialogService.ShowContentDialogAsync
。
public class MainPageViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public MainPageViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private async void ShowContentDialog() { var viewModel = new AddTextContentDialogViewModel(); ContentDialogResult result = await dialogService.ShowContentDialogAsync(viewModel) if (result == ContentDialogResult.Primary) { Texts.Add(dialogViewModel.Text); } } }
显示消息对话框
在 ViewModel 中,通过调用IDialogService.ShowMessageDialogAsync
来打开对话框。
public class MainPageViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public MainPageViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private async void ShowMessageDialog() { await dialogService.ShowMessageDialogAsync( "This is the text.", "This Is The Title", new[] { new UICommand { Label = "OK" }, new UICommand { Label = "Close" } }); } }
显示单选和多选文件选择器
选择单个文件
在 ViewModel 中,通过调用IDialogService.PickSingleFileAsync
来打开对话框。
public class MainPageViewModel : ViewModelBase { private readonly IDialogService dialogService; public MainPageViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private async void PickSingleFile() { var settings = new FileOpenPickerSettings { SuggestedStartLocation = PickerLocationId.DocumentsLibrary, FileTypeFilter = new List<string> { ".txt" } }; StorageFile storageFile = await dialogService.PickSingleFileAsync(settings); if (storageFile != null) { SingleFilePath = storageFile.Path; } } }
选择多个文件
在 ViewModel 中,通过调用IDialogService.PickMultipleFilesAsync
来打开对话框。
public class MainPageViewModel : ViewModelBase { private readonly IDialogService dialogService; public MainPageViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private async void PickMultipleFiles() { var settings = new FileOpenPickerSettings { SuggestedStartLocation = PickerLocationId.DocumentsLibrary, FileTypeFilter = new List<string> { ".txt" } }; IReadOnlyList<StorageFile> storageFiles = await dialogService.PickMultipleFilesAsync(settings); if (storageFiles.Any()) { MultipleFilesPath = string.Join(";", storageFiles.Select(storageFile => storageFile.Path)); } } }
显示保存文件选择器
在 ViewModel 中,通过调用IDialogService.PickSaveFileAsync
来打开对话框。
public class MainPageViewModel : INotifyPropertyChanged { private readonly IDialogService dialogService; public MainPageViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private async void SaveFile() { var settings = new FileSavePickerSettings { SuggestedStartLocation = PickerLocationId.DocumentsLibrary, FileTypeChoices = new Dictionary<string, IList<string>> { { "Text Documents", new List<string> { ".txt" } } }, DefaultFileExtension = ".txt" }; StorageFile storageFile = await dialogService.PickSaveFileAsync(settings); if (storageFile != null) { Path = storageFile.Path; } } }
显示单选文件夹选择器
在 ViewModel 中,通过调用IDialogService.PickSingleFolderAsync
来打开对话框。
public class MainPageViewModel : ViewModelBase { private readonly IDialogService dialogService; public MainPageViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private async void BrowseFolder() { var settings = new FolderPickerSettings { SuggestedStartLocation = PickerLocationId.DocumentsLibrary, FileTypeFilter = new List<string> { ".txt" } }; StorageFolder storageFolder = await dialogService.PickSingleFolderAsync(settings); if (storageFolder != null) { Path = storageFolder.Path; } } }
GitHub
代码也可在 GitHub 上获取。欢迎您提交 issue 和 pull request。
NuGet
如果您想将 MVVM Dialogs 包含在您的项目中,您可以直接从 NuGet 安装。
要安装 MVVM Dialogs,请在程序包管理器控制台中运行以下命令:
PM> Install-Package MvvmDialogs
历史
- 2016 年 9 月 21 日:代码更新
- 更新了
DialogService
的构造函数,使该类更易于与 IoC 容器集成。
- 更新了
- 2016 年 5 月 22 日:代码更新
- 添加了对通用 Windows 平台 (UWP) 的支持。
- 2015 年 8 月 26 日:文章更新。
- 在 flyingxu 的评论之后,添加了关于与 MVVM Light 集成的信息。
- 2015 年 6 月 24 日:主要代码重构。
- 源代码可在 GitHub 上获取。
- 程序包可通过 NuGet 获取。
- 2010 年 10 月 5 日:代码更新。
- 根据 d302241 的评论更新了源代码。
- 2010 年 4 月 4 日:代码更新。
- 根据 Michael Sync 的评论更新了源代码。
- 转换为 .NET 4。
- 2009 年 6 月 18 日:代码更新。
- 代码不再在设计器模式下抛出异常。
- 修复了错误的接口摘要。
- 2009 年 6 月 2 日:代码更新。
- 向
IDialogService
添加了ShowOpenFileDialog
方法。 - 实现了服务定位器,而不是将
DialogService
保持为单例。
- 向
- 2009 年 5 月 27 日:文章更新。
- 根据 William E. Kempf 的评论更新了引言。
- 2009 年 5 月 25 日:初始版本。