很早就想寫這麼一篇文章來對近幾年使用Prism架構來設計軟體來做一次深入的分析了,但直到最近才開始整理,說到軟體系統的設計這裡面有太多的學問,隻有經過大量的探索才能夠設計出好的軟體産品,就本人的了解,一個好的軟體必須有良好的設計,這其中包括:易閱讀、易擴充、低耦合、子產品化等等,如果你想設計一個好的系統當然還要考慮更多的方面,本篇文章主要是基于微軟的Prism架構來談一談構模組化塊化、熱插拔軟體系統的思路,這些都是經過大量項目實戰的結果,希望能夠通過這篇文章的梳理能夠對建構軟體系統有更加深刻的了解。
首先要簡單介紹一下Prism這個架構:Prism架構通過功能子產品化的思想,将複雜的業務功能和UI耦合性進行分離,通過子產品化,來最大限度的降低耦合性,很适合我們進行類似插件化的思想來組織系統功能,并且子產品之間,通過釋出和訂閱事件來完成資訊的通信,而且其開放性支援多種架構內建。通過這些簡單的介紹就能夠對此有一個簡單的了解,這裡面加入了兩種依賴注入容器,即:Unity和MEF兩種容器,在使用的時候我們首先需要确定使用何種容器,這個是第一步。第二步就是如何建構一個成熟的子產品化軟體,這個部分需要我們能夠對整個軟體系統功能上有一個合理的拆分,隻有真正地完全了解整個系統才能夠合理抽象Module,然後降低Module之間的耦合性。第三步就是關于子產品之間是如何進行通訊的,這一部分也是非常重要的部分,今天這篇文章就以Prism的Unity依賴注入容器為例來說明如何構模組化塊化軟體系統,同時也簡要說明一下軟體系統的建構思路。
這裡以百度地圖為例來說一下如果使用WPF+Prism的架構來設計的話,該怎樣來設計,當然這裡隻是舉一個例子,當然這篇文章不會就裡面具體的代碼的邏輯來進行分析,事實上我們也不清楚這個裡面具體的内部實作,這裡僅僅是個人的觀點。

圖一 百度地圖主界面
注意下面所有的代碼并非和上面的截圖一緻,截圖僅供參考
如圖一所示,整個界面從功能主體上區分的話,就能夠很好的分成這幾個部分,左側是一個搜尋區域,右邊是兩個功能區和一個個人資訊區域,中間是地圖區域,這個是我們在看完這個地圖之後第一眼就能想到的使用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,然後分别設計這幾個部分,這裡列出部分代碼僅供參考。
<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中去調用,這樣就能夠實作不同的子產品之間進行通信,具體注入的方法請參考下面的代碼。
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的接口的方法,那該怎麼來擷取到實作這個接口的執行個體呢?
下面的代碼提供了兩個方法,一個同步方法和一個異步的方法來擷取目前的執行個體,比如使用同步的方法,我們調用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的冰山一角,了解好這個架構将為我們開發複雜的應用程式提供一種新的思路。