AvalonDock 2.0+Caliburn.Micro+MahApps.Metro实现Metro风格插件式系统(一)
随着IOS7由之前UI的拟物化设计变为如今的扁平化设计,也许扁平化的时代要来了,当然我们是不是该吐槽一下,苹果什么时候也开始跟风了,自GOOGLE和微软界面扁平化过后,苹果也加入了这一队伍。
AvalonDock
AvalonDock 是一个.NET库,用于在停靠模式布局(docking)中排列一系列WPF/WinForm控件。最新发布的版本原生支持MVVM框架、Aero Snap特效并具有更好的性能。
AvalonDock 2.0版本已经发布了,新版本是用MVVM框架重新编写,似乎也用了Command(命令)模式。2.0版的文档尚未发布,但你可以参考Avalon.TestApp 或者2.0版源码中的Avalon.MVVMTestApp文件夹来查看新的API。
这个库使用很简单——只需要用AvalonDock提供的控件包含你自己的控件,页面布局就立即变成可停靠的(dockable)。可以参考 入门 页面获取样例代码,了解不同控件的特性。当然你也可以在自己的C#代码中实例化或操作这些控件。2.0版本中,控件功能与以前一致,但控件名称已经改变,因此建议参考前述样例代码直至参考文档更新为止。
大名鼎鼎SharpEevelop也应用了AvalonDock,由于SharpEevelop的框架过于庞大,并且SharpEevelop里的AvalonDock 1.3的版本,并不支持MVVM的模式,所以就兴起了自己做一个插件式系统,当然也跟一下扁平化的风,目前框架已经做好并应用到个人项目中,本着开源的思想我会把框架搭建的过程,以及遇到的种种问题分享出来。
Caliburn.Micro
Caliburn是Rob Eisenberg在2009年提出的一个开源框架,可以应用于WPF,Silverlight,WP7等,框架基于MVVM模式,像它的名字一样,是企业级应用的一把利器。
基于WPF的框架有很多,Prism,WAF等,每个框架都有自己侧重点,像Prism侧重于模块间的组合,WAF侧重于分层设计。通观CM的设计,它的一些想法如下: 1.ActionMessage,结合了Blend中的TriggerAction,可以把UI控件中的事件绑定到后台方法,类似于CallMethodAction。CM对ActionMessage进行了很多扩展,包括可以传入多个参数,参数支持绑定,可以通过CanExecute作执行前判断并设置控件的Enable等。
2.Conventions,协定,这个词听上去有点虚,其实就是智能匹配的意思。CM制定了一系列匹配的规则,比如说View和ViewModel之间的匹配,绑定时传入控件名可以找到控件,传入方法名可以绑定到方法等等。
3.Screen和Conductor,作为一个Presentation的框架,各个UI部件(Widget或者叫Pad)的管理是必不可少的。Screen就是用来表示UI部件的,它定义了一些列UI部件的生命期事件,比如Activated,DeActivated等。Conductor是用来管理Screen的,类似于传统的Controller,不同的Screen可以用一个Conductor来管理,Conductor也使用了策略模式允许更改对Screen的处理。
4.Coroutines,协同程序,定义了一组程序的执行,简化了异步编程。比如说在网络中下载图片并显示,通常来说需要显示BusyIndicator,后台线程去网络读取图片,读取成功后Invoke到UI线程,取消BusyIndicator,显示图片。CM提供了一个IResult接口,大大的简化了异步编程,结合ActionMessage,为AOP的扩展提供了可能。
5.配置性和扩展性,CM移除掉了原Caliburn的一些IOC实现,作为一个通用框架,最常用办法就是使用工厂模式结合配置文件提供可配置性,使用IOC来解耦组件间的依赖。CM默认是使用MEF来做IOC扩展的,你可以自定义Bootstrapper来使用你喜欢的IOC容器,如Unity等。
6.设计时支持(Design-time support),CM中的ActionMessage是继承自Blend中的TriggerAction的,也就是说可以在Blend编辑ActionMessage,大大方便了使用。 (这段摘抄了周永恒大大的部分对CM框架的解析,大家想详细了解的话可以去他的博客去学习,我就不仔细说明了,以后我会用到的地方做说明)
MahApps.Metro
这是一个Metro样式的开源项目,应用该项目可以使你的软件具有metro的风格,具体就不多说了。
这是测试项目第一阶段的运行结果
言归正传,我们从零开始创建项目,下面是整个测试项目的结构:
MefBootstrapper
这是启动加载类,一般我们WPF程序是从APP.XAML里StartupUri=“****WINDOWS.XAML”来启动主窗体,但现在由MefBootstrapper担当了启动窗体的职责:
public class MefBootstrapper : Bootstrapper<IShell> { private CompositionContainer container; protected override void Configure() { /*CompositionContainer 对象在应用程序中有两种的主要用途。首先,它跟踪哪些部分可用于组合、它们的依赖项,并且充当任何指定组合的上下文。其次,它提供了应用程序可以启动组合的方法、获取组合部件的实例,或填充可组合部件的依存关系。 部件可直接用于容器,或通过 Catalog 属性来用于容器。在此 ComposablePartCatalog 中可发现的所有部件都可以供容器来满足导入,还包括直接添加的任何部件。*/ container = new CompositionContainer( new AggregateCatalog(AssemblySource.Instance.Select(x => new AssemblyCatalog(x))) ); var batch = new CompositionBatch(); var dockScreenManager = new DockScreenManager(); batch.AddExportedValue<IWindowManager>(new WindowManager());//將指定的导出加入至 CompositionBatch 物件 batch.AddExportedValue<IDockScreenManager>(dockScreenManager); batch.AddExportedValue<IEventAggregator>(new EventAggregator()); batch.AddExportedValue(container); container.Compose(batch);//在容器上执行组合,包括指定的 CompositionBatch 中的更改 } protected override object GetInstance(Type serviceType, string key) { var contract = string.IsNullOrEmpty(key) ? AttributedModelServices.GetContractName(serviceType) : key;//获取指定类型的规范协定名称 var exports = container.GetExportedValues<object>(contract);//返回具有从指定的类型参数派生的协定名称的已导出对象。如果不是正好有一个匹配的已导出对象,则将引发异常。 if (exports.Any()) return exports.First(); throw new Exception(string.Format("Could not locate any instances of contract {0}.", contract)); } protected override IEnumerable<object> GetAllInstances(Type serviceType) { return container.GetExportedValues<object>(AttributedModelServices.GetContractName(serviceType)); } protected override void BuildUp(object instance) { container.SatisfyImportsOnce(instance);//满足指定的 ComposablePart 对象的导入,而无需注册该对象以进行重新组合。 } }
由上可知, MefBootstrapper继承与CM框架提供Bootstrapper<TRootModel>,当Bootstrapper加载时,CM框架便会从MEF容器里寻找出TRootModel类型的实例,并且show出来,也就是我们的主窗体,之后我会把项目源码放出来,大家可以自己跟踪OnStartup事件。
我们来看看APP.XMAL
<Application x:Class="DemoApplication.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:DemoApplication" > <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary> <local:MefBootstrapper x:Key="bootstrapper" /> </ResourceDictionary> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
IShell
一个简单的接口,为了方便MEF导出部件
public interface IShell { }
ShellView.xaml
<MetrolControls:MetroWindow x:Class="DemoApplication.ShellView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:MetrolControls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" Title="ShellView" Height="500" Width="800"> <Window.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Colours.xaml"/> <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml"/> <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Window.Resources> <Grid> <ContentControl x:Name="DockContent" Margin="0,2,0,0" Grid.Row="0"/> </Grid> </MetrolControls:MetroWindow>
可以看到,这时我们引用了MahApps.Metro,MahApps.Metro自定义了一个WINDOWS,在我看来比传统的和谐那么一点,MahApps.Metro里还有10多种自定义控件,有兴趣的可以自己去研究
ShellViewModel
[Export(typeof(IShell))] class ShellViewModel:IShell { [Import] public IScreen DockContent { get; set; } }
一个ShellView.xaml对应一个ShellViewModel,当ShellViewModel标记为Export时,Bootstrapper会把当前程序集所有标记为Export的类导入CM框架的IOC容器里,ShellViewModel相当于ShellView的Datacontext,一个View的加载过程为,由Model找到(CM框架定义了各种查找规则)View,并把Model绑定到View的Datacontext,以后我们UI的逻辑代码就可以写在Model里面,并与UI完全分开,这就是我们所说的MVVM模式。上面也有一个典型的View绑定Model里的属性,细心的可以看到:
[Import]
public IScreen DockContent { get; set; }
该属性的名称和ShellView.xaml里的<ContentControl x:Name="DockContent" Margin="0,2,0,0" Grid.Row="0"/> 的命名完全一样,奇怪的是我们并没有写任何绑定,但DockContent是怎么绑定到View里面的呢,其实绑定的过程已经由CM框架帮我们做了,CM框架会帮助我们把Model里和控件名称一样的属性绑定在一起,这就然我们省了一些事,这只是CM框架的一些小特性。
好了,主窗体的说完了,下面我们来看看怎么把AvalonDock融合进去,上面我们说过了,一个Model对应一个View,所以我们要显示一个UserControl时得生成一对Model-View,
DockView.xaml
<UserControl x:Class="DemoApplication.Views.DockView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" xmlns:avalonDock="http://avalondock.codeplex.com" d:DesignHeight="300" d:DesignWidth="800"> <UserControl.Resources> <avalonDock:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/> </UserControl.Resources> <Grid> <Grid x:Name="layoutRoot"> <avalonDock:DockingManager Grid.Row="1" x:Name="dockManager" AllowMixedOrientation="True" > <avalonDock:DockingManager.Theme> <avalonDock:MetroTheme/> </avalonDock:DockingManager.Theme> <avalonDock:DockingManager.LayoutItemTemplate> <DataTemplate> <ContentControl IsTabStop="False" /> </DataTemplate> </avalonDock:DockingManager.LayoutItemTemplate> <avalonDock:LayoutRoot> <avalonDock:LayoutPanel Orientation="Horizontal" > <avalonDock:LayoutAnchorablePaneGroup DockWidth="200" Orientation="Vertical" > <avalonDock:LayoutAnchorablePane > <avalonDock:LayoutAnchorable Title="left" ></avalonDock:LayoutAnchorable> </avalonDock:LayoutAnchorablePane> </avalonDock:LayoutAnchorablePaneGroup> <avalonDock:LayoutPanel Orientation="Vertical" > <avalonDock:LayoutDocumentPaneGroup Orientation="Horizontal"> <avalonDock:LayoutDocumentPane > <avalonDock:LayoutDocument Title="main"></avalonDock:LayoutDocument> </avalonDock:LayoutDocumentPane> </avalonDock:LayoutDocumentPaneGroup> <avalonDock:LayoutAnchorablePaneGroup DockHeight="100" Orientation="Horizontal" > <avalonDock:LayoutAnchorablePane > <avalonDock:LayoutAnchorable Title="bottom" ></avalonDock:LayoutAnchorable> </avalonDock:LayoutAnchorablePane> </avalonDock:LayoutAnchorablePaneGroup> </avalonDock:LayoutPanel> <avalonDock:LayoutAnchorablePaneGroup DockWidth="200" Orientation="Horizontal" > <avalonDock:LayoutAnchorablePane > <avalonDock:LayoutAnchorable Title="Right" ></avalonDock:LayoutAnchorable> </avalonDock:LayoutAnchorablePane> </avalonDock:LayoutAnchorablePaneGroup> </avalonDock:LayoutPanel> </avalonDock:LayoutRoot> </avalonDock:DockingManager> </Grid> </Grid> </UserControl>
这是VS2012设计器的显示
这些东西的学习周期还是有的,我就不一一去说。有些东西只可意会不可言传。
DockViewModel
[Export(typeof(IScreen))] public class DockViewModel : Screen { }
我们可以看到ShellViewModel里的DockContent就是IScreen类型的,由于标记为Import,所以程序会自动帮我们把MEF容器里IScreen类型注入,所以其实DockContent就是DockView,我这里为了方便直接用了CM框架的IScreen,如果有两个类标记为[Export(typeof(IScreen))],就会导致程序异常,因为有两个实例。程序不知道该导出哪个,所以我们之后会定义另一个接口,该接口只有唯一一个类即唯一的DockViewModel标记为导出,因为我们DockView就是唯一的,导入和导出部件这是MEF的知识,MEF是什么大家可以百度学习,CM框架默认是MEF作为容器。
MEF
Managed Extensibility Framework(MEF)是.NET平台下的一个扩展性管理框架,它是一系列特性的集合,包括依赖注入(DI)以及Duck Typing等。MEF为开发人员提供了一个工具,让我们可以轻松的对应用程序进行扩展并且对已有的代码产生最小的影响,开发人员在开发过程中根据功能要求定义一些扩展点,之后扩展人员就可以使用这些扩展点与应用程序交互;同时MEF让应用程序与扩展程序之间不产生直接的依赖,这样也允许在多个具有同样的扩展需求之间共享扩展程序
好了,第一阶段就先这样,以后我会慢慢更新,直道整个插件系统的完成,项目初始源码:
http://download.csdn.net/detail/ws01271188/5649317
http://pan.baidu.com/share/link?shareid=1619526114&uk=554439928 备用