很早就想寫這麼一篇文章來對近幾年使用Prism框架來設計軟件來作一次深刻的分析了,但直到最近纔開始整理,說到軟件系統的設計這裏面有太多的學問,只有通過大量的探索纔可以設計出好的軟件產品,就本人的理解,一個好的軟件必須有良好的設計,這其中包括:易閱讀、易擴展、低耦合、模塊化等等,若是你想設計一個好的系統固然還要考慮更多的方面,本篇文章主要是基於微軟的Prism框架來談一談構建模塊化、熱插拔軟件系統的思路,這些都是通過大量項目實戰的結果,但願可以經過這篇文章的梳理可以對構建軟件系統有更加深入的理解。html
首先要簡單介紹一下Prism這個框架:Prism框架經過功能模塊化的思想,將複雜的業務功能和UI耦合性進行分離,經過模塊化,來最大限度的下降耦合性,很適合咱們進行相似插件化的思想來組織系統功能,而且模塊之間,經過發佈和訂閱事件來完成信息的通訊,並且其開放性支持多種框架集成。經過這些簡單的介紹就可以對此有一個簡單的理解,這裏面加入了兩種依賴注入容器,即:Unity和MEF兩種容器,在使用的時候咱們首先須要肯定使用何種容器,這個是第一步。第二步就是如何構建一個成熟的模塊化軟件,這個部分須要咱們可以對整個軟件系統功能上有一個合理的拆分,只有真正地徹底理解整個系統纔可以合理抽象Module,而後下降Module之間的耦合性。第三步就是關於模塊之間是如何進行通信的,這一部分也是很是重要的部分,今天這篇文章就以Prism的Unity依賴注入容器爲例來講明如何構建模塊化軟件系統,同時也簡要說明一下軟件系統的構建思路。express
這裏以百度地圖爲例來講一下若是使用WPF+Prism的框架來設計的話,該怎樣來設計,固然這裏只是舉一個例子,固然這篇文章不會就裏面具體的代碼的邏輯來進行分析,事實上咱們也不清楚這個裏面具體的內部實現,這裏僅僅是我的的觀點。架構
圖一 百度地圖主界面app
注意下面全部的代碼並不是和上面的截圖一致,截圖僅供參考框架
如圖一所示,整個界面從功能主體上區分的話,就可以很好的分紅這幾個部分,左側是一個搜索區域,右邊是兩個功能區和一個我的信息區域,中間是地圖區域,這個是咱們在看完這個地圖以後第一眼就能想到的使用Prism可以構建的幾個模塊(Modules)。在定完整個系統能夠分爲哪幾個模塊以後咱們緊接着就要分析每個模塊包含哪些功能,並根據這些功能可以定義哪些接口,咱們能夠新建一個類庫,專門用於定義整個應用程序的接口,並放在單獨的類庫中,好比左側的地圖搜索區域咱們能夠定義一個IMapSearch的接口,用於定於這個部分有哪些具體的功能,以下面代碼所示。 異步
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows;
namespace IGIS.SDK { public delegate List<Models.SearchResult> OnMapSearchHandle(string keyword); public interface IMapSearch { void AddSearchListener(string type, OnMapSearchHandle handle); void RemoveSearchListener(string type); void ShowResults(List<Models.SearchResult> results); void ClearResults(); System.Collections.ObjectModel.ObservableCollection<Models.SearchResult> GetAllResults(); event EventHandler<string> OnSearchCompleted; event EventHandler<System.Collections.ObjectModel.ObservableCollection<Models.SearchResult>> OnClearSearchResult; event EventHandler<System.Collections.ObjectModel.ObservableCollection<Models.SearchResult>> OnExecuteMultiSelected; void ShowFloatPanel(Models.SearchResult targetResult, FrameworkElement ui); } }
這是第一步,爲左側的搜索區域定義好接口,固然模塊化的設計必然包括界面和界面抽象,即WPF中的View層和ViewModel層以及Model層,咱們能夠單獨新建一個項目(自定義控件庫爲佳)來單獨實現這一部分的MVVM,而後生成單獨的DLL供主程序去調用,好比新建一個自定義空間庫命名爲Map.SearchModule,而後分別設計這幾個部分,這裏列出部分代碼僅供參考。ide
<UserControl x:Class="IGIS.MapSearch" 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" Title="IGIS" xmlns:cvt="clr-namespace:IGIS.Utils" xmlns:gisui="clr-namespace:IGIS.UI;assembly=IGIS.UI" xmlns:region="http://www.codeplex.com/CompositeWPF" xmlns:ui="clr-namespace:X.UI;assembly=X.UI" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" d:DesignHeight="600" d:DesignWidth="1100"> <Grid> ...... </Grid> </UserControl>
固然最重要的部分代碼都是在ViewModel層中去實現的,這個層必需要繼承自IMapSearch這個接口,而後和View層經過DataContext綁定到一塊兒,這樣一個完整的模塊化的雛形就出來了,後面還有幾個重要的部分再一一講述。模塊化
using IGIS.SDK.Models; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using System.Collections.ObjectModel; using X; using X.Infrastructure; namespace IGIS.ViewModels { class SearchManager : X.Infrastructure.VMBase, IGIS.SDK.IMapSearch { public SearchManager() { Search = new Microsoft.Practices.Prism.Commands.DelegateCommand(DoSearch); ClearResult = new Microsoft.Practices.Prism.Commands.DelegateCommand(DoClearResult); ShowSelected = new Microsoft.Practices.Prism.Commands.DelegateCommand(DoShowSelected); Listeners.Add(new Listener { Name = "所有", Handle = null }); } private void DoShowSelected() { if (null != OnExecuteMultiSelected) { System.Collections.ObjectModel.ObservableCollection<SearchResult> selected = new ObservableCollection<SearchResult>(); foreach (var itm in SelectedItems) { if (itm is SearchResult) selected.Add(itm as SearchResult); } OnExecuteMultiSelected(this, selected); } } private static SearchManager _instance; public static SearchManager Instance { get { if (null == _instance) _instance = new SearchManager(); return _instance; } set { _instance = value; } } private void DoSearch() { ClearResults(); foreach (var ls in Listeners) { if (string.IsNullOrEmpty(SelectedType) || SelectedType == "所有" || SelectedType == ls.Name) if (ls.Handle != null) { List<SearchResult> res = null; Application.Current.Dispatcher.Invoke(new Action(() => { res = ls.Handle.Invoke(Keyword); }), System.Windows.Threading.DispatcherPriority.Normal); if (null != res && res.Count > 0) { foreach (var itm in res) { Application.Current.Dispatcher.Invoke(new Action(() => { Results.Add(itm); })); } } } } if (null != OnSearchCompleted) OnSearchCompleted(Results, Keyword); DoRemoteSearch(SelectedType, Keyword); } private string _keyword; public string Keyword { get { return _keyword; } set { if (_keyword != value) { _keyword = value; OnPropertyChanged("Keyword"); } } } private string _selectedType = "所有"; public string SelectedType { get { return _selectedType; } set { if (_selectedType != value) { _selectedType = value; OnPropertyChanged("SelectedType"); } } } private ICommand _showSelected; public ICommand ShowSelected { get { return _showSelected; } set { _showSelected = value; } } private ICommand _search; public ICommand Search { get { return _search; } set { if (_search != value) { _search = value; OnPropertyChanged("Search"); } } } private ICommand _ClearResult; public ICommand ClearResult { get { return _ClearResult; } set { _ClearResult = value; } } private void DoClearResult() { ClearResults(); } private System.Collections.ObjectModel.ObservableCollection<SearchResult> _results = new System.Collections.ObjectModel.ObservableCollection<SearchResult>(); public System.Collections.ObjectModel.ObservableCollection<SearchResult> Results { get { return _results; } set { if (_results != value) { _results = value; OnPropertyChanged("Results"); } } } private System.Collections.IList _selectedItems; public System.Collections.IList SelectedItems { get { return _selectedItems; } set { _selectedItems = value; } } #region SDK public class Listener : X.Infrastructure.NotifyObject { private string _name; public string Name { get { return _name; } set { if (_name != value) { _name = value; OnPropertyChanged("Name"); } } } private SDK.OnMapSearchHandle _handle; public SDK.OnMapSearchHandle Handle { get { return _handle; } set { _handle = value; } } } public event EventHandler<string> OnSearchCompleted; public event EventHandler<System.Collections.ObjectModel.ObservableCollection<SDK.Models.SearchResult>> OnClearSearchResult; public event EventHandler<ObservableCollection<SearchResult>> OnExecuteMultiSelected; private System.Collections.ObjectModel.ObservableCollection<Listener> _listeners = new System.Collections.ObjectModel.ObservableCollection<Listener>(); public System.Collections.ObjectModel.ObservableCollection<Listener> Listeners { get { return _listeners; } set { if (_listeners != value) { _listeners = value; OnPropertyChanged("Listeners"); } } } public System.Collections.ObjectModel.ObservableCollection<SearchResult> GetAllResults() { return Results; } public void AddSearchListener(string type, SDK.OnMapSearchHandle handle) { Application.Current.Dispatcher.Invoke(new Action(() => { var itm = Listeners.Where(x => x.Name == type).SingleOrDefault() ?? null; if (null == itm) { itm = new Listener() { Name = type }; Listeners.Add(itm); } itm.Handle = handle; })); } public void RemoveSearchListener(string type) { Application.Current.Dispatcher.Invoke(new Action(() => { try { var itm = Listeners.Where(x => x.Name == type).SingleOrDefault() ?? null; if (null != itm) { Listeners.Remove(itm); } } catch (Exception) { } })); } public void ShowResults(List<SearchResult> results) { ClearResults(); foreach (var itm in results) { Application.Current.Dispatcher.Invoke(new Action(() => { Results.Add(itm); })); } } public void ClearResults() { Application.Current.Dispatcher.Invoke(new Action(() => { if (null != OnClearSearchResult && Results.Count > 0) OnClearSearchResult(this, Results); Results.Clear(); ClearRemoteResults(); })); } public void ShowFloatPanel(SearchResult targetResult, FrameworkElement ui) { if (null != OnShowFloatPanel) OnShowFloatPanel(targetResult, ui); } internal event EventHandler<FrameworkElement> OnShowFloatPanel; #endregion #region 大屏端同步命令 void DoRemoteSearch(string type, string keyword) { X.Factory.GetSDKInstance<X.IDataExchange>().Send(new IGIS.SDK.Messages.RemoteMapSearchMessage() { SelectedType = this.SelectedType, Keyword = this.Keyword }, "IGISMapSearch"); } void ClearRemoteResults() { X.Factory.GetSDKInstance<X.IDataExchange>().Send(new X.Messages.MessageBase(), "IGISClearMapSearch"); } #endregion } }
若是熟悉Prism的開發者確定知道這部分能夠完整的定義爲一個Region,在完成這部分以後,最重要的部分就是將當前的實現接口IGIS.SDK.IMapSearch的對象注入到UnityContainer中從而在其餘的Module中去調用,這樣就可以實現不一樣的模塊之間進行通訊,具體注入的方法請參考下面的代碼。 ui
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Practices.Unity; using X; namespace IGIS { public class IGISProductInfo : IModule { Microsoft.Practices.Prism.Regions.IRegionViewRegistry m_RegionViewRegistry; public IGISProductInfo(Microsoft.Practices.Unity.IUnityContainer container) { m_RegionViewRegistry = _RegionViewRegistry; container.RegisterInstance<IGIS.SDK.IMapSearch>(ViewModels.SearchManager.Instance); } public void Initialize() { m_RegionViewRegistry.RegisterViewWithRegion(「MapSearchRegion」, typeof(Views.IGIS.MapSearch)); } }
}
首先咱們經過m_RegionViewRegistry.RegisterViewWithRegion(「MapSearchRegion」, typeof(Views.IGIS.MapSearch))來將當前的View註冊到主程序的Shell中,在主程序中咱們只須要經過<ContentControl region:RegionManager.RegionName="MapSearchRegion"></ContentControl>就可以將當前的View放到主程序的中,從而做爲主程序的界面的一部分,而後經過代碼:container.RegisterInstance<IGIS.SDK.IMapSearch>(ViewModels.SearchManager.Instance),就可以將當前實現IMapSearch的接口的實例注入到Prism框架的全局的UnityContainer中,最後一步也是最關鍵的就是在其它的模塊中,若是咱們須要調用當前實現IMapSearch的接口的方法,那該怎麼來獲取到實現這個接口的實例呢?this
下面的代碼提供了兩個方法,一個同步方法和一個異步的方法來獲取當前的實例,好比使用同步的方法,咱們調用GetSDKInstance這個方法傳入類型:IGIS.SDK.IMapSearch時就可以獲取到注入到容器中的惟一實例:ViewModels.SearchManager.Instance,這樣咱們就可以獲取到這個實例了。
public static T GetSDKInstance<T>() where T : class { if (currentInstances.ContainsKey(typeof(T))) return currentInstances[typeof(T)] as T; try { var instance = Microsoft.Practices.ServiceLocation.ServiceLocator.Current.GetInstance<T>(); currentInstances[typeof(T)] = instance; return instance; } catch (Exception ex) { System.Diagnostics.Trace.TraceError(ex.ToString()); return null; } } private static object Locker = new object(); public static void GetSDKInstanceAysnc<T>(Action<T> successAction) where T : class { if (currentInstances.ContainsKey(typeof(T))) { successAction.Invoke(currentInstances[typeof(T)] as T); return; } Task.Factory.StartNew(new Action(() => { lock (Locker) { T instance = null; int tryCount = 0; while (instance == null && tryCount <= 100) { tryCount++; try { instance = Microsoft.Practices.ServiceLocation.ServiceLocator.Current.GetInstance<T>(); } catch { } if (null != instance) { currentInstances[typeof(T)] = instance; successAction.Invoke(instance); return; } else { System.Threading.Thread.Sleep(50); } } } })); }
在看完上面的介紹以後咱們彷佛對基於Prism的模塊化開發思路有了必定的理解了,可是這些模塊是在什麼時候進行加載的呢?Prism框架是一種預加載模式,即生成的每個Module在主程序Shell初始化的時候就會去加載每個繼承自IModule的接口的模塊,固然這些模塊是分散在程序的不一樣目錄中的,在加載的時候須要爲其指定具體的目錄,這樣在主程序啓動時就會加載不一樣的模塊,而後每一個模塊加載時又會將繼承自特定接口的實例註冊到一個全局的容器中從而供不一樣的模塊之間相互調用,從而實現模塊之間的相互調用,同理圖一中的功能區、我的信息區、地圖區都可以經過繼承自IModule接口來實現Prism框架的統一管理,這樣整個軟件就能夠分紅不一樣的模塊,從而彼此獨立最終構成一個複雜的系統,固然這篇文章只是作一個大概的分析,爲對Prism框架有必定理解的開發者能夠有一個指導思想,若是想深刻了解Prism的思想仍是得經過官方的參考代碼去一點點理解其指導思想,同時若是須要對Prism有更多的理解,也能夠參考我以前的博客,本人也將一步步完善這個系列。
最後咱們要看看主程序如何在初始化的時候來加載這些不一樣的模塊的dll的,請參考下面的代碼:
using System; using System.Windows; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Practices.Prism.Modularity; using Microsoft.Practices.Unity; using Microsoft.Practices.Prism.UnityExtensions; using Microsoft.Practices.Prism.Logging; namespace Dvap.Shell.CodeBase.Prism { public class DvapBootstrapper : Microsoft.Practices.Prism.UnityExtensions.UnityBootstrapper { private readonly string[] m_PluginsFolder=new string[3] { "FunctionModules", "DirectoryModules", "Apps"}; private readonly CallbackLogger m_callbackLogger = new CallbackLogger(); #region Override /// <summary> /// 建立惟一的Shell對象 /// </summary> /// <returns></returns> protected override DependencyObject CreateShell() { return this.Container.TryResolve<Dvap.Shell.Shell>(); } protected override void InitializeShell() { base.InitializeShell(); Application.Current.MainWindow = (Window)this.Shell; Application.Current.MainWindow.Show(); } /// <summary> /// 建立惟一的Module的清單 /// </summary> /// <returns></returns> protected override IModuleCatalog CreateModuleCatalog() { return new CodeBase.Prism.ModuleCatalogCollection(); } /// <summary> /// 配置惟一的ModuleCatalog,這裏咱們經過從特定的路徑下加載 /// dll /// </summary> protected override void ConfigureModuleCatalog() { try { var catalog = ((CodeBase.Prism.ModuleCatalogCollection)ModuleCatalog); foreach (var pluginFolder in m_PluginsFolder) { if (pluginFolder.Contains("~")) { DirectoryModuleCatalog catApp = new DirectoryModuleCatalog() { ModulePath = pluginFolder.Replace("~", AppDomain.CurrentDomain.BaseDirectory) }; catalog.AddCatalog(catApp); } else { if (!System.IO.Directory.Exists(@".\" + pluginFolder)) { System.IO.Directory.CreateDirectory(@".\" + pluginFolder); } foreach (string dic in System.IO.Directory.GetDirectories(@".\" + pluginFolder)) { DirectoryModuleCatalog catApp = new DirectoryModuleCatalog() { ModulePath = dic }; catalog.AddCatalog(catApp); } } } } catch (Exception) { throw; } } protected override ILoggerFacade CreateLogger() { return this.m_callbackLogger; } #endregion } }
看到沒有每個宿主應用程序都有一個繼承自Microsoft.Practices.Prism.UnityExtensions.UnityBootstrapper的類,咱們須要重寫其中的一些方法來實現Prism程序的模塊加載,例如重寫 override void ConfigureModuleCatalog() 咱們的宿主程序就知道去哪裏加載這些繼承自IModule的dll,還有必須重載CreateShell和InitializeShell()
這些基類的方法來制定主程序的Window,有了這些咱們就可以構造一個完整的Prism程序了,對了還差最後一步,就是啓動Prism的Bootstrapper,咱們通常是在WPF程序的App.xaml.cs中啓動這個,例如:
using System; using System.Collections.Generic; using System.Configuration; using System.Data; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; namespace Dvap.Shell { /// <summary> /// App.xaml 的交互邏輯 /// </summary> public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); new CodeBase.Prism.DvapBootstrapper().Run(); this.DispatcherUnhandledException += new System.Windows.Threading.DispatcherUnhandledExceptionEventHandler(App_DispatcherUnhandledException); AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); } private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { try { if (e.ExceptionObject is System.Exception) { WriteLogMessage((System.Exception)e.ExceptionObject); } } catch (Exception ex) { WriteLogMessage(ex); } } private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { try { WriteLogMessage(e.Exception); e.Handled = true; } catch (Exception ex) { WriteLogMessage(ex); } } public static void WriteLogMessage(Exception ex) { //若是不存在則建立日誌文件夾 if (!System.IO.Directory.Exists("Log")) { System.IO.Directory.CreateDirectory("Log"); } DateTime now = DateTime.Now; string logpath = string.Format(@"Log\Error_{0}{1}{2}.log", now.Year, now.Month, now.Day); System.IO.File.AppendAllText(logpath, string.Format("\r\n************************************{0}*********************************\r\n", now.ToString("yyyy-MM-dd HH:mm:ss"))); System.IO.File.AppendAllText(logpath, ex.Message); System.IO.File.AppendAllText(logpath, "\r\n"); System.IO.File.AppendAllText(logpath, ex.StackTrace); System.IO.File.AppendAllText(logpath, "\r\n"); System.IO.File.AppendAllText(logpath, "\r\n*************************************************r\n"); } } }
在應用程序啓動時調用 new CodeBase.Prism.DvapBootstrapper().Run()啓動Prism應用程序,從而完成整個過程,固然上面的講解只可以說明Prism的冰山一角,瞭解好這個框架將爲咱們開發複雜的應用程序提供一種新的思路。