在上文《分享一個(gè)非常漂亮的WPF界面框架》中我簡單的介紹了一個(gè)界面框架,有朋友已經(jīng)指出了,這個(gè)界面框架是基于ModernUI來實(shí)現(xiàn)的,在該文我將分享所有的源碼,并詳細(xì)描述如何基于ModernUI來構(gòu)造一個(gè)非常通用的、插件化的WPF開發(fā)框架。下載源碼的同志,希望點(diǎn)擊一下推薦。
本文將按照以下四點(diǎn)來介紹:
(1)ModernUI簡介;
(2)構(gòu)建通用界面框架的思路;
(3)基于ModernUI和OSGi.NET的插件化界面框架實(shí)現(xiàn)原理及源碼分析;
(4)其它更有趣的東西~~。
1 ModernUI簡介
ModernUI(http://mui.codeplex.com/)是一個(gè)開源的WPF界面庫,利用該界面庫,我們可以創(chuàng)建很酷的應(yīng)用程序。下面是ModernUI官方示例,你可以從官方網(wǎng)站直接下載源碼運(yùn)行,如果是.NET 4.0的話,記得要聲明“NET4”預(yù)編譯變量,否則無法編譯通過。
要編寫這樣的WPF界面,我們需要在一個(gè)Window上聲明菜單和Tab頁面,下圖是定義菜單的聲明。
此外,每一個(gè)Tab風(fēng)格頁面,你也需要手動(dòng)的為菜單創(chuàng)建這樣的界面元素。
直接用這樣的方式來使用ModernUI,顯然不太適合團(tuán)隊(duì)協(xié)作性的并行開發(fā),因?yàn)樵谝粋€(gè)團(tuán)隊(duì)的協(xié)作中,不同的人需要完成不同的功能,實(shí)現(xiàn)不同頁面,每個(gè)人都需要來更改主界面。
我非常希望模塊化的開發(fā)方法,因?yàn)檫@可以盡可能的復(fù)用現(xiàn)有資產(chǎn),使程序員可以聚焦在自己關(guān)注的業(yè)務(wù)邏輯上,不需要關(guān)心UI的使用。下面,我將來描述基于ModernUI實(shí)現(xiàn)的一個(gè)通用界面框架,這個(gè)界面框架允許程序員在自己的業(yè)務(wù)模塊中配置需要顯示的界面元素。
2 通用界面框架實(shí)現(xiàn)思路
我希望能夠?qū)崿F(xiàn)這樣的通用界面框架:
(1)程序員可以直接實(shí)現(xiàn)需要展現(xiàn)業(yè)務(wù)邏輯的界面,不需要關(guān)注如何使用ModernUI;
(2)程序員可以通過簡單的配置就可以將自己實(shí)現(xiàn)的業(yè)務(wù)邏輯頁面顯示在主界面中;
(3)這個(gè)界面框架可以完全復(fù)用。
當(dāng)我看到ModernUI這個(gè)界面庫時(shí),我希望將應(yīng)用程序做成模塊化,每一個(gè)模塊能夠:
(1)通過以下配置能夠直接顯示二級(jí)菜單。
(2)通過以下配置能夠直接顯示三級(jí)菜單。
這樣做的好處是,開發(fā)插件的時(shí)候可以不需要關(guān)心界面框架插件;團(tuán)隊(duì)在協(xié)作開發(fā)應(yīng)用的時(shí)候,可以獨(dú)立開發(fā)并不需要修改主界面;團(tuán)隊(duì)成員的插件可以隨時(shí)集成到這個(gè)主界面;當(dāng)主界面無法滿足我們的布局時(shí)或者用戶需求無法滿足時(shí),可以直接替換主界面框架而不需要修改任何插件代碼。
最終的效果如下,以下界面的幾個(gè)菜單及點(diǎn)擊菜單顯示的內(nèi)容由DemoPlugin插件、DemoPlugin2插件來提供。當(dāng)插件框架加載更多插件時(shí),界面上會(huì)出現(xiàn)更多的菜單;反之,當(dāng)插件被卸載或者被停止時(shí),則相應(yīng)的菜單將消失掉。
下面我來介紹如何實(shí)現(xiàn)。
3 基于ModernUI和OSGi.NET的插件化界面框架實(shí)現(xiàn)原理及源碼分析
3.1 OSGi.NET插件框架原理簡介
OSGi.NET框架是一個(gè)完全通用的.NET插件框架,它支持WPF、WinForm、ASP.NET、ASP.NET MVC 3.0/4.0、控制臺(tái)等任意.NET應(yīng)用程序,也就是說,你可以基于該插件框架來快速構(gòu)架插件化的應(yīng)用程序。OSGi.NET插件框架提供了插件化支持、插件擴(kuò)展和面向服務(wù)支持三大功能。
OSGi.NET插件框架啟動(dòng)時(shí),從插件目錄中搜索插件,安裝并啟動(dòng)這些插件,將這些插件組裝在插件框架中;一個(gè)插件可以暴露擴(kuò)展點(diǎn),允許其它插件在不更改其代碼情況下,擴(kuò)展該插件的功能;插件間可以通過服務(wù)來進(jìn)行通訊。
在一個(gè)插件應(yīng)用程序中,它首先要獲取一個(gè)入口點(diǎn),這個(gè)入口點(diǎn)由一個(gè)插件來提供,然后進(jìn)入這個(gè)插件的入口并運(yùn)行起來。一個(gè)提供入口的插件通常是一個(gè)主界面插件,比如上面介紹的這個(gè)WPF界面框架。也就是說,插件應(yīng)用程序啟動(dòng)起來后,會(huì)先運(yùn)行這個(gè)界面框架的主界面。而主界面一般都提供了關(guān)于界面元素的擴(kuò)展,允許其它插件將菜單、導(dǎo)航和內(nèi)容頁面注冊到主界面,因此,當(dāng)主界面運(yùn)行時(shí),它會(huì)將其它插件注冊的界面元素顯示出來。當(dāng)用戶點(diǎn)擊界面元素時(shí),插件框架就會(huì)加載這個(gè)插件的頁面,某個(gè)插件的頁面在呈現(xiàn)時(shí),則有可能會(huì)從數(shù)據(jù)庫中提取數(shù)據(jù)展示,這時(shí)候,該插件則可能會(huì)調(diào)用數(shù)據(jù)訪問服務(wù)提供的通用數(shù)據(jù)訪問接口。OSGi.NET提供的三大功能,剛好能夠非常的吻合這樣的系統(tǒng)的啟動(dòng)形式。當(dāng)然,OSGi.NET除了提供插件三大支撐功能之外,它還支持插件動(dòng)態(tài)性與隔離性。動(dòng)態(tài)性,意味著我們可以在運(yùn)行時(shí)來動(dòng)態(tài)安裝、啟動(dòng)、停止、卸載和更新插件,而隔離性則意味著每一個(gè)插件都擁有自己獨(dú)立的目錄,有自己獨(dú)立的類型加載器和類型空間。
基于OSGi.NET插件框架,我們很容易實(shí)現(xiàn)插件的動(dòng)態(tài)安裝、遠(yuǎn)程管理、自動(dòng)化部署、自動(dòng)升級(jí)和應(yīng)用商店。下面,我來描述如何使用OSGi.NET來構(gòu)建一個(gè)WPF插件應(yīng)用。
3.2 基于OSGi.NET來實(shí)現(xiàn)WPF插件應(yīng)用
利用OSGi.NET來創(chuàng)建一個(gè)WPF插件應(yīng)用非常的簡單。只需要實(shí)現(xiàn):(1)創(chuàng)建一個(gè)插件主程序,定義插件目錄;(2)在主程序中利用BootStrapper實(shí)現(xiàn)OSGi.NET內(nèi)核升級(jí)檢測與自動(dòng)升級(jí);(3)啟動(dòng)插件框架;(4)利用PageFlowService獲取主界面,然后運(yùn)行主界面。下面我們看一下插件主程序。(注:如果你安裝了OSGi.NET框架,可以直接使用項(xiàng)目模板來創(chuàng)建WPF主程序項(xiàng)目。)
在這個(gè)主程序,我們在項(xiàng)目的屬性將輸出路徑改為bin,并在bin目錄下創(chuàng)建一個(gè)Plugins目錄,然后將OSGi.NET四個(gè)標(biāo)準(zhǔn)插件拷貝到Plugins目錄,它們分別用于:(1)插件遠(yuǎn)程管理,即RemotingManagement和WebServiceWrapperService,支持遠(yuǎn)程管理控制臺(tái)調(diào)試用;(2)插件管理服務(wù),即UIShell.BundleManagementService,支持對(duì)本地插件管理和插件倉庫訪問與下載;(3)頁面流服務(wù),即UIShell.PageFlowService,用于獲取主界面。
下面我們來看一下App.xaml.cs源碼,在這里實(shí)現(xiàn)了插件加載、啟動(dòng)和進(jìn)入主界面的功能。
namespace UIShell.iOpenWorks.WPF{ /// <summary> /// WPF startup class. /// </summary> public partial class App : Application { // Use object type to avoid load UIShell.OSGi.dll before update. private object _bundleRuntime; public App() { UpdateCore(); StartBundleRuntime(); } void UpdateCore() // Update Core Files, including BundleRepositoryOpenAPI, PageFlowService and OSGi Core assemblies. { if (AutoUpdateCoreFiles) { new CoreFileUpdater().UpdateCoreFiles(CoreFileUpdateCheckType.Daily); } } void StartBundleRuntime() // Start OSGi Core. { var bundleRuntime = new BundleRuntime(); bundleRuntime.AddService<Application>(this); bundleRuntime.Start(); Startup += App_Startup; Exit += App_Exit; _bundleRuntime = bundleRuntime; } void App_Startup(object sender, StartupEventArgs e) { Application app = Application.Current; var bundleRuntime = _bundleRuntime as BundleRuntime; app.ShutdownMode = ShutdownMode.OnLastWindowClose; #region Get the main window var pageFlowService = bundleRuntime.GetFirstOrDefaultService<IPageFlowService>(); if (pageFlowService == null) { throw new Exception("The page flow service is not installed."); } if (pageFlowService.FirstPageNode == null || string.IsNullOrEmpty(pageFlowService.FirstPageNode.Value)) { throw new Exception("There is not first page node defined."); } var windowType = pageFlowService.FirstPageNodeOwner.LoadClass(pageFlowService.FirstPageNode.Value); if (windowType == null) { throw new Exception(string.Format("Can not load Window type '{0}' from Bundle '{1}'.", pageFlowService.FirstPageNode.Value, pageFlowService.FirstPageNodeOwner.SymbolicName)); } app.MainWindow = System.Activator.CreateInstance(windowType) as Window; #endregion app.MainWindow.Show(); } void App_Exit(object sender, ExitEventArgs e) { if (_bundleRuntime != null) { var bundleRuntime = _bundleRuntime as BundleRuntime; bundleRuntime.Stop(); _bundleRuntime = null; } } // Other codes }}
上述代碼非常簡單,我將介紹一下每一個(gè)函數(shù)的功能。
(1)構(gòu)造函數(shù):調(diào)用UpdateCore和StartBundleRuntime;
(2)UpdateCore:調(diào)用BootStrapper程序集的CoreFileUpdater來實(shí)現(xiàn)內(nèi)核文件升級(jí);
(3)StartBundleRuntime:創(chuàng)建一個(gè)BundleRuntime,即插件框架,BundleRuntime默認(rèn)構(gòu)造函數(shù)指定的插件目錄為Plugins;啟動(dòng)BundleRuntime,即啟動(dòng)插件框架;掛載Startup和Exit事件;
(4)在App_Startup事件處理函數(shù)中,從插件框架獲取PageFlowService服務(wù),利用該服務(wù)獲取主界面,然后創(chuàng)建該界面實(shí)例,并運(yùn)行;
(5)在App_Exit事件處理函數(shù)中,終止插件框架,釋放資源。
3.3 基于ModernUI實(shí)現(xiàn)通用界面插件框架
我在第2節(jié)描述了通用界面框架的思路。這個(gè)界面框架將基于OSGi.NET插件框架三大功能之一——插件擴(kuò)展來實(shí)現(xiàn)。我將按照以下順序來描述實(shí)現(xiàn)。
3.3.1 OSGi.NET插件擴(kuò)展原理
下圖是OSGi.NET插件擴(kuò)展原理,在這里,需要暴露擴(kuò)展點(diǎn)的插件暴露一個(gè)ExtensionPoint,提供擴(kuò)展的插件則聲明一個(gè)Extension(XML格式),如下所示。暴露擴(kuò)展點(diǎn)的插件通過OSGi.NET框架獲取所有Extension,然后對(duì)其進(jìn)行處理。
依據(jù)第2節(jié)描述,通用界面框架插件需要暴露擴(kuò)展點(diǎn)和處理擴(kuò)展。暴露擴(kuò)展點(diǎn)意味著它需要定義界面擴(kuò)展的格式。下面我來介紹擴(kuò)展格式的XML定義。
3.3.2 界面擴(kuò)展XML定義
根據(jù)界面框架要實(shí)現(xiàn)的功能,我們定義的擴(kuò)展格式,如下所示。擴(kuò)展點(diǎn)的名稱為UIShell.WpfShellPlugin.LinkGroups。通過LinkGroup來定義一級(jí)菜單,通過Link來定義葉子節(jié)點(diǎn)菜單,通過TabLink來定義三級(jí)菜單的Tab布局方式。
<Extension Point="UIShell.WpfShellPlugin.LinkGroups"> <LinkGroup DisplayName="一級(jí)菜單" DefaultContentSource="默認(rèn)顯示頁面"> <Link DisplayName="二級(jí)菜單" Source="二級(jí)菜單頁面" /> <TabLink DisplayName="三級(jí)菜單Tab布局" DefaultContentSource="默認(rèn)頁面" Layout="List/Tab"> <Link DisplayName="三級(jí)菜單" Source="三級(jí)菜單頁面" /> </TabLink> </LinkGroup></Extension>
界面框架插件需要做的就是獲取這樣的XML定義,并且自動(dòng)在界面上將元素創(chuàng)建出來并自動(dòng)加載插件提供的頁面。下面我來介紹界面框架如何實(shí)現(xiàn)。
3.3.3 界面框架的實(shí)現(xiàn)
界面框架基于ModernUI來實(shí)現(xiàn),它需要完成:(1)為Extension創(chuàng)建擴(kuò)展模型;(2)獲取所有擴(kuò)展模型對(duì)象,并在主界面創(chuàng)建界面元素;(3)監(jiān)聽擴(kuò)展變更事件,動(dòng)態(tài)變更界面元素。
首先,我們來看看擴(kuò)展模型的構(gòu)建。在這里,定義了LinkGroupData、TabLinkData、LinkData分別對(duì)應(yīng)于擴(kuò)展的XML的元素。
這里的ShellExtensionPointHandler對(duì)象則用于同OSGi.NET框架擴(kuò)展擴(kuò)展信息,并將其轉(zhuǎn)換成擴(kuò)展對(duì)象模型,然后存儲(chǔ)在LinkGroups屬性中。LinkGroups為ObservableCollection,當(dāng)添加或者刪除LinkGroup時(shí)會(huì)拋出Add/Remov事件。下面來看一下這個(gè)類的代碼。
using System;using System.Collections.Generic;using System.Collections.ObjectModel;using System.Linq;using System.Text;using System.Xml;using UIShell.OSGi;namespace UIShell.WpfShellPlugin.ExtensionModel{ public class ShellExtensionPointHandler { public const string ExtensionPointName = "UIShell.WpfShellPlugin.LinkGroups"; public IBundle Bundle { get; private set; } public ObservableCollection<LinkGroupData> LinkGroups { get; private set; } public ShellExtensionPointHandler(IBundle bundle) { Bundle = bundle; InitExtensions(); if (Bundle.Context != null) { Bundle.Context.ExtensionChanged += Context_ExtensionChanged; } } void InitExtensions() // Init { if (Bundle.Context == null) { return; } // Get all extensions. var extensions = Bundle.Context.GetExtensions(ExtensionPointName); LinkGroups = new ObservableCollection<LinkGroupData>(); // Convert extensions to LinkGroupData collection. foreach (var extension in extensions) { AddExtension(extension); } } // Handle ExtensionChanged event. void Context_ExtensionChanged(object sender, ExtensionEventArgs e) { if (e.ExtensionPoint.Equals(ExtensionPointName)) { // Create LinkGroupData objects for new Extension. if (e.Action == CollectionChangedAction.Add) { AddExtension(e.Extension); } else // Remove LinkGroupData objects respond to the Extension. { RemoveExtension(e.Extension); } } } // Convert Extension to LinkGroupData instances. void AddExtension(Extension extension) { LinkGroupData linkGroup; foreach (XmlNode node in extension.Data) { if (node is XmlComment) { continue; } linkGroup = new LinkGroupData(extension); linkGroup.FromXml(node); LinkGroups.Add(linkGroup); } } // Remove LinkGroupData instances of the Extension. void RemoveExtension(Extension extension) { var toBeRemoved = new List<LinkGroupData>(); foreach (var linkGroup in LinkGroups) { if (linkGroup.Extension.Equals(extension)) { toBeRemoved.Add(linkGroup); } } foreach (var linkGroup in toBeRemoved) { LinkGroups.Remove(linkGroup); } } }}
這個(gè)類有以下幾個(gè)方法:
(1)InitExtensions:即從OSGi.NET框架獲取已經(jīng)注冊的擴(kuò)展信息,將其轉(zhuǎn)換成LinkGroupData實(shí)例,并保存;
(2)Context_ExtensionChanged事件處理函數(shù):即當(dāng)Extension被添加或者刪除時(shí)的處理函數(shù),這在插件安裝和卸載時(shí)發(fā)生,我們需要將新建的Extension轉(zhuǎn)換成LinkGroupData實(shí)例保存起來,需要已刪除的Extension對(duì)應(yīng)的LinkGroupData實(shí)例移除掉。
那接下來我們來看一下主界面如何根據(jù)擴(kuò)扎模型來創(chuàng)建或者刪除界面元素。首先,你可以看到,這個(gè)主界面是空的沒有預(yù)先定義任何的界面元素。
那你一定猜到了,這個(gè)界面肯定是通過代碼來動(dòng)態(tài)創(chuàng)建界面元素,我們來看看代碼先。
namespace UIShell.WpfShellPlugin{ /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : ModernWindow { public static ShellExtensionPointHandler ShellExtensionPointHandler { get; set; } private List<Tuple<LinkGroupData, LinkGroup>> LinkGroupTuples { get; set; } public MainWindow() { InitializeComponent(); LinkGroupTuples = new List<Tuple<LinkGroupData, LinkGroup>>(); ShellExtensionPointHandler = new ShellExtensionPointHandler(BundleActivator.Bundle); ShellExtensionPointHandler.LinkGroups.CollectionChanged += LinkGroups_CollectionChanged; InitializeLinkGroupsForExtensions(); } void InitializeLinkGroupsForExtensions() { foreach (var linkGroupData in ShellExtensionPointHandler.LinkGroups) { CreateLinkGroupForData(linkGroupData); } // 設(shè)置第一個(gè)頁面 if (ShellExtensionPointHandler.LinkGroups.Count > 0) { var first = ShellExtensionPointHandler.LinkGroups[0]; ContentSource = new Uri(first.FormatSource(first.DefaultContentSource), UriKind.RelativeOrAbsolute); } } void LinkGroups_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { Action action = () => { if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) { // 新加了LinkGroupData foreach (LinkGroupData item in e.NewItems) { CreateLinkGroupForData(item); } } else if(e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) { // 刪除了LinkGroupData foreach (LinkGroupData item in e.OldItems) { RemoveLinkGroupForData(item); } } }; Dispatcher.Invoke(action); } void CreateLinkGroupForData(LinkGroupData linkGroupData) { var linkGroup = new LinkGroup { DisplayName = linkGroupData.DisplayName, GroupName = linkGroupData.GroupName }; foreach (var linkData in linkGroupData.Links) { if (linkData is LinkData) { linkGroup.Links.Add(new Link { DisplayName = linkData.DisplayName, Source = new Uri(linkData.FormatSource((linkData as LinkData).Source), UriKind.RelativeOrAbsolute) }); } else if (linkData is TabLinkData) { linkGroup.Links.Add(new Link { DisplayName = linkData.DisplayName, Source = new Uri("UIShell.WpfShellPlugin@UIShell.WpfShellPlugin.Pages.ContentPlaceHolder?LinkId=" + linkData.LinkId.ToString(), UriKind.RelativeOrAbsolute) }); } } if (linkGroupData.IsTitleLink) { TitleLinks.Add(new Link { DisplayName = linkGroupData.DisplayName, Source = new Uri(linkGroupData.FormatSource(linkGroupData.DefaultContentSource), UriKind.RelativeOrAbsolute) }); } MenuLinkGroups.Add(linkGroup); LinkGroupTuples.Add(new Tuple<LinkGroupData, LinkGroup>(linkGroupData, linkGroup)); } void RemoveLinkGroupForData(LinkGroupData linkGroupData) { var tuple = LinkGroupTuples.Find(t => t.Item1.Equals(linkGroupData)); if (tuple != null) { MenuLinkGroups.Remove(tuple.Item2); LinkGroupTuples.Remove(tuple); } } }}
上面的代碼也很簡單,邏輯很清晰,我來說明一下各個(gè)方法的用處:
(1)InitializeLinkGroupsForExtensions:獲取擴(kuò)展模型對(duì)象,并將對(duì)象轉(zhuǎn)換成界面元素LinkGroup,然后監(jiān)聽擴(kuò)展模型變更事件;
(2)LinkGroups_CollectionChanged:擴(kuò)展模型變更事件,當(dāng)有擴(kuò)展對(duì)象添加時(shí),需要添加新的界面元素;反之,則需要移除界面元素;
(3)CreateLinkGroupForData:為擴(kuò)展模型創(chuàng)建界面元素LinkGroup;
(4)RemoveLinkGroupForData:當(dāng)擴(kuò)展模型被刪除時(shí),需要將對(duì)應(yīng)的界面元素刪除掉。
為了支持插件化,還需要為ModernUI做一個(gè)變更,下面我將來介紹。
3.4 ModernUI插件化支撐所做的變更
為了支持插件化,我需要對(duì)ModernUI的ContentLoader進(jìn)行擴(kuò)展,使其支持直接從插件加載內(nèi)容頁面。詳細(xì)查看以下代碼。
/// <summary>/// Loads the content from specified uri./// </summary>/// <param name="uri">The content uri</param>/// <returns>The loaded content.</returns>protected virtual object LoadContent(Uri uri){ // don't do anything in design mode if (ModernUIHelper.IsInDesignMode) { return null; } string uriString = string.Empty; string paraString = string.Empty; Dictionary<string, string> parameters = new Dictionary<string, string>(); if (uri.OriginalString.Contains('')) { var uriPara = uri.OriginalString.Split(''); uriString = uriPara[0]; paraString = uriPara[1]; var parameterStrs = paraString.Split('&'); string[] parameterStrSplitted; foreach (var parameterStr in parameterStrs) { parameterStrSplitted = parameterStr.Split('='); parameters.Add(parameterStrSplitted[0], parameterStrSplitted[1]); } } else { uriString = uri.OriginalString; } object result = null; // 1st Format: [BundleSymbolicName]@[Class Full Name] if (uriString.Contains('@')) { var bundleSymbolicNameAndClass = uriString.Split('@'); if (bundleSymbolicNameAndClass.Length != 2 || string.IsNullOrEmpty(bundleSymbolicNameAndClass[0]) || string.IsNullOrEmpty(bundleSymbolicNameAndClass[1])) { throw new Exception("The uri must be in format of '[BundleSymbolicName]@[Class Full Name]'"); } var bundle = BundleRuntime.Instance.Framework.Bundles.GetBundleBySymbolicName(bundleSymbolicNameAndClass[0]); if (bundle == null) { throw new Exception(string.Format("The uri is not correct since the bunde '{0}' does not exist.", bundleSymbolicNameAndClass[0])); } var type = bundle.LoadClass(bundleSymbolicNameAndClass[1]); if (type == null) { throw new Exception(string.Format("The class '{0}' is not found in bunle '{1}'.", bundleSymbolicNameAndClass[1], bundleSymbolicNameAndClass[0])); } result = Activator.CreateInstance(type); } // 2nd Format: /[AssemblyName],Version=[Version];component/[XAML relative path] else if (string.IsNullOrEmpty(paraString)) { result = Application.LoadComponent(uri); } else { result = Application.LoadComponent(new Uri(uriString, UriKind.RelativeOrAbsolute)); } ApplyProperties(result, parameters); return result;}
這集成了默認(rèn)的加載行為,同時(shí)支持:(1)以“[BundleSymbolicName]@[PageClassName]”方式支持內(nèi)容加載;(2)支持WPF傳統(tǒng)資源加載方式;(3)支持參數(shù)化。
另外,為了實(shí)現(xiàn)三級(jí)菜單,我定義了一個(gè)ContentPlaceHolder,它用于獲取第三級(jí)的菜單,并創(chuàng)建內(nèi)容,代碼如下。
namespace UIShell.WpfShellPlugin.Pages{ /// <summary> /// ContentPlaceHolder.xaml 的交互邏輯 /// </summary> public partial class ContentPlaceHolder : UserControl { private string _linkId = string.Empty; private FirstFloor.ModernUI.Windows.Controls.ModernTab _tab; public string LinkId { get { return _linkId; } set { _linkId = value; TabLinkData tabLinkData = null; foreach (var linkGroupData in MainWindow.ShellExtensionPointHandler.LinkGroups) { foreach (var link in linkGroupData.Links) { if (link.LinkId.ToString().Equals(_linkId, StringComparison.OrdinalIgnoreCase)) { tabLinkData = link as TabLinkData; break; } } } if (tabLinkData != null) { _tab.SelectedSource = new Uri(tabLinkData.FormatSource(tabLinkData.DefaultContentSource), UriKind.RelativeOrAbsolute); _tab.Layout = (TabLayout)Enum.Parse(typeof(TabLayout), tabLinkData.Layout); foreach(var linkData in tabLinkData.Links) { _tab.Links.Add(new Link { DisplayName = linkData.DisplayName, Source = new Uri(linkData.FormatSource(linkData.Source), UriKind.RelativeOrAbsolute) }); } } } } public ContentPlaceHolder() { InitializeComponent(); _tab = FindName("ModernTab") as FirstFloor.ModernUI.Windows.Controls.ModernTab; } }}
它利用傳遞的參數(shù)可以獲取對(duì)應(yīng)的三級(jí)菜單的擴(kuò)展模型,然后創(chuàng)建對(duì)應(yīng)的界面元素。
到此,我們已經(jīng)成功實(shí)現(xiàn)了整個(gè)插件化的界面框架了,文章有點(diǎn)長,能堅(jiān)持看到這的基本屬于勇士了~~,接下來還想用一點(diǎn)點(diǎn)篇幅演示一下界面框架動(dòng)態(tài)性。
4 動(dòng)態(tài)性演示
OSGi.NET動(dòng)態(tài)性支持允許我們在程序運(yùn)行中來安裝、啟動(dòng)、停止、卸載和更新插件,請看下圖。當(dāng)你運(yùn)行下載的程序時(shí),最開始會(huì)展示以下菜單,其中“演示11、演示12”菜單由DemoPlugin插件注冊,“演示3”由DemoPlugin2插件提供,此時(shí),你運(yùn)行一下遠(yuǎn)程管理控制臺(tái),輸入list指令后,可以發(fā)現(xiàn)這兩個(gè)插件都是Active狀態(tài)。
下面我們輸入“stop 2”指令,將DemoPlugin插件停止,如下圖所示,此時(shí)你可以發(fā)現(xiàn)DemoPlugin注冊的菜單已經(jīng)動(dòng)態(tài)的從主界面中被移除掉了。
同樣,你還可以繼續(xù)嘗試Start、Stop、Install、Uninstall等指令來動(dòng)態(tài)更改插件狀態(tài),從而影響應(yīng)用程序的行為。
當(dāng)然,你也可以通過“插件管理”來實(shí)現(xiàn)對(duì)內(nèi)核安裝的插件的狀態(tài)變更,如下所示。
再進(jìn)一步,你可以直接訪問插件倉庫來安裝更多的插件。你可以在源碼中查看到如何實(shí)現(xiàn)插件管理和插件倉庫訪問及下載安裝插件的代碼。
怎樣,很強(qiáng)大吧??!如果我們構(gòu)建了這樣的通用框架,以后開發(fā)起來那簡單多了。當(dāng)然,如果你還有興趣的話,你可以再嘗試了解基于插件的一鍵部署和自動(dòng)化升級(jí),我在《分享讓你震驚的自動(dòng)化升級(jí)和部署方案,讓我們一起來PK一下!》這篇文章介紹了。
好了~,非常感謝這么耐心看完這篇文章。該附上源碼了~。
源碼下載 點(diǎn)擊下載。
聯(lián)系客服