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

AvalonDock [2.0] 教程第五部分 - 加载/保存布局,带解引用 DockingManager

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (16投票s)

2014 年 1 月 31 日

CPOL

10分钟阅读

viewsIcon

147563

downloadIcon

9473

无需 DockingManager 引用即可保存/加载 AvalonDock 布局。

简介

本文分为两个主要部分。第一部分演示了如何在应用程序启动和关闭时加载/保存 AvalonDock 布局。第二部分在前一部分的基础上,实现了两个额外的命令,用于在程序运行时持久化和重新加载布局。每个部分都有其对应的下载文件。

您也可以在我同时开始维护的存储库中找到类似的示例应用程序(如果您愿意贡献的话):https://github.com/Dirkster99/AvalonDock

在启动/关闭时加载/保存 AvalonDock 布局

请下载并查看 Version_05_Edi.zip 文件中的代码,以跟随本节的讨论。

我最近帮助一位朋友开始了他的 AvalonDock 开发,我意识到许多人已经开始重视 MVVM 模式,但并不是很多人真正了解它为什么重要。也就是说,似乎每个人都知道 MVVM 以及它如何解耦层,但很少有人知道解耦在 WPF 中为什么是必需的。 

我们知道关注点分离以及类似之类的架构术语,但如果我只是不关心,像使用 WinForms 一样使用 WPF 呢?只需在 ViewModel 中存储一个引用,并在应用程序逻辑认为合适时调用适当的方法或设置属性: 

public DockingManager ADManager{ get; set; }

// Implement some processing logic like saving and loading layouts
// through a reference of the view element in the viewmodel
public void UpdateLayout()
{
  this.ADManager.UpdateLayout();
  this.ADManager. ...
}

将 WinForms 编程风格与 MVVM 混合(如上草图所示)是不可取的。这种编程风格可能导致应用程序不稳定、不确定且难以调试。这种不稳定性通常会发生,因为 WPF 应用程序实现多于一个线程 [2],并且如果避免使用依赖项属性、通过绑定进行命令、(路由)事件、路由命令以及所有其他使 WPF 应用程序区别于其他计算机科学世界的细微之处,那么在这些线程之间进行通信可能会很麻烦。 

本文展示了如何在 ViewModel 或其下方的任何位置不引用 DockingManager 实例的情况下使用 AvalonDock。这对于初学者来说是一项宝贵的练习,因为删除 ViewModel 中视图的硬引用可以确保导致应用程序不稳定的线程问题成为过去。 

本文本节中使用的示例处理任务是在应用程序启动/关闭时加载和保存文档布局。这个有价值的示例练习将在下面的下一个主要部分中进一步扩展。

准备 

本文基于本教程第 4 步中发布的上一篇文章的解决方案 [1]。您可以下载前一篇文章的解决方案,并按照文章的步骤添加每段代码,或者下载本文提供的完整解决方案来验证所需的代码更改。

您应该了解附加行为 [3] 才能理解本文。只需查看我在另一篇文章中记录的示例,并在掌握它之后再回来。我在这里不会深入研究附加行为的细节,因为我宁愿专注于它与 AvalonDock 的应用。

Using the Code

我的解决方案的核心是 MainWindow.xaml 代码中 DockingManager 实例上的附加行为 [3]。每当 DockingManager 实例触发 LoadUnload 事件时,此行为就会做出响应。 

因此,我们可以在其关联的命名空间和文件夹中添加附加行为,例如 Edi.View.Behavior(请参阅示例代码),调用新的附加行为类 AvalonDockLayoutSerializer,并将示例解决方案中的代码粘贴到文件中。

 

接下来,我们可以调整 MainWindow.xaml 以利用新的附加行为类。添加对上述命名空间的引用

xmlns:AVBehav="clr-namespace:Edi.View.Behavior"

...并像这样在 XAML 代码中使用该引用

<avalonDock:DockingManager
  AnchorablesSource="{Binding Tools}" 
  DocumentsSource="{Binding Files}"
  ActiveContent="{Binding ActiveDocument, Mode=TwoWay, Converter={StaticResource ActiveDocumentConverter}}"
  
  AVBehav:AvalonDockLayoutSerializer.LoadLayoutCommand="{Binding ADLayout.LoadLayoutCommand}"
  AVBehav:AvalonDockLayoutSerializer.SaveLayoutCommand="{Binding ADLayout.SaveLayoutCommand}"
  
  Grid.Row="2">

请注意,我们在上面的代码中不再使用 x:Name="dockManager" 属性。此代码几乎在所有 AvalonDock(示例)代码中都使用,如果您是 WPF 新手,它可能会导致痛苦的时光。因此,删除此 x:Name 属性可能需要您删除相应的代码引用才能使其正常工作。 

上面的代码到底是如何工作的?AvalonDockLayoutSerializer 中的附加行为可以绑定并执行一个命令来加载和保存布局文件。当 DockingManager 触发 Load 事件时,将执行 LoadLayoutCommand 命令。Load 事件是标准的 WPF 事件,表示视图的实例化。Unload 事件也是标准的 WPF 事件,表示视图即将被销毁。此事件将导致执行 SaveLayoutCommand

LoadLayoutCommandSaveLayoutCommand 都绑定到 Workspace 类中的一个名为 ADLayout(类型为 AvalonDockLayoutViewModel)的 ViewModel 属性。AvalonDockLayoutViewModel 类公开了用于保存/加载布局命令的属性。

因此,要完成此解决方案,我们需要在 Edi.ViewModel 命名空间中添加 AvalonDockLayoutViewModel 类的代码。并在 Workspace 类中添加属性

private AvalonDockLayoutViewModel mAVLayout = null;

/// <summary>
/// Expose command to load/save AvalonDock layout on application startup and shut-down.
/// </summary>
public AvalonDockLayoutViewModel ADLayout
{
  get
  {
    if (this.mAVLayout == null)
      this.mAVLayout = new AvalonDockLayoutViewModel();

    return this.mAVLayout;
  }
}

还有一些实用属性,如 Workspace.LayoutFileNameDirAppDataCompany,但它们只是简单的字符串属性,仅用于避免硬编码和冗余的字符串定义。您可以通过浏览示例解决方案来找到这些属性。

我知道我可以使用 Blend Interactivity DLL 来替换本文中的附加行为,但我希望使这个练习尽可能简单和有价值。所以,如果您认为您知道本文的主题,请自行研究 Blend Interactivity。

如果您想在更复杂的场景中看到该行为,也可以下载我的编辑器(https://edi.codeplex.com/)。 

通过命令加载/保存布局 

总结前面的部分:我们使用附加行为来监听事件,将其转换为命令,并通过命令绑定执行。一旦附加行为模式被充分理解 [3] 并正确应用,这些场景就很容易解决。

  1. 视图.事件
  2. 附加行为
  3. 执行绑定到 ViewModel 的命令
  4. 执行相应的 ViewModel 方法

还有一些用例需要应用程序生命周期内的 AvalonDock 布局保存和加载。这些用例可能需要更复杂的实现,其中: 

  1. ViewModel 发起功能
  2. 视图实现相应的处理并返回结果
  3. ViewModel 完成处理(例如,将结果存储在数据库中)。

我将上述场景命名为“往返”,因为它涉及到 ViewModel 启动某些处理,需要视图完成它,然后 ViewModel 实现处理的最后步骤。因此,正确实现它更复杂。在我开始写这篇文章时,我并没有一个很好的解决方案构想,但 CodeProject 的一位朋友(请参阅下方的论坛)出现了,并提请我注意事件聚合器 [4] 模式。事实证明,这正是解决此类问题的良好方案,正如您在 **Version_05_LoadSaveCommand.zip** 下载文件中看到的那样。  

此解决方案(请参阅 Version_05_LoadSaveCommand.zip)基于 MVVM Light,并且稍微复杂一些。您会看到我删除了 AvalonDock 和 AvalonEdit 项目,而是使用了 NuGet 引用。由于我们使用此框架来实现事件聚合器模式,因此也有一个 MVVM Light 的 NuGet 引用。 

代码在类级别上与本文第一部分讨论的初始实现非常相似。但用户界面中有两个新按钮,我将在下面讨论。

“**保存布局**”按钮绑定到 ApplicationViewModel 类中的 ADLayout.SaveWorkspaceLayoutToStringCommand 属性。与之前的解决方案一样,ADLayout.SaveWorkspaceLayoutToStringCommand 属性在 AvalonDockLayoutViewModel 类中实现。此命令绑定到 SaveWorkspaceLayout_Executed() 方法,并使用以下发布者代码

Messenger.Default.Send(new NotificationMessageAction<string>(
                       Notifications.GetWorkspaceLayout,
                       (result) =>
                       {
                         this.current_layout = result;
                       }));

这行代码创建一个新的 NotificationMessageAction<string> 对象,并将其发送给订阅了此类消息的任何方。此消息的结果是一个字符串,当结果返回时,该字符串将被设置为成员字符串 current_layout。但我在这里跳得太快了。在我们查看订阅者部分之前,这似乎没有多大意义——所以让我们看看 MainWindow.xaml.cs 来理解双方的故事。

MainWindow.xaml.cs 中的 MainWindow 类的构造函数有这行代码

Messenger.Default.Register<NotificationMessageAction<string>>
  (this, notication_message_action_recieved);

这是对 NotificationMessageAction<string> 类的通知的订阅,并告诉 MVVM Light,每当发布者发送此类通知时,就执行 MainWindow.notication_message_action_recieved 方法。然后,该方法用于保存来自 DockingManager 的布局,并将其作为字符串参数在最终执行语句中返回

string xmlLayoutString = string.Empty;

using (StringWriter fs = new StringWriter())
{
  XmlLayoutSerializer xmlLayout = new XmlLayoutSerializer(this.dockManager);
  xmlLayout.Serialize(fs);
  xmlLayoutString = fs.ToString();
}

message.Execute(xmlLayoutString);

...现在我们已经看到了双方的故事,因此我们可以理解为什么 current_layoutDockingManager 类的 Xml 布局字符串设置。 

您可能会注意到,我们在 MainWindow.xaml 中重新引入了 dockManager 引用(我们在上述方法中将其删除了)。对于符合 MVVM 的设计来说,这也没问题,因为该字段是私有的(通过 x:FieldModifier private),并且它永远不会被传递到 MainWindow 类之外的任何地方。

<avalonDock:DockingManager AnchorablesSource="{Binding Tools}"
 x:Name="dockManager" x:FieldModifier="private"
 DocumentsSource="{Binding Files}"
 ActiveContent="{Binding ActiveDocument, Mode=TwoWay, Converter={StaticResource ActiveDocumentConverter}}"
 IsEnabled="{Binding IsBusy, Converter={StaticResource BooleanNotConverter}, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
 AVBehav:AvalonDockLayoutSerializer.LoadLayoutCommand="{Binding ADLayout.LoadLayoutCommand}"
 AVBehav:AvalonDockLayoutSerializer.SaveLayoutCommand="{Binding ADLayout.SaveLayoutCommand}"

 Grid.Row="2">

如果符合其私有且不传递给视图外部其他类的要求,则上述引用是可以的。但是,这很困难,并且可能会被忽略(就像我在早期版本的示例代码中所做的那样)。因此,最佳的 MVVM 实现仍然是不需要 x:Name 属性的视图。但是,当我们不得不面对现实以及时间、精力等方面的限制时,谨慎实现它是完全可以的。

下面的顺序图为我们提供了鸟瞰图,并让我们看到了这个故事的讽刺之处。讽刺的是,用户在 MainWindow 中启动了一个功能,该功能向自身发送一条消息来保存 MainWindow 中包含的 DockingManager 的布局(!)

 

当然,这种情况可以更轻松地实现。但是,想象一下 DockingManager 可能包含在一个 UserControl 中,该 UserControl 放置在 MainWindow 或其他窗口中。或者考虑将“保存布局”按钮放置在 GUI 的完全不同的部分——一个额外的对话框或一个单独的窗口。上述解决方案将适用于所有这些情况以及更多情况,这在您探索高级 WPF 功能时非常重要。

“**加载布局**”按钮在 AvalonDockLayoutViewModel 类中的 current_layout 字符串设置为某个值后即可启用。您可以更改显示工具窗口的排列方式,然后单击“**加载布局**”以确认它确实加载了先前保存的布局。该方法的功能也通过上面讨论的事件聚合器实现。您现在应该能够理解并验证其功能。如果您仍然看到未解决的问题,请随时提问。

结论

本文提供了 3 种解决方案,应该可以帮助您在自己的应用程序中保存和加载 DockingManager 类的布局。第二部分基于 **MVVM Light**,如果没有 CodeProject 的某位用户(请参阅下方的论坛)贡献他的想法,就不会有这篇文章。非常感谢。

其他框架,如 **Caliburn.Micro** 或 **PRISM**,也支持事件聚合模式。我通过从 MVVM Light 解决方案重新工程化来开发了 **PRISM** 解决方案。如果您愿意使用 Caliburn.Micro 实现类似的示例,请随时将您的示例代码发送给我。否则,我将继续教程的下一步,期待与您联系。 

无论如何,我一如既往地期待您的反馈。

参考文献 

© . All rights reserved.