65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (76投票s)

2009年5月26日

Apache

8分钟阅读

viewsIcon

964492

downloadIcon

10970

一个框架,用于解决在 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.ShowDialogIDialogService.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 日:初始版本。
© . All rights reserved.