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






4.96/5 (16投票s)
无需 DockingManager 引用即可保存/加载 AvalonDock 布局。
- 下载 Version_05_Edi.zip - 1.3 MB
- 下载 Version_05_LoadSaveCommand.zip
- 下载 Version_05_LoadSaveCommand_PRISM.zip
简介
本文分为两个主要部分。第一部分演示了如何在应用程序启动和关闭时加载/保存 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
实例触发 Load
或 Unload
事件时,此行为就会做出响应。
因此,我们可以在其关联的命名空间和文件夹中添加附加行为,例如 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
。
LoadLayoutCommand
和 SaveLayoutCommand
都绑定到 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.LayoutFileName
、DirAppData
和 Company
,但它们只是简单的字符串属性,仅用于避免硬编码和冗余的字符串定义。您可以通过浏览示例解决方案来找到这些属性。
我知道我可以使用 Blend Interactivity DLL 来替换本文中的附加行为,但我希望使这个练习尽可能简单和有价值。所以,如果您认为您知道本文的主题,请自行研究 Blend Interactivity。
如果您想在更复杂的场景中看到该行为,也可以下载我的编辑器(https://edi.codeplex.com/)。
通过命令加载/保存布局
总结前面的部分:我们使用附加行为来监听事件,将其转换为命令,并通过命令绑定执行。一旦附加行为模式被充分理解 [3] 并正确应用,这些场景就很容易解决。
- 视图.事件
- 附加行为
- 执行绑定到 ViewModel 的命令
- 执行相应的 ViewModel 方法
还有一些用例需要应用程序生命周期内的 AvalonDock 布局保存和加载。这些用例可能需要更复杂的实现,其中:
- ViewModel 发起功能
- 视图实现相应的处理并返回结果
- 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_layout
用 DockingManager
类的 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 实现类似的示例,请随时将您的示例代码发送给我。否则,我将继续教程的下一步,期待与您联系。
无论如何,我一如既往地期待您的反馈。
参考文献
- [1] 本教程的其他部分
- AvalonDock [2.0] 教程第一部分 - 添加工具窗口
https://codeproject.org.cn/Articles/483507/AvalonDock-2-0-Tutorial-Part-1-Adding-a-Tool-Windo
AvalonDock [2.0] 教程第二部分 - 添加开始页
https://codeproject.org.cn/Articles/483533/AvalonDock-2-0-Tutorial-Part-2-Adding-a-Start-Page
- AvalonDock [2.0] 教程第三部分 - AvalonDock 中的 AvalonEdit
https://codeproject.org.cn/Articles/570313/AvalonDock-2-0-Tutorial-Part-3-AvalonEdit-in-Avalo
AvalonDock [2.0] 教程第四部分 - 集成 AvalonEdit 选项
https://codeproject.org.cn/Articles/570324/AvalonDock-2-0-Tutorial-Part-4-Integrating-AvalonE
- AvalonDock [2.0] 教程第一部分 - 添加工具窗口
- [2] 线程模型
http://msdn.microsoft.com/library/ms741870%28v=vs.110%29.aspx
[3] 附加行为中的模式
https://codeproject.org.cn/Articles/422537/Patterns-in-Attached-Behaviours
- [4] 什么、为什么和如何:事件聚合器
http://developingzack.blogspot.de/2012/09/what-why-and-how-event-aggregator.html