很早就想写这么一篇文章来对近几年使用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的冰山一角,了解好这个框架将为咱们开发复杂的应用程序提供一种新的思路。