在上文《分享一個很是漂亮的WPF界面框架》中我簡單的介紹了一個界面框架,有朋友已經指出了,這個界面框架是基於ModernUI來實現的,在該文我將分享全部的源碼,並詳細描述如何基於ModernUI來構造一個很是通用的、插件化的WPF開發框架。下載源碼的同志,但願點擊一下推薦。javascript
本文將按照如下四點來介紹:html
(1)ModernUI簡介;java
(2)構建通用界面框架的思路;node
(3)基於ModernUI和OSGi.NET的插件化界面框架實現原理及源碼分析;程序員
(4)其它更有趣的東西~~。數據庫
1 ModernUI簡介app
ModernUI(http://mui.codeplex.com/)是一個開源的WPF界面庫,利用該界面庫,咱們能夠建立很酷的應用程序。下面是ModernUI官方示例,你能夠從官方網站直接下載源碼運行,若是是.NET 4.0的話,記得要聲明「NET4」預編譯變量,不然沒法編譯經過。框架
要編寫這樣的WPF界面,咱們須要在一個Window上聲明菜單和Tab頁面,下圖是定義菜單的聲明。模塊化
此外,每個Tab風格頁面,你也須要手動的爲菜單建立這樣的界面元素。函數
直接用這樣的方式來使用ModernUI,顯然不太適合團隊協做性的並行開發,由於在一個團隊的協做中,不一樣的人須要完成不一樣的功能,實現不一樣頁面,每一個人都須要來更改主界面。
我很是但願模塊化的開發方法,由於這能夠儘量的複用現有資產,使程序員能夠聚焦在本身關注的業務邏輯上,不須要關心UI的使用。下面,我未來描述基於ModernUI實現的一個通用界面框架,這個界面框架容許程序員在本身的業務模塊中配置須要顯示的界面元素。
2 通用界面框架實現思路
我但願可以實現這樣的通用界面框架:
(1)程序員能夠直接實現須要展示業務邏輯的界面,不須要關注如何使用ModernUI;
(2)程序員能夠經過簡單的配置就能夠將本身實現的業務邏輯頁面顯示在主界面中;
(3)這個界面框架能夠徹底複用。
當我看到ModernUI這個界面庫時,我但願將應用程序作成模塊化,每個模塊可以:
(1)經過如下配置可以直接顯示二級菜單。
(2)經過如下配置可以直接顯示三級菜單。
這樣作的好處是,開發插件的時候能夠不須要關心界面框架插件;團隊在協做開發應用的時候,能夠獨立開發並不須要修改主界面;團隊成員的插件能夠隨時集成到這個主界面;當主界面沒法知足咱們的佈局時或者用戶需求沒法知足時,能夠直接替換主界面框架而不須要修改任何插件代碼。
最終的效果以下,如下界面的幾個菜單及點擊菜單顯示的內容由DemoPlugin插件、DemoPlugin2插件來提供。當插件框架加載更多插件時,界面上會出現更多的菜單;反之,當插件被卸載或者被中止時,則相應的菜單將消失掉。
下面我來介紹如何實現。
3 基於ModernUI和OSGi.NET的插件化界面框架實現原理及源碼分析
3.1 OSGi.NET插件框架原理簡介
OSGi.NET框架是一個徹底通用的.NET插件框架,它支持WPF、WinForm、ASP.NET、ASP.NET MVC 3.0/4.0、控制檯等任意.NET應用程序,也就是說,你能夠基於該插件框架來快速構架插件化的應用程序。OSGi.NET插件框架提供了插件化支持、插件擴展和麪向服務支持三大功能。
OSGi.NET插件框架啓動時,從插件目錄中搜索插件,安裝並啓動這些插件,將這些插件組裝在插件框架中;一個插件能夠暴露擴展點,容許其它插件在不更改其代碼狀況下,擴展該插件的功能;插件間能夠經過服務來進行通信。
在一個插件應用程序中,它首先要獲取一個入口點,這個入口點由一個插件來提供,而後進入這個插件的入口並運行起來。一個提供入口的插件一般是一個主界面插件,好比上面介紹的這個WPF界面框架。也就是說,插件應用程序啓動起來後,會先運行這個界面框架的主界面。而主界面通常都提供了關於界面元素的擴展,容許其它插件將菜單、導航和內容頁面註冊到主界面,所以,當主界面運行時,它會將其它插件註冊的界面元素顯示出來。當用戶點擊界面元素時,插件框架就會加載這個插件的頁面,某個插件的頁面在呈現時,則有可能會從數據庫中提取數據展現,這時候,該插件則可能會調用數據訪問服務提供的通用數據訪問接口。OSGi.NET提供的三大功能,恰好可以很是的吻合這樣的系統的啓動形式。固然,OSGi.NET除了提供插件三大支撐功能以外,它還支持插件動態性與隔離性。動態性,意味着咱們能夠在運行時來動態安裝、啓動、中止、卸載和更新插件,而隔離性則意味着每個插件都擁有本身獨立的目錄,有本身獨立的類型加載器和類型空間。
基於OSGi.NET插件框架,咱們很容易實現插件的動態安裝、遠程管理、自動化部署、自動升級和應用商店。下面,我來描述如何使用OSGi.NET來構建一個WPF插件應用。
3.2 基於OSGi.NET來實現WPF插件應用
利用OSGi.NET來建立一個WPF插件應用很是的簡單。只須要實現:(1)建立一個插件主程序,定義插件目錄;(2)在主程序中利用BootStrapper實現OSGi.NET內核升級檢測與自動升級;(3)啓動插件框架;(4)利用PageFlowService獲取主界面,而後運行主界面。下面咱們看一下插件主程序。(注:若是你安裝了OSGi.NET框架,能夠直接使用項目模板來建立WPF主程序項目。)
在這個主程序,咱們在項目的屬性將輸出路徑改成bin,並在bin目錄下建立一個Plugins目錄,而後將OSGi.NET四個標準插件拷貝到Plugins目錄,它們分別用於:(1)插件遠程管理,即RemotingManagement和WebServiceWrapperService,支持遠程管理控制檯調試用;(2)插件管理服務,即UIShell.BundleManagementService,支持對本地插件管理和插件倉庫訪問與下載;(3)頁面流服務,即UIShell.PageFlowService,用於獲取主界面。
下面咱們來看一下App.xaml.cs源碼,在這裏實現了插件加載、啓動和進入主界面的功能。
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 } }
上述代碼很是簡單,我將介紹一下每個函數的功能。
(1)構造函數:調用UpdateCore和StartBundleRuntime;
(2)UpdateCore:調用BootStrapper程序集的CoreFileUpdater來實現內核文件升級;
(3)StartBundleRuntime:建立一個BundleRuntime,即插件框架,BundleRuntime默認構造函數指定的插件目錄爲Plugins;啓動BundleRuntime,即啓動插件框架;掛載Startup和Exit事件;
(4)在App_Startup事件處理函數中,從插件框架獲取PageFlowService服務,利用該服務獲取主界面,而後建立該界面實例,並運行;
(5)在App_Exit事件處理函數中,終止插件框架,釋放資源。
3.3 基於ModernUI實現通用界面插件框架
我在第2節描述了通用界面框架的思路。這個界面框架將基於OSGi.NET插件框架三大功能之一——插件擴展來實現。我將按照如下順序來描述實現。
3.3.1 OSGi.NET插件擴展原理
下圖是OSGi.NET插件擴展原理,在這裏,須要暴露擴展點的插件暴露一個ExtensionPoint,提供擴展的插件則聲明一個Extension(XML格式),以下所示。暴露擴展點的插件經過OSGi.NET框架獲取全部Extension,而後對其進行處理。
依據第2節描述,通用界面框架插件須要暴露擴展點和處理擴展。暴露擴展點意味着它須要定義界面擴展的格式。下面我來介紹擴展格式的XML定義。
3.3.2 界面擴展XML定義
根據界面框架要實現的功能,咱們定義的擴展格式,以下所示。擴展點的名稱爲UIShell.WpfShellPlugin.LinkGroups。經過LinkGroup來定義一級菜單,經過Link來定義葉子節點菜單,經過TabLink來定義三級菜單的Tab佈局方式。
<Extension Point="UIShell.WpfShellPlugin.LinkGroups"> <LinkGroup DisplayName="一級菜單" DefaultContentSource="默認顯示頁面"> <Link DisplayName="二級菜單" Source="二級菜單頁面" /> <TabLink DisplayName="三級菜單Tab佈局" DefaultContentSource="默認頁面" Layout="List/Tab"> <Link DisplayName="三級菜單" Source="三級菜單頁面" /> </TabLink> </LinkGroup> </Extension>
界面框架插件須要作的就是獲取這樣的XML定義,而且自動在界面上將元素建立出來並自動加載插件提供的頁面。下面我來介紹界面框架如何實現。
3.3.3 界面框架的實現
界面框架基於ModernUI來實現,它須要完成:(1)爲Extension建立擴展模型;(2)獲取全部擴展模型對象,並在主界面建立界面元素;(3)監聽擴展變動事件,動態變動界面元素。
首先,咱們來看看擴展模型的構建。在這裏,定義了LinkGroupData、TabLinkData、LinkData分別對應於擴展的XML的元素。
這裏的ShellExtensionPointHandler對象則用於同OSGi.NET框架擴展擴展信息,並將其轉換成擴展對象模型,而後存儲在LinkGroups屬性中。LinkGroups爲ObservableCollection,當添加或者刪除LinkGroup時會拋出Add/Remov事件。下面來看一下這個類的代碼。
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); } } } }
這個類有如下幾個方法:
(1)InitExtensions:即從OSGi.NET框架獲取已經註冊的擴展信息,將其轉換成LinkGroupData實例,並保存;
(2)Context_ExtensionChanged事件處理函數:即當Extension被添加或者刪除時的處理函數,這在插件安裝和卸載時發生,咱們須要將新建的Extension轉換成LinkGroupData實例保存起來,須要已刪除的Extension對應的LinkGroupData實例移除掉。
那接下來咱們來看一下主界面如何根據擴扎模型來建立或者刪除界面元素。首先,你能夠看到,這個主界面是空的沒有預先定義任何的界面元素。
那你必定猜到了,這個界面確定是經過代碼來動態建立界面元素,咱們來看看代碼先。
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); } // 設置第一個頁面 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); } } } }
上面的代碼也很簡單,邏輯很清晰,我來講明一下各個方法的用處:
(1)InitializeLinkGroupsForExtensions:獲取擴展模型對象,並將對象轉換成界面元素LinkGroup,而後監聽擴展模型變動事件;
(2)LinkGroups_CollectionChanged:擴展模型變動事件,當有擴展對象添加時,須要添加新的界面元素;反之,則須要移除界面元素;
(3)CreateLinkGroupForData:爲擴展模型建立界面元素LinkGroup;
(4)RemoveLinkGroupForData:當擴展模型被刪除時,須要將對應的界面元素刪除掉。
爲了支持插件化,還須要爲ModernUI作一個變動,下面我未來介紹。
3.4 ModernUI插件化支撐所作的變動
爲了支持插件化,我須要對ModernUI的ContentLoader進行擴展,使其支持直接從插件加載內容頁面。詳細查看如下代碼。
/// <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; }
這集成了默認的加載行爲,同時支持:(1)以「[BundleSymbolicName]@[PageClassName]」方式支持內容加載;(2)支持WPF傳統資源加載方式;(3)支持參數化。
另外,爲了實現三級菜單,我定義了一個ContentPlaceHolder,它用於獲取第三級的菜單,並建立內容,代碼以下。
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; } } }
它利用傳遞的參數能夠獲取對應的三級菜單的擴展模型,而後建立對應的界面元素。
到此,咱們已經成功實現了整個插件化的界面框架了,文章有點長,能堅持看到這的基本屬於勇士了~~,接下來還想用一點點篇幅演示一下界面框架動態性。
4 動態性演示
OSGi.NET動態性支持容許咱們在程序運行中來安裝、啓動、中止、卸載和更新插件,請看下圖。當你運行下載的程序時,最開始會展現如下菜單,其中「演示十一、演示12」菜單由DemoPlugin插件註冊,「演示3」由DemoPlugin2插件提供,此時,你運行一下遠程管理控制檯,輸入list指令後,能夠發現這兩個插件都是Active狀態。
下面咱們輸入「stop 2」指令,將DemoPlugin插件中止,以下圖所示,此時你能夠發現DemoPlugin註冊的菜單已經動態的從主界面中被移除掉了。
一樣,你還能夠繼續嘗試Start、Stop、Install、Uninstall等指令來動態更改插件狀態,從而影響應用程序的行爲。
固然,你也能夠經過「插件管理」來實現對內核安裝的插件的狀態變動,以下所示。
再進一步,你能夠直接訪問插件倉庫來安裝更多的插件。你能夠在源碼中查看到如何實現插件管理和插件倉庫訪問及下載安裝插件的代碼。
怎樣,很強大吧!!若是咱們構建了這樣的通用框架,之後開發起來那簡單多了。固然,若是你還有興趣的話,你能夠再嘗試瞭解基於插件的一鍵部署和自動化升級,我在《分享讓你震驚的自動化升級和部署方案,讓咱們一塊兒來PK一下!》這篇文章介紹了。
好了~,很是感謝這麼耐心看完這篇文章。該附上源碼了~。
源碼下載 點擊下載。