很早就想寫這么一篇文章來對近幾年使用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的冰山一角,了解好這個框架將為我們開發復雜的應用程序提供一種新的思路。
