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

AvalonDock 和 MVVM

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (49投票s)

2011年8月11日

CPOL

34分钟阅读

viewsIcon

234066

downloadIcon

9243

演示了将AvalonDock与MVVM应用程序集成的一种技术。

示例代码

示例代码

目录

引言

您是否正在使用或希望使用 AvalonDock

您是否希望将其与 MVVM 一起使用?

在本文中,我将演示一种在MVVM应用程序中适配AvalonDock使用的方法。

我为本文开发的示例应用程序是一个简单的文本编辑器。该技术的核心代码在一个我称之为AvalonDockHost的类中。该类位于其自己的项目中,可以在其他AvalonDock应用程序中重用。

AvalonDockHost封装了AvalonDock,并作为一个适配器,使其能够与您MVVM应用程序的其余部分相对和谐地共存。然而,需要注意的是,AvalonDockHost并非旨在成为AvalonDock的完整封装,也并非旨在让AvalonDock更容易使用。我确实相信最终结果是AvalonDock更容易使用,但只有通过MVVM通常使事情变得更容易的方式,但通常只有在您已经了解MVVM和底层技术的情况下。所以更简单地说,要充分利用这一点,如果您已经了解了至少使用AvalonDock的基础知识,那将会有帮助。

本文包含三个部分。第一部分是对示例应用程序的演练,并说明了如何使用AvalonDockHost。第二部分讨论了AvalonDockHost的实现。如果您对实现不感兴趣,请随时跳过第二部分。第三部分是对AvalonDockHost的简短参考。

屏幕截图

这张截图显示了示例应用程序。选项卡式文档区域包含多个打开的文本文件。右上方窗格是当前打开文档的列表。右下方窗格是当前活动文档的简单概述。

假设知识

假设您了解 C#,并且至少对 WPFMVVM 有基本的掌握。

我还假设您理解 AvalonDock 的基本工作原理和功能。要快速掌握,我建议您阅读 AvalonDock 入门教程

提及这一点似乎太明显了,但我还是会说。如果您不知道AvalonDock或MVVM是什么,或者即使您知道这些技术但不知道如何使用它们,那么本文可能不是您正在寻找的。本文并非旨在成为关于使用AvalonDock或MVVM的教程,而是关于如何将两者合并的文章。

背景

我使用AvalonDock已经很多年了,我认为它总体上非常棒,但是开箱即用它不支持MVVM。尽管他们可能有很多原因,但我真的希望能够将AvalonDock与MVVM一起使用!

在我使用AvalonDock的大部分时间里,我都是以非MVVM的方式使用的。我不能说我对这种情况完全不满意,但我确实有一段时间感到有些不适。

当然,AvalonDock还有其他替代方案,也有一些现有的AvalonDock封装。然而,经过一些研究和实验,我没有找到让我满意的,于是我开始编写自己的AvalonDock MVVM封装。我也觉得社区缺乏一个清晰简单的关于如何将AvalonDock与MVVM一起使用的说明。

我感到惊喜,我希望在阅读本文后您也会同意,它实际上并没有想象中那么困难。经过几个小时的实验和思考,我写出了本文代码的初稿。

目标和概念

我将通过一个简单的文本编辑器来演示AvalonDock MVVM技术。在阅读文章时,请牢记示例应用程序仅仅是一个玩具,其复杂程度仅限于必要。主要目标是演示在MVVM应用程序中使用AvalonDock。

我希望在我的应用程序的视图模型中添加文档和窗格的视图模型,并自动实例化相应的AvalonDock组件。我还希望能够通过我的视图模型间接操作AvalonDock组件。例如,设置活动文档/窗格以及以编程方式显示/隐藏窗格。

AvalonDockHost是此技术核心的类。它是一个WPF用户控件,其目的是适配AvalonDock到MVVM,并允许文档和窗格反映在应用程序的视图模型中。像任何好的WPF可重用控件一样,AvalonDockHost将对应用程序视图模型的结构做出很少的假设。

最后,我希望AvalonDockHost具有最少的依赖项。目前,它仅依赖于AvalonDock和.Net框架。我认为这非常重要,因为现有的解决方案通常会附带我不需要的附加代码、库和臃肿。使用本文的代码,我不需要您使用任何其他组件,这意味着您可以自由集成您想要的任何其他组件,例如MVVM框架或扩展框架,如果您这样做,我很想听听您的经验。

示例应用程序演练

解决方案和项目

解压 AvalonDockMVVMSample.zip 并用Visual Studio打开 AvalonDockMVVMSample.2008.sln(我仍在用VS 2008,如果您使用VS 2010,请使用 AvalonDockMVVMSample.2010.sln)。

该解决方案包含以下项目

主类AvalonDockHost可以在AvalonDockMVVM项目中找到。我将在实现部分详细讨论AvalonDockHost的内部工作。在演练中,我只会讨论AvalonDockHost的使用方法。

SampleApp项目包含应用程序、主窗口和窗格的视图。

ViewModels项目包含应用程序的所有视图模型类。

运行示例应用程序

您应该先运行示例应用程序并探索其功能。示例应用程序具有简单文本编辑器应有的功能:创建、打开、保存和关闭文档。视图菜单允许隐藏和显示窗格。

以下带注释的截图显示了视图菜单,并标示了应用程序的各个组件。

文档在中央的选项卡式文档区域中创建和打开。在选项卡式文档区域的右侧有两个可停靠的窗格。上方窗格是当前打开文档的列表。下方窗格是当前活动文档的简单概述。我将文档和窗格统称为面板

正如您对AvalonDock的期望一样,面板可以从主窗口中分离出来,作为浮动窗口,或重新停靠在主窗口的不同位置。这允许用户自定义应用程序的布局。

AvalonDockHost、视图和视图模型

示例应用程序有三个不同的视图类:MainWindowOpenDocumentsPaneViewDocumentOverviewPaneView。还有一个视图没有单独的类。文本文件文档的视图非常简单,仅包含一个WPF TextBox,我已将其视图作为DataTemplate内联到MainWindow.xaml中。ViewModels项目包含每个视图的视图模型类。

这是类的(简化)概述(感谢 StarUML

实线表示派生(类层次结构),虚线表示依赖或使用。

MainWindowViewModel具有DocumentsPanes属性,它们是文档和窗格视图模型的集合。这些属性在MainWindow.xaml中已数据绑定到AvalonDockHostDocumentsPanes属性。

    <AvalonDockMVVM:AvalonDockHost
        x:Name="avalonDockHost"
        Panes="{Binding Panes}"
        Documents="{Binding Documents}"
        ...
        />

有了这些数据绑定,现在将文档添加到AvalonDock就像将文档添加到视图模型的Documents集合一样容易。同样,通过将窗格添加到Panes集合来将其添加到AvalonDock。实际上,我们还没有完全完成,因为我们仍然需要了解AvalonDockHost如何将面板视图模型转换为适当的AvalonDock组件,我们很快就会讲到。

MainWindow.xaml中,有额外的ActiveDocumentActivePane数据绑定。

    <AvalonDockMVVM:AvalonDockHost
        ...
        ActiveDocument="{Binding ActiveDocument}"
        ActivePane="{Binding ActivePane}"
        ...
        />

ActiveDocument设置为活动文档的视图模型,ActivePane设置为活动窗格的视图模型。这些属性可以通过视图模型进行设置,并通过数据绑定将更改传播到AvalonDockHost,从而使AvalonDockHost激活并聚焦指定的AvalonDock组件。这些属性也反映了AvalonDock当前的内部状态。当用户直接与AvalonDock交互(绕过我们的视图模型)来选择和激活一个面板时,AvalonDockHost会收到通知,并根据需要设置ActiveDocumentActivePane。然后,这些更改通过数据绑定传播回视图模型。

现在我们已经查看了AvalonDockHost和视图模型之间的绑定,这里有一个图表总结了这些关系。

我经常将应用程序的视图模型视为一个视图模型树。在示例应用程序中确实是这样,尽管视图模型树在这里并不特别深。其他更复杂的应用程序在树中有更多的级别。树的根是主窗口的视图模型:MainWindowViewModel。在树的下一级是各个面板的视图模型。

下面的实例化图显示了打开文档时的(简化)视图模型树,如前面的截图所示。视图模型对象为蓝色,其关联的视图为紫色。

下一张图显示了相同运行的示例应用程序的视觉树的带注释的截图,并显示了AvalonDockHost和其他视图的位置。

为了了解视图模型树是如何实例化的,让我们看看MainWindowViewModel的构造函数。首先,它保存了传入的IDialogProvider接口的引用。

    public MainWindowViewModel(IDialogProvider dialogProvider)
    {
        this.DialogProvider = dialogProvider;
        
        // ... rest of method ... 
    }

IDialogProvider是我将视图相关的服务间接提供给视图模型的方式。它允许视图模型调用打开和保存文件对话框,并报告错误消息。它也用于弹出对话框,在关闭已修改的文件时请求用户确认。

接下来创建窗格的视图模型。

    public MainWindowViewModel(IDialogProvider dialogProvider)
    {
        // ... save dialogProvider reference ...

        //
        // Initialize the 'Document Overview' pane view-model.
        //
        this.DocumentOverviewPaneViewModel = new DocumentOverviewPaneViewModel(this);

        //
        // Initialize the 'Open Documents' pane view-model.
        //
        this.OpenDocumentsPaneViewModel = new OpenDocumentsPaneViewModel(this);

        // ... rest of method ... 
    }

请注意,MainWindowViewModel的引用被传递到每个窗格视图模型的构造函数中。这允许它们访问各种服务,例如检索打开的文档列表和当前活动的文档。这可能是您在实际文本编辑器设计中想要打破的依赖项,也许主窗口的视图模型应该通过接口向子视图模型提供服务,但对我的目的来说,这很简单有效。

接下来,将窗格视图模型添加到Panes集合中。

    public MainWindowViewModel(IDialogProvider dialogProvider)
    {
        // ... save dialogProvider reference ...

        // ... instantiate pane view-models ...

        //
        // Add view-models for panes to the 'Panes' collection.
        //
        this.Panes = new ObservableCollection();
        this.Panes.Add(this.DocumentOverviewPaneViewModel);
        this.Panes.Add(this.OpenDocumentsPaneViewModel);

        // ... rest of method ... 
    }

由于视图模型Panes集合已数据绑定到AvalonDockHostPanes集合,因此这些视图模型随后被推送到AvalonDockHost,在那里它们被转换为适当的AvalonDock组件。

在构造函数结束时,会实例化一个示例文本文件文档并将其添加到Documents集合中。将文档视图模型添加到Documents集合会导致它随后被转换为AvalonDock组件。

    public MainWindowViewModel(IDialogProvider dialogProvider)
    {
        // ... save dialogProvider reference ...

        // ... instantiate pane view-models ...
        
        // ... add pane view-models to Panes collection ...

        //
        // Add an example/test document view-model.
        //
        this.Documents = new ObservableCollection();
        this.Documents.Add(new TextFileDocumentViewModel(string.Empty, "test data!", true));
    }

MainWindowViewModel包含实现文本编辑器功能的。例如NewFileOpenFileSaveFile等方法,这些方法最终由MainWindow.xaml中定义的命令调用。我不会解释其中大部分函数,因为它们都非常简短且易于阅读。阅读这些函数时,需要注意的关键点是,将文档添加到AvalonDock是通过将文档的视图模型添加到Documents集合来实现的。关闭文档是通过从Documents集合中删除文档的视图模型来实现的。添加和删除窗格也类似且同样容易,但是文本编辑器会话中最频繁发生的以及最有趣的动态添加和删除文档。

我现在将检查当用户单击AvalonDock文档关闭按钮后关闭文档时会发生什么。

除了单击AvalonDock文档关闭按钮外,还有几种其他关闭文档的方式。您可以从文件菜单中选择关闭全部关闭。退出应用程序也会隐式关闭所有文档。

当用户通过菜单关闭文档(或所有文档)或退出应用程序时,会调用一个视图模型方法,该方法将文档从Documents集合中删除,从而关闭它。当关闭已修改的文档时,首先会查询用户以确认是否关闭已修改的文档。只有当用户批准该操作后,文档才会被关闭。

然而,当单击AvalonDock文档关闭按钮时,情况会发生变化。在这种情况下,用户直接与AvalonDock交互,视图模型通常会被绕过。为了通知应用程序以这种方式关闭的文档,AvalonDockHost会引发DocumentClosing事件。

示例应用程序会处理此事件。

    <AvalonDockMVVM:AvalonDockHost
        ...
        DocumentClosing="avalonDockHost_DocumentClosing"
        />

事件处理程序会调用视图模型中的文档关闭逻辑。

    /// <summary>
    /// Event raised when a document is being closed by clicking the 'X' button in AvalonDock.
    /// </summary>
    private void avalonDockHost_DocumentClosing(object sender, DocumentClosingEventArgs e)
    {
        var document = (TextFileDocumentViewModel)e.Document;
        if (!this.ViewModel.QueryCanCloseFile(document))
        {
            e.Cancel = true;
        }
    }

需要重申的是,DocumentClosing事件仅在用户单击AvalonDock文档关闭按钮后关闭文档时引发。当视图模型直接从Documents集合中删除文档时,不会引发此事件。这是合乎逻辑的,因为当视图模型直接删除文档时,视图模型已经知道文档正在被关闭,因此不需要被通知。然而,当文档因用户直接与AvalonDock交互而关闭时,视图模型除了通过DocumentClosing事件外,没有其他方式知道文档正在被关闭。

当应用程序处理DocumentClosing时,文档正在关闭过程中,但尚未实际关闭。您可以从前面的代码片段中看到,可以取消关闭操作并阻止文档被关闭。DocumentClosing事件完成后,并且如果关闭操作未被取消,文档将被关闭,AvalonDockHost本身会将文档的视图模型从Documents集合中删除。

AvalonDock文档和窗格的数据模板

MainWindow.xaml的开头附近,在路由命令声明之后,是用于将面板视图模型转换为适当AvalonDock组件的数据模板。示例应用程序有三个这样的数据模板:一个用于文本文件文档,一个用于两个窗格。

每个数据模板的DataType都设置为相关的视图模型类。当视图模型添加到AvalonDockHostDocumentsPanes集合时,将以一种非常类似于WPF将视图模型与DataTemplate关联的常规机制的方式,在视觉树中搜索匹配的数据模板。当AvalonDockHost将视图模型转换为UI元素时,它期望根元素是AvalonDock组件。这就是为什么所有数据模板的根元素都是DocumentContentDockableContent。使用其他类型的UI元素作为根元素将导致异常。

作为第一个示例,您可以看到TextFileDocumentViewModel的数据模板包含一个带有嵌入式WPF TextBox的AvalonDock DocumentContent

    <!-- 
    Data template for displaying tabbed documents.
    This is really simple they are just represented by an AvalonDock
    DocumentContent that contains a simple WPF TextBox.
    -->           
    <DataTemplate
        DataType="{x:Type ViewModels:TextFileDocumentViewModel}"
        >
        <ad:DocumentContent
            Title="{Binding Title}"      
            ToolTip="{Binding ToolTip}"
            >                
            <TextBox
                Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
                />
        </ad:DocumentContent>
    </DataTemplate>

您现在可能想知道为什么数据模板的根元素要求是AvalonDock组件。毕竟,AvalonDockHost有单独的DocumentsPanes集合,因此它知道文档和窗格的区别,为什么不能自动创建一个适当的AvalonDock组件,而不是让程序员在数据模板中指定它?它之所以重要,有一个原因。它允许AvalonDock组件的属性被显式设置或数据绑定到视图模型。例如,在上面的代码片段中,TitleToolTip已数据绑定到视图模型属性。

如果您愿意,您可以进一步创建自己的AvalonDock组件的封装,用于这些数据模板。那么您将几乎完全从AvalonDock中抽象出来。然而,完全抽象不是我的目的,我个人认为这会花费更多的时间。

在上面的数据模板中,很容易看出为什么文本文件文档没有显式的视图。 '视图'只包含一个TextBox,所以创建一个单独的用户控件似乎有些小题大做。相反,我已将'视图'内联到主窗口中。TextBoxText属性已数据绑定到文档视图模型中的Text属性,并且UpdateSourceTrigger设置为PropertyChanged,因此每当用户更改文本框中的文本时,视图模型的文本都会更新。可以肯定的是,这效率不高,但它使示例代码保持简单。

窗格的数据模板也声明在MainWindow.xaml中,但是,与文档视图模型的视图不同,窗格视图模型的视图被委托给单独的用户控件。

例如,我只展示了OpenDocumentsPaneViewModel的数据模板。DocumentOverviewPaneViewModel的视图非常相似,所以我让您自己查看。这个数据模板包含一个AvalonDock DockableContent作为根元素。嵌入其中的是OpenDocumentsPaneView用户控件的一个实例。

    <!--
    The DataTemplate for the 'Open Documents' pane.
    This uses an AvalonDock DockableContent that contains an instance of the
    OpenDocumentPaneView user control.
    -->
    <DataTemplate
        DataType="{x:Type ViewModels:OpenDocumentsPaneViewModel}"
        >
        <ad:DockableContent
            x:Name="openDocumentsPane"
            Title="Open Documents"
            AvalonDockMVVM:AvalonDockHost.IsPaneVisible="{Binding IsVisible}"
            >
            <local:OpenDocumentsPaneView />
        </ad:DockableContent>
    </DataTemplate>

为每个定义的窗格命名非常重要,例如这里命名为openDocumentsPane。AvalonDock布局需要这些名称来保存和恢复。没有它们,您将无法在应用程序会话之间持久化自定义用户布局。

注意IsPaneVisible附加属性的数据绑定。在下一节中,我将讨论此数据绑定如何允许通过视图模型以编程方式控制窗格的可见性。

视图模型控制窗格可见性

不幸的是,AvalonDock没有可设置的IsVisible属性可供我们数据绑定到视图模型。为了解决这个问题,我使用了WPF附加属性机制,在AvalonDock窗格上外部创建一个新的属性并附加它,以实现所需的功能。

IsPaneVisible达到了这个目的,让我们再次看看那个数据绑定。

    AvalonDockMVVM:AvalonDockHost.IsPaneVisible="{Binding IsVisible}"

IsPaneVisible仅用于附加到AvalonDock DockableContent。它提供了一个布尔属性,可以设置为truefalse来显示或隐藏窗格。当然,它不是直接使用的,而是已数据绑定到视图模型的IsVisible属性。因此,我们可以使用视图模型属性以编程方式更改窗格的可见性。

这张图说明了在前几个XAML代码片段中显示的IsPaneVisible绑定。

实现部分,我将更详细地解释IsPaneVisible的工作原理。目前,知道可以通过视图模型操作窗格可见性就足够了。

一个很好的例子可以在主窗口的视图菜单的XAML中看到。菜单项的IsChecked已数据绑定到视图模型的IsVisible

    <MenuItem
        Header="_Open Documents"
        IsChecked="{Binding OpenDocumentsPaneViewModel.IsVisible}"
        IsCheckable="True"
        />

单击此菜单项现在可以切换打开的文档窗格的可见性,而无需额外的命令或代码隐藏。

ShowAllPanes方法是程序化设置IsVisible的一个例子。

    /// <summary>
    /// Show all panes.
    /// </summary>
    public void ShowAllPanes()
    {
        foreach (var pane in this.Panes)
        {
            pane.IsVisible = true;
        }
    }

此方法完全在视图模型中运行,遍历所有窗格并将每个窗格的IsVisible设置为trueHideAllPanes以类似的方式工作,但将属性设置为false

设置活动文档或活动窗格

让我们回顾一下我们之前看到的ActiveDocumentActivePane的数据绑定。

    <AvalonDockMVVM:AvalonDockHost
        ...
        ActiveDocument="{Binding ActiveDocument}"
        ActivePane="{Binding ActivePane}"
        ...
        />

数据绑定允许通过视图模型查询和操作AvalonDock当前选定的文档或窗格。

打开的文档窗格的代码中有一个例子。在打开的文档列表中选择一个文档会导致该文档被激活。OpenDocumentsPaneViewModelActiveDocument属性会转发到MainWindowViewModelActiveDocument

    /// <summary>
    /// View-model for the active document.
    /// </summary>
    public TextFileDocumentViewModel ActiveDocument
    {
        get
        {
            return this.MainWindowViewModel.ActiveDocument;
        }
        set
        {
            this.MainWindowViewModel.ActiveDocument = value;
        }
    }

由于MainWindowViewModelActiveDocument已数据绑定到AvalonDockHostActiveDocument,一个的更改会传播到另一个,并导致相关的AvalonDock组件被激活。因此,OpenDocumentsPaneViewModelActiveDocument的更改也会设置活动文档。

现在查看OpenDocumentsPaneView.xaml,我们看到OpenDocumentsPaneViewModelActiveDocument被数据绑定为ListBox的选定项。

    <!-- ListBox that displays the list of open documents. -->
    <ListBox
        x:Name="documentsListBox"
        Grid.Row="0"
        ItemsSource="{Binding Documents}"
        SelectedItem="{Binding ActiveDocument}"
        />

因此,每当用户更改列表中的选择时,该选择就会设置AvalonDock中的活动文档。反之亦然。当用户直接与AvalonDock交互并选择一个文档时,该更改会通过数据绑定传播到视图模型,并最终设置ListBox的选定项。

为了传播更改,OpenDocumentsPaneViewModel会处理MainWindowViewModelActiveDocumentChanged事件。作为响应,它会为自己的ActiveDocument属性引发自己的PropertyChanged事件。这会导致数据绑定更新ListBox的选定项。

    /// <summary>
    /// Event raised when the active document in the main window has changed.
    /// </summary>
    private void MainWindowViewModel_ActiveDocumentChanged(object sender, EventArgs e)
    {
        OnPropertyChanged("ActiveDocument");
    }

上述讨论也适用于设置活动窗格,尽管使用的是ActivePane属性。

AvalonDock布局

我在开发代码时遇到的一个问题是如何处理文档和窗格的默认布局。通常,在非MVVM风格中使用AvalonDock时,您会在MainWindow.xaml中直接创建默认布局,正如在AvalonDock 入门教程中所描述的那样。在应用程序运行时,用户可以根据自己的喜好重新排列布局,AvalonDock方便地提供了保存和还原布局的方法,使得在会话之间持久化用户布局变得非常容易。

然而,当将AvalonDock与MVVM一起使用时,您会发现无法直接在XAML中指定默认布局。这是因为AvalonDockHost,我的AvalonDock封装,提供了您通常会在MainWindow.xaml中硬编码的默认布局。AvalonDockHost的默认布局很简单,无法根据您的需求进行自定义。我可以向AvalonDockHost添加功能,以允许自定义其默认布局,尽管我认为这最多会是一种笨拙的解决方案。幸运的是,我找到了一个更好的解决方案,它简单易用,并且易于实现。

简而言之,您必须创建一个包含默认布局的文件,将该文件作为嵌入式资源添加到应用程序中,然后在您的应用程序首次加载时,它应该从这个嵌入式默认布局文件中恢复其布局。

要生成布局文件,您的应用程序应该已运行并设置了AvalonDock。在将面板重新排列成令人满意的默认布局后,您应该使用AvalonDock内置的SaveLayout方法保存布局文件。如果您已经设置了应用程序在退出时保存用户布局,这将很容易。例如,在示例应用程序中,当应用程序退出时会调用SaveLayout

    /// <summary>
    /// Event raised when the window is about to close.
    /// </summary>
    private void Window_Closing(object sender, CancelEventArgs e)
    {
        ...

        //
        // When the window is closing, save AvalonDock layout to a file.
        //
        avalonDockHost.DockingManager.SaveLayout(LayoutFileName);
    }

生成的布局文件现在应该作为嵌入式资源添加到应用程序中。如果文件类型设置为其他内容,您将无法检索资源,所以要小心。

这张截图显示了示例应用程序项目中的嵌入式资源默认布局文件。

这张截图显示了默认布局文件的属性。注意生成操作设置为嵌入式资源

现在是关于默认布局的最后一块拼图。默认布局应该在不存在自定义用户布局文件时应用。这显然是在应用程序第一次运行时,但这也允许用户删除他们的自定义布局文件,以便应用程序下次启动时布局恢复到默认状态。

布局,无论是嵌入式默认布局还是自定义用户布局,都只能在AvalonDock加载后才能恢复。为此,AvalonDockHost会引发AvalonDockLoaded事件,该事件由示例应用程序处理。

    <AvalonDockMVVM:AvalonDockHost
        ...
        AvalonDockLoaded="avalonDockHost_AvalonDockLoaded"
        ...
        />

事件处理程序会加载一个自定义用户布局文件,或者加载嵌入式默认布局文件。在第一种情况下,如果自定义用户布局文件已存在,我们只需将AvalonDock的RestoreLayout方法用于加载文件并恢复用户布局。

    /// <summary>
    /// Event raised when AvalonDock has loaded.
    /// </summary>
    private void avalonDockHost_AvalonDockLoaded(object sender, EventArgs e)
    {
        if (System.IO.File.Exists(LayoutFileName))
        {
            //
            // If there is already a saved layout file, restore AvalonDock layout from it.
            //
            avalonDockHost.DockingManager.RestoreLayout(LayoutFileName);
        }
        else
        {
            //
            // ... no previously saved layout exists, need to load default layout ...
            //
        }
    }

在第二种情况下,当不存在用户布局文件时,布局是通过读取嵌入式资源的流来恢复的。

    /// <summary>
    /// Event raised when AvalonDock has loaded.
    /// </summary>
    private void avalonDockHost_AvalonDockLoaded(object sender, EventArgs e)
    {
        if (System.IO.File.Exists(LayoutFileName))
        {
            //
            // ... load existing custom user-layout file ...
            //
        }
        else
        {
            //
            // Load the default AvalonDock layout from an embedded resource.
            //
            var assembly = Assembly.GetExecutingAssembly();
            using (var stream = assembly.GetManifestResourceStream(DefaultLayoutResourceName))
            {
                avalonDockHost.DockingManager.RestoreLayout(stream);
            }
        }
    }

通过指定完整的资源名称来检索嵌入式资源,在本例中是:

    /// <summary>
    /// Name of the embedded resource that contains the default AvalonDock layout.
    /// </summary>
    private static readonly string DefaultLayoutResourceName = "SampleApp.Resources.DefaultLayoutFile.xml";

现在显而易见的问题是,我是如何弄清楚使用哪个名称的?

从我的例子中,您可能可以推断出任何嵌入式资源的名称。它似乎是<命名空间> + <资源路径> + <资源文件名>的组合,或者类似的东西。但是,有一种万无一失的方法可以确定。

以下代码检索所有嵌入式资源的完整名称数组。

    string[] names = this.GetType().Assembly.GetManifestResourceNames();

您可以将此列表打印到调试输出来查找资源的名称。

关闭文件的用户确认

当用户尝试关闭一个已修改但未保存的文件时,会调用一个对话框来请求确认是否真的要关闭该文件。当用户关闭所有文件或在任何已打开文件被修改时退出应用程序时,也会发生这种情况。

此代码与AvalonDock没有特别关系,所以我不会详细介绍,但这是一个很好的机会来解释示例应用程序的各个方面。

该功能的关键是TextFileDocumentViewModelIsModified属性。您可能还记得从之前的XAML代码片段中,设置了一个数据绑定,使得用户在TextBox中的任何更改都会立即传播到视图模型的Text属性。是Text属性的setter将IsModified设置为true。这样,每当用户更改文本时,文档就会被标记为已修改。随后的为IsModified引发的PropertyChanged事件会导致一系列操作,从而更新文档的标题和工具提示以及应用程序的标题栏。

视图模型中的CloseFile方法不直接使用IsModified,而是调用辅助方法QueryCanCloseFile(我们之前也看到过)来检查它是否可以关闭文件。

    /// <summary>
    /// Close the specified document.
    /// Returns 'true' if the user allowed the file to be closed,
    /// or 'false' if the user canceled closing of the file.
    /// </summary>
    public bool CloseFile(TextFileDocumentViewModel document)
    {
        if (!QueryCanCloseFile(document))
        {
            //
            // User has chosen not to close the file.
            //
            return false;
        }

        this.Documents.Remove(document);

        //
        // File has been closed.
        //
        return true;
    }

只有当用户确认操作后,已修改的文档才会被实际关闭(即从Documents集合中删除)。

QueryCanClose会调用已修改文档的确认对话框。

    /// <summary>
    /// Determine if the file can be closed.
    /// If the file is modified, but not saved, the user is asked
    /// to confirm that the document should be closed.
    /// </summary>
    public bool QueryCanCloseFile(TextFileDocumentViewModel document)
    {
        if (document.IsModified)
        {
            //
            // Ask the user to confirm closing a modified document.
            //
            if (!this.DialogProvider.QueryCloseModifiedDocument(document))
            {
                // User doesn't want to close it.
                return false;
            }
        }

        return true;
    }

确认对话框通过IDialogProvider接口间接调用,这使得视图模型与视图很好地分离。

关闭所有文件功能只需为每个打开的文档调用CloseFile方法,因此它实际上以相同的方式运行。

退出应用程序,这会隐式关闭所有打开的文档,工作方式类似。主窗口的Closing事件会调用视图模型中的OnApplicationClosing。如果任何文档已被修改,它会调用一个不同的对话框来请求用户确认。

    /// <summary>
    /// Called when the application is closing.
    /// Return 'true' to allow application to exit.
    /// </summary>
    public bool OnApplicationClosing()
    {
        if (this.AnyDocumentIsModified)
        {
            if (!this.DialogProvider.QueryCloseApplicationWhenDocumentsModified())
            {
                //
                // User has cancelled application exit.
                //
                return false;
            }
        }

        //
        // Allow application exit to proceed.
        //
        return true;
    }

同样,确认通过IDialogProvider间接调用。

到此,示例应用程序的演练就结束了。在下一部分,我们将深入探讨AvalonDockHost的内部。

实现

本文的这一部分致力于理解AvalonDockHost类的实现。您可以在AvalonDockMVVM项目中找到AvalonDockHost

AvalonDockHost XAML

AvalonDockHost是一个用户控件。它在AvalonDockHost.xaml中的声明非常简单。

    <UserControl 
        x:Class="AvalonDockMVVM.AvalonDockHost"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:local="clr-namespace:AvalonDockMVVM"
        xmlns:ad="clr-namespace:AvalonDock;assembly=AvalonDock"
        >
        <ad:DockingManager 
            x:Name="dockingManager"
            Loaded="AvalonDock_Loaded"
            />
    </UserControl>

XAML声明仅包含DockingManager,它是AvalonDock视觉树的根元素。它被命名为dockingManager,并直接从代码隐藏中引用以添加和删除AvalonDock组件。

已处理Loaded事件,并将其作为AvalonDockLoaded事件传播到应用程序。我们已经在示例应用程序中看到该事件是如何处理的,以便可以恢复AvalonDock布局。

同步ActiveDocument和ActivePane

已处理AvalonDock的ActiveContentChanged事件,以便在活动或聚焦的面板更改时通知AvalonDockHost

该事件在构造函数中挂钩。

    public AvalonDockHost()
    {
        InitializeComponent();

        //
        // Hook the AvalonDock event that is raised when the focused content is changed.
        //
        dockingManager.ActiveContentChanged += new EventHandler(dockingManager_ActiveContentChanged);

        UpdateActiveContent();
    }

调用UpdateActiveContent可确保ActiveDocumentActivePane属性设置为正确的初始值。UpdateActiveContent也由ActiveContentChanged事件处理程序调用,以便随着时间的推移,ActiveDocumentActivePane与AvalonDock的内部状态保持同步。

UpdateActiveContent查询AvalonDock当前的活动组件,然后根据组件的类型更新ActiveDocumentActivePane

    /// <summary>
    /// Update the active pane and document from the currently active AvalonDock component.
    /// </summary>
    private void UpdateActiveContent()
    {
        var activePane = dockingManager.ActiveContent as DockableContent;
        if (activePane != null)
        {
            //
            // Set the active document so that we can bind to it.
            //
            this.ActivePane = activePane.DataContext;
        }
        else
        {
            var activeDocument = dockingManager.ActiveContent as DocumentContent;
            if (activeDocument != null)
            {
                //
                // Set the active document so that we can bind to it.
                //
                this.ActiveDocument = activeDocument.DataContext;
            }
        }
    }

应用程序可以通过编程方式或通过数据绑定来设置ActiveDocumentActivePane属性,我们在演练中看到了一个例子。在内部,文档和窗格都被简单地视为面板,因此只有一个方法可以处理两者属性已更改的事件。

    /// <summary>
    /// Event raised when the ActiveDocument or ActivePane property has changed.
    /// </summary>
    private static void ActiveDocumentOrPane_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var c = (AvalonDockHost)d;
        ManagedContent managedContent = null;
        if (e.NewValue != null &&
            c.contentMap.TryGetValue(e.NewValue, out managedContent))
        {
            managedContent.Activate();
        }
    }

contentMap是一个字典,它将面板的视图模型映射到其关联的AvalonDock组件。上面的代码片段从contentMap中检索面板的AvalonDock组件,并对其调用Activate方法。我们很快就会看到contentMap是如何初始化的。

同步文档和窗格

DocumentsPanes依赖属性是集合,它们为AvalonDockHost转换为AvalonDock组件的面板提供视图模型。再次,我们将看到文档和窗格在内部都被视为面板

当这些属性中的任何一个发生更改时(例如,当分配了新集合时),会调用一个属性更改事件处理程序,将集合的内容转换为AvalonDock组件并添加到DockingManager中。CollectionChanged事件已为这两个集合挂钩,以便AvalonDockHost在将来发生任何更改时(例如添加或删除文档和窗格)得到通知。

在处理新分配的集合之前,事件处理程序会处理(如果存在)先前分配的集合。

    /// <summary>
    /// Event raised when the 'Documents' or 'Panes' properties have changed.
    /// </summary>
    private static void DocumentsOrPanes_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var c = (AvalonDockHost)d;

        //
        // Deal with the previous value of the property.
        //
        if (e.OldValue != null)
        {
            //
            // Remove the old panels from AvalonDock.
            //
            var oldPanels = (IList)e.OldValue;
            c.RemovePanels(oldPanels);

            var observableCollection = oldPanels as INotifyCollectionChanged;
            if (observableCollection != null)
            {
                //
                // Unhook the CollectionChanged event, we no longer need to receive notifications
                // of modifications to the collection.
                //
                observableCollection.CollectionChanged -= new NotifyCollectionChangedEventHandler(c.documentsOrPanes_CollectionChanged);
            }
        }

        //
        // Deal with the new value of the property.
        //
        if (e.NewValue != null)
        {
            //
            // Add the new panels to AvalonDock.
            //
            var newPanels = (IList)e.NewValue;
            c.AddPanels(newPanels);

            var observableCollection = newPanels as INotifyCollectionChanged;
            if (observableCollection != null)
            {
                //
                // Hook the CollectionChanged event to receive notifications
                // of future modifications to the collection.
                //
                observableCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(c.documentsOrPanes_CollectionChanged);
            }
        }
    }

CollectionChanged事件使AvalonDockHost意识到已添加或删除了面板。响应此事件,会实例化AvalonDock组件并将其添加到AvalonDock,或者将其从AvalonDock中移除。

    /// <summary>
    /// Event raised when the 'Documents' or 'Panes' collection have had items added/removed.
    /// </summary>
    private void documentsOrPanes_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Reset)
        {
            //
            // The collection has been cleared, need to remove all
            // documents or panes from AvalonDock depending on which collection was
            // actually cleared.
            //
            ResetDocumentsOrPanes(sender);
        }
        else
        {
            if (e.OldItems != null)
            {
                //
                // Remove the old panels from AvalonDock.
                //
                RemovePanels(e.OldItems);
            }

            if (e.NewItems != null)
            {
                //
                // Add the new panels to AvalonDock.
                //
                AddPanels(e.NewItems);
            }
        }
    }

ResetDocumentsOrPanes处理Reset操作,并被委托给一个单独的方法,因为它有点复杂。我不得不承认这有点 hack。大多数情况下,我能够使用相同的事件处理程序和方法来处理文档和窗格,或者我统称为面板。在这种情况下,我不能,我将这归咎于我所认为的CollectionChangedReset操作的不足。问题在于Reset操作不提供已删除项目的列表。相反,您需要维护一个单独的内部项目列表,以便您已经知道集合中以前的项目。这是一个令人烦恼的问题,在过去(例如我以前的文章)我通过创建我自己的ObservableCollection版本来解决这个问题,它的工作方式更友好。然而,在这篇文章中,我的目标是最小化依赖项,所以我没有引入那个类。

考虑到文档和窗格在内部被视为面板,并且ResetDocumentsOrPanes应该删除所有文档或所有窗格,因此它必须弄清楚要删除哪种类型的对象。为了实现这一点,它会将事件发送者与DocumentsPanes属性进行比较,以确定是哪个集合引发了CollectionChanged事件。在此测试之后,它就知道应该删除哪种类型的面板,并通过检查AvalonDockHost拥有的唯一内部集合contentMap字典(稍后我们将看到)来继续删除它们。这并非我写过的最好的代码,但它能完成工作,并且是解决一个令人讨厌问题的便捷解决方案。如果您愿意,请自行检查该方法(如果您认为有更少的hacky但同样便捷的解决方案,请给我留言)。

继续,AddPanels方法为每个新面板调用AddPanel。然后AddPanel就是有趣的地方:在这里,面板视图模型被转换为AvalonDock组件。正如我们在演练中所见,AvalonDock组件将由数据模板指定。必须在视觉树中搜索以查找数据模板资源。如果找到与视图模型类型匹配的数据模板,则将其实例化,并将生成的UI元素(预计是AvalonDock组件)添加到AvalonDock。

在视觉树中搜索数据模板是WPF已经在内部执行的操作,但不幸的是,它似乎没有提供对该功能的编程访问。所以我创建了我自己的方法DataTemplateUtils.InstanceTemplate,它搜索并实例化数据模板。我不会介绍该方法的内部工作原理,但请随时自行查看代码。

因此,考虑到所有这些,AddPanel的首要任务是查找并实例化AvalonDock组件。

    /// <summary>
    /// Add a panel to Avalondock.
    /// </summary>
    private void AddPanel(object panel)
    {
        //
        // Instantiate a UI element based on the panel's view-model type.
        // The visual-tree is searched to find a DataTemplate keyed to the requested type.
        //
        var panelViewModelType = panel.GetType();
        var uiElement = DataTemplateUtils.InstanceTemplate(panelViewModelType, this, panel);
        if (uiElement == null)
        {
            throw new ApplicationException("Failed to find data-template for type: " + panel.GetType().Name);
        }
        
        // ... rest of method ...
    }

接下来必须检查实例化的UI元素,以确保它实际上是一个AvalonDock组件。为此,它被类型转换为AvalonDock ManagedContent,这是AvalonDock文档和窗格的基类。如果实例化的UI元素不是ManagedContent,那么它不是AvalonDock组件,就会引发异常。在验证AvalonDock组件后,将其添加到contentMap字典中,以便以后可以轻松检索。

    private void AddPanel(object panel)
    {
        // ... instantiate uiElement from data type of view-model ...

        //
        // Cast the instantiated UI element to an AvalonDock ManagedContent.
        // ManagedContent can refer to either an AvalonDock DocumentContent
        // or an AvalonDock DockableContent.
        //
        var managedContent = uiElement as ManagedContent;
        if (managedContent == null)
        {
            throw new ApplicationException("Found data-template for type: " + panel.GetType().Name + ", but the UI element generated is not a ManagedContent (base-class of DocumentContent/DockableContent), rather it is a " + uiElement.GetType().Name);
        }

        //
        // Associate the panel's view-model with the Avalondock ManagedContent so it can be retrieved later.
        //
        contentMap[panel] = managedContent;

        // ... rest of method ...
    }

接下来,挂钩各种事件以跟踪AvalonDock组件的持续状态。

    private void AddPanel(object panel)
    {
        // ... instantiate uiElement from data type of view-model ...
        
        // ... type-cast to ManagedContent and add entry to contentMap ...

        //
        // Hook the event to track when the document has been closed.
        //
        managedContent.Closed += new EventHandler(managedContent_Closed);

        var documentContent = managedContent as DocumentContent;
        if (documentContent != null)
        {
            //
            // For documents only, hook Closing so that the application can be informed
            // when a document is in the process of being closed by the use clicking the
            // AvalonDock close button.
            //
            documentContent.Closing += new EventHandler(documentContent_Closing);
        }
        else
        {
            var dockableContent = managedContent as DockableContent;
            if (dockableContent != null)
            {
                //
                // For panes only, hook StateChanged so we know when a DockableContent is shown/hidden.
                //
                dockableContent.StateChanged += new RoutedEventHandler(dockableContent_StateChanged);
            }
            else
            {
                throw new ApplicationException("Panel " + managedContent.GetType().Name + " is expected to be either DocumentContent or DockableContent."); 
            }
        }
        
        // ... rest of method ...
    }

为每个AvalonDock组件处理Closed事件,并确保任何关闭的面板也被内部从AvalonDockHost中移除。

根据面板的类型,还会挂钩另一个事件。对于文档,它是Closing事件,处理它允许AvalonDockHost在用户单击AvalonDock文档关闭按钮后关闭文档时收到通知。在这种情况下,AvalonDockHost会引发DocumentClosing事件,以便应用程序知道文档正在关闭。

对于窗格,会挂钩StateChanged事件。AvalonDockHost处理此事件,以便在窗格可见性发生更改时得到通知。作为响应,它将IsPaneVisible附加属性设置为truefalse以指示窗格的可见性。

继续,AddPanel的最后任务是显示和激活AvalonDock组件。

    private void AddPanel(object panel)
    {
        // ... instantiate uiElement from data type of view-model ...
        
        // ... type-cast to ManagedContent and add entry to contentMap ...
        
        // ... hook events on the ManagedContent ...

        managedContent.Show(dockingManager);
        managedContent.Activate();
    }

现在让我们看看RemovePanel。当文档或窗格从DocumentsPanes集合中删除时,就会调用此方法。从contentMap中检索AvalonDock组件并关闭它。

    /// <summary>
    /// Remove a panel from Avalondock.
    /// </summary>
    private void RemovePanel(object panel)
    {
        //
        // Look up the document in the content map.
        //
        ManagedContent managedContent = null;
        if (contentMap.TryGetValue(panel, out managedContent))
        {
            disableClosingEvent = true;

            try
            {
                //
                // The content was still in the map, and therefore still open, so close it.
                //
                managedContent.Close();
            }
            finally
            {
                disableClosingEvent = false;
            }
        }
    }

当文档被关闭时,会引发Closing事件,并且在RemovePanel中设置为truedisableClosingEvent变量在此处发挥作用,以防止DocumentClosingEvent传播到应用程序。

    /// <summary>
    /// Event raised when an AvalonDock DocumentContent is being closed.
    /// </summary>
    private void documentContent_Closing(object sender, CancelEventArgs e)
    {
        var documentContent = (DocumentContent)sender;
        var document = documentContent.DataContext;

        if (!disableClosingEvent)
        {
            if (this.DocumentClosing != null)
            {
                //
                // Notify the application that the document is being closed.
                //
                var eventArgs = new DocumentClosingEventArgs(document);
                this.DocumentClosing(this, eventArgs);

                if (eventArgs.Cancel)
                {
                    //
                    // Closing of the document is to be cancelled.
                    //
                    e.Cancel = true;
                    return;
                }
            }
        }

        documentContent.Closing -= new EventHandler(documentContent_Closing);
    }

由于RemovePanel仅在应用程序本身已将面板从DocumentsPanes集合中删除时调用,因此不需要引发DocumentClosing事件,因为应用程序已经知道文档正在被关闭,因此不需要该事件。当关闭的是文档(而不是窗格)时,Closing事件由AvalonDockHost处理。由于是应用程序删除了文档,因此它已经知道文档已被关闭,无需引发DocumentClosing事件,因此使用了disableClosingEvent来阻止引发DocumentClosing事件。

当用户单击AvalonDock文档关闭按钮时,是AvalonDock本身(而不是应用程序或AvalonDockHost)调用ManagedContentClose方法。这也会导致Closing事件被引发,在这种情况下,disableClosingEvent将被设置为其默认值,即false。这意味着在这种情况下,会引发DocumentClosing来通知应用程序。这就是我们想要的,因为应用程序没有其他方式知道文档即将关闭,并且应用程序(以及因此用户)有机会否决关闭文档很重要。

最终,在面板关闭后,会引发其Closed事件。响应此事件,AvalonDockHost会删除面板的contentMap条目,取消挂钩事件并执行其他清理任务。

    /// <summary>
    /// Event raised when an Avalondock ManagedContent has been closed.
    /// </summary>
    private void managedContent_Closed(object sender, EventArgs e)
    {
        var managedContent = (ManagedContent)sender;
        var content = managedContent.DataContext;

        //
        // Remove the content from the content map right now.
        // There is no need to keep it around any longer.
        //
        contentMap.Remove(content);

        managedContent.Closed -= new EventHandler(managedContent_Closed);

        var documentContent = managedContent as DocumentContent;
        if (documentContent != null)
        {
            this.Documents.Remove(content);

            if (this.ActiveDocument == content)
            {
                //
                // Active document has closed, clear it.
                //
                this.ActiveDocument = null;
            }
        }
        else
        {
            var dockableContent = managedContent as DockableContent;
            if (dockableContent != null)
            {
                //
                // For panes only, unhook StateChanged event.
                //
                dockableContent.StateChanged -= new RoutedEventHandler(dockableContent_StateChanged);

                this.Panes.Remove(content);

                if (this.ActivePane == content)
                {
                    //
                    // Active pane has closed, clear it.
                    //
                    this.ActivePane = null;
                }
            }
        }
    }

Closed事件处理程序还确保文档或窗格已从DocumentsPanes集合中删除。当文档被AvalonDock文档关闭按钮关闭时,这一点很重要,因为这是文档实际从视图模型中删除的唯一方式。如果关闭的是活动文档或活动窗格,则将ActiveDocumentActivePane重置为null

IsPaneVisible 附加属性

在本节中,我将讨论IsPaneVisible附加属性的实现。此属性附加到AvalonDock窗格,以允许其可见性由布尔变量控制。主要目的是将该属性数据绑定到视图模型属性,从而允许窗格可见性完全在视图模型中控制。

我们已经在前面看到AvalonDockHost是如何挂钩StateChanged的。当窗格的可见性发生变化时(例如,当用户单击AvalonDock窗格隐藏按钮后关闭窗格时),会引发此事件。

事件处理程序根据窗格的当前可见性将附加属性的值设置为truefalse

    /// <summary>
    /// Event raised when the 'dockable state' of a DockableContent has changed.
    /// </summary>
    private void dockableContent_StateChanged(object sender, RoutedEventArgs e)
    {
        var dockableContent = (DockableContent)sender;
        SetIsPaneVisible(dockableContent, dockableContent.State != DockableContentState.Hidden);
    }

在演练中,我们查看了MainWindow.xaml中示例应用程序窗格的数据模板。让我们回顾一下打开的文档窗格的数据模板以刷新我们的记忆。

    <DataTemplate
        DataType="{x:Type ViewModels:OpenDocumentsPaneViewModel}"
        >
        <ad:DockableContent
            x:Name="openDocumentsPane"
            Title="Open Documents"
            AvalonDockMVVM:AvalonDockHost.IsPaneVisible="{Binding IsVisible}"
            >
            <local:OpenDocumentsPaneView />
        </ad:DockableContent>
    </DataTemplate>

数据绑定将IsPaneVisible引用为AvalonDockMVVM:AvalonDockHost.IsPaneVisible。这是在数据绑定中引用附加属性的标准语法。在此示例中,IsPaneVisible已数据绑定到OpenDocumentPaneViewModelIsVisible属性。通过此数据绑定,并且如我们在演练中所见,可以通过视图模型控制窗格的可见性。

IsPaneVisibleAvalonDockHost注册为附加属性。

    public static readonly DependencyProperty IsPaneVisibleProperty =
        DependencyProperty.RegisterAttached("IsPaneVisible", typeof(bool), typeof(AvalonDockHost),
            new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, IsPaneVisible_PropertyChanged));

属性更改事件处理程序是附加属性的主要工作。

    /// <summary>
    /// Event raised when the IsPaneVisible property changes.
    /// </summary>
    private static void IsPaneVisible_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var avalonDockContent = o as ManagedContent;
        if (avalonDockContent != null)
        {
            bool isVisible = (bool)e.NewValue;
            if (isVisible)
            {
                avalonDockContent.Show();
            }
            else
            {
                avalonDockContent.Hide();
            }
        }
    }

IsPaneVisible属性更改时,将调用事件处理程序,并根据IsPaneVisible的新值显示或隐藏窗格。

到此,《AvalonDockHost》实现部分结束。接下来是结论,然后是AvalonDockHost参考部分。

结论

本文讨论了AvalonDockHost的使用和实现。AvalonDockHost是我创建的AvalonDock封装,用于将AvalonDock适配到MVVM应用程序。

感谢您花时间阅读本文。

参考

本节是AvalonDockHost属性的参考。

Panes

获取并设置窗格视图模型对象的集合。

向此集合添加对象会导致一个窗格被添加到AvalonDock。

应为每种类型的窗格定义一个DataTemplate,并且DataTemplate的根应为AvalonDock DockableControl。

注意:此属性最初为null,您应该为其分配一个集合,或者,正如其预期用途,将其数据绑定到视图模型中的集合。

Documents

获取并设置文档视图模型对象的集合。

向此集合添加对象会导致一个文档被添加到AvalonDock。

应为每种类型的文档定义一个DataTemplate,并且DataTemplate的根应为AvalonDock DocumentContent。

注意:此属性最初为null,您应该为其分配一个集合,或者,正如其预期用途,将其数据绑定到视图模型中的集合。

ActivePane

获取并设置当前活动窗格的视图模型对象。

以编程方式设置此属性会更改活动的焦点AvalonDock面板。

它也可以数据绑定到视图模型属性,以便可以通过视图模型设置活动窗格。当用户直接选择AvalonDock窗格时,此属性会自动更新。

ActiveDocument

获取并设置当前活动文档的视图模型对象。

以编程方式设置此属性会更改活动的焦点AvalonDock面板。

它也可以数据绑定到视图模型属性,以便可以通过视图模型设置活动文档。

当用户直接选择AvalonDock文档时,此属性会自动更新。

IsPaneVisible

获取或设置窗格的可见性状态。

此附加属性仅用于附加到AvalonDock DockableContent,该内容是窗格视图模型的数据模板的根元素。

将此属性设置为true会显示窗格,设置为false会隐藏窗格。当用户通过单击AvalonDock窗格隐藏按钮隐藏AvalonDock窗格时,此属性会自动更新。

DockingManager

获取AvalonDock DockingManager。

直接访问DockingManager允许应用程序保存和恢复AvalonDock布局。

AvalonDockLoaded

当AvalonDock加载后,将引发此事件。

应用程序可以通过恢复AvalonDock布局来响应此事件。

DocumentClosing

当用户单击AvalonDock文档关闭按钮后正在关闭文档时,将引发此事件。它允许应用程序在必要时取消文档关闭操作。

注意:当文档被应用程序自身从Documents集合中删除而关闭时,不会引发此事件。在这种情况下,应用程序已经知道文档正在被关闭,因此不需要该事件。

SetIsPaneVisible

设置IsPaneVisible附加属性的值。

虽然不应该需要直接使用它,因为IsPaneVisible旨在数据绑定到视图模型属性,并且应该设置视图模型而不是调用此方法。

GetIsPaneVisible

获取IsPaneVisible的值。

由于上述原因,不应直接使用此方法。

更新历史

  • 2011/11/08:文章首次发布。
© . All rights reserved.