CloudNotes之桌面客戶端篇:插件系統的實現

CloudNotes版本更新歷史與各版本下載地址請點擊此處html

CloudNotes中文系列文章彙總列表請點擊此處git

查看CloudNotes源代碼請點擊此處程序員

有時候,同一個名詞,針對不一樣的人羣,應該採用不一樣的表達方式。好比插件的概念,對於程序員而言,能夠將其稱爲插件,或者擴展。對於用戶而言,或許「擴展功能」一詞會更加貼切。本文仍是脫離不了碼農的氣質,繼續討論技術問題,所以,我會以「插件」一詞進行描述。github

概述

1.0.5504.38654版本開始,CloudNotes桌面客戶端能夠支持插件了。不只該版本默認附帶了三個插件,並且開發人員還能很是方便地使用Visual Studio 2013/2015,基於.NET Framework 4.5.1,爲CloudNotes開發本身的插件。本文將首先介紹插件系統的設計。須要注意的是,目前插件部分的實現,僅僅是針對CloudNotes的桌面客戶端,從此隨着CloudNotes的不斷更新和改進,可能會添加諸如Windows Phone、Web等客戶端,這些客戶端不在本文討論範圍以內。web

有圖有真相。先看幾張新版本的截圖吧。首先是在「工具」菜單中多出了一個「從網頁導入」的菜單項。經過該菜單項,用戶能夠直接輸入須要導入的頁面的URL地址,而後CloudNotes會將此頁面保存爲當前用戶的一條筆記。筆記標題即爲頁面的標題,若是沒法識別頁面標題,或者標題已經存在,該功能還會提示用戶指定一個新的標題:api

image

其次,在「文件」菜單中出現了「另存爲」菜單項。別小看這個普普統統的「另存爲」,它但是以插件的形式實現的。緩存

image

點擊此菜單項,會打開標準的「另存爲」對話框,用來將當前打開的筆記保存到本地磁盤文件。至於以何種格式的文件進行保存,就是由插件來實現了。在1.0.5504.38654版本中,默認自帶的兩個導出型插件,能夠分別將當前筆記保存爲純文本格式和HTML格式。安全

此外,CloudNotes桌面客戶端還爲插件配置提供了豐富的界面,好比用戶能夠本身決定如何在「工具」菜單中顯示插件菜單項,還能夠對每一個插件進行單獨配置:服務器

image

接下來就讓咱們一塊兒看看,在CloudNotes中,插件系統是如何設計實現的。app

設計與實現

首先能夠考慮到的是,既然咱們要爲應用程序開發插件,那麼咱們就須要可以經過一套機制,更確切地說是一套接口,將一部分應用程序的功能委託給插件進行處理,這樣作的理由是顯而易見的。好比,在CloudNotes桌面客戶端中,會爲插件提供導入筆記的接口,插件只須要從另外一些途徑得到了筆記的內容,就可使用這個接口將筆記導入到CloudNotes中;其次,插件是能夠動態加載的,這也是插件的基本特性之一,那麼插件的加載和管理,就成爲插件系統實現的另外一個話題;再次,插件是能夠配置的,咱們的設計須要對插件的可配置性進行考慮,這就須要包含如下三個內容:一、要可以方便地提供插件配置界面,二、要可以方便地打開或保存插件的配置信息,三、要可以爲插件的開發提供配置系統的應用程序接口。下面就從這三個方面出發,詳細講解CloudNotes桌面客戶端中插件系統的設計與實現。

插件的分類

相信你已經從上面的截圖中瞭解到,在CloudNotes桌面客戶端中,插件分爲兩種類型:工具型和導出型。導出型功能相對單一:只負責將當前筆記導出成一個本地文件,而工具型插件所能實現的功能就比較多了。

image

技術上看,兩種插件類型都是Extension抽象類的子類,在後續的系列文章中,我會詳細介紹如何使用CloudNotes桌面客戶端的擴展框架,分別開發工具型和導出型插件。

Shell

Shell是CloudNotes桌面客戶端插件系統所引入的第一個概念,Shell在計算機領域中的英文本意是「外殼程序」的意思,在此表述了這樣一種機制:它提供了應用程序的基礎功能,卻隱含了另外一部分的具體實現。這個概念聽起來有點抽象,然而在咱們的插件系統中,全部的插件操做對象,就是這個Shell。

CloudNotes.DesktopClient.Extensibility命名空間下,定義了IShell接口,它表述全部實現了該接口的類,均是CloudNotes桌面客戶端的外殼程序。在該接口中,咱們定義了可以提供給插件所使用的各類屬性與方法,好比ImportNote方法,它能夠實現筆記的導入,還有Note屬性,它包含了當前選中的筆記的內容。下面的類圖描述了IShell接口以及與之相關的類之間的關係:

image

因而可知,CloudNotes桌面客戶端的主窗體FrmMain就是一個殼(Shell),而插件的Execute方法會使用IShell接口進行所需的操做,也就是說,插件會調用FrmMain中由IShell接口定義的屬性和方法。

每當FrmMain窗體初始化的時候,它會調用InitializeExtensions方法對全部插件進行初始化,主要工做就是生成菜單項,若是當前所加載的插件是導出型插件的話,在該方法中就會針對「另存爲」對話框生成相應的文件格式過濾器。從下面這段代碼能夠看到,當某個插件菜單項被點擊的時候,與該菜單項關聯的插件將被執行,而當前FrmMain窗體的實例,則會以Execute方法的參數形式傳遞給插件執行邏輯。

extensionToolStrip.Click +=
    (s, e) => { SafeExecutionContext.Execute(this, () => toolExtension.Execute(this)); };

Shell實際上是一個比較老的概念,根據維基百科中所述,計算機中的Shell是指操做系統提供的一組用戶界面(能夠是文本的,也能夠是圖形化的),用戶經過這些界面與操做系統交互。在CloudNotes桌面客戶端中,借用了這個概念,爲插件提供了與CloudNotes桌面客戶端的交互界面。

插件管理器(Extension Manager)

在CloudNotes桌面客戶端中,插件是由插件管理器負責加載並管理的。插件管理器的功能其實很簡單,主要部分就是插件的裝載:掃描指定路徑下全部的DLL文件,發現若是是合理的.NET程序集,而且其中包含插件類型定義時,就會使用反射,獲取插件類型並實例化插件,最後將其保存在本地的一個字典集合裏以備使用。這部分代碼被定義在CloudNotes.DesktopClient.Extensibility命名空間下的ExtensionManager類中,相對仍是比較簡單的:

public void Load()
{
    var extensionFiles = Directory.EnumerateFiles(this.path, Constants.ExtensionFileSearchPattern,
        SearchOption.AllDirectories);
    foreach (var extensionFile in extensionFiles)
    {
        try
        {
            var assembly = Assembly.LoadFrom(extensionFile);
            foreach (var type in assembly.GetExportedTypes())
            {
                try
                {
                    if (type.IsDefined(typeof (ExtensionAttribute)) &&
                        type.IsSubclassOf(typeof (Extension)))
                    {
                        var extensionLoaded = (Extension) Activator.CreateInstance(type);
                        this.OnExtensionLoaded(extensionLoaded.Name);
                        this.extensions.Add(extensionLoaded.ID, extensionLoaded);
                    }
                }
                catch
                {
                }
            }
        }
        catch
        {
        }
    }
}

在此就不對這部分代碼做過多解釋了。引入插件管理器的一個最大好處就是,能夠保證插件在整個CloudNotes桌面客戶端的生命週期中只被裝載一次,而且可以很方便地在多個組件之間共享:

  • FrmMain窗體:須要經過插件管理器將所加載的插件顯示到用戶界面,並觸發插件的執行
  • FrmAbout窗體:須要經過插件管理器獲取所加載的插件的詳細信息,並顯示給用戶
  • FrmSettings窗體:須要經過插件管理器獲取所加載插件的配置信息,併爲用戶提供插件配置的功能
  • SingleInstanceController:在該類型中初始化插件管理器,並加載插件

接下來的話題就是,什麼時候啓動插件的加載過程?插件的加載其實有不少種方式,相對而言,如下兩種方式最爲簡單常見:

  • 提供一個啓動界面,在應用程序啓動的時候加載插件,並在啓動界面上顯示加載狀況:這種方式最爲常見,實現也很簡單。不少應用程序都使用這種方式來完成應用程序初始化和插件加載的過程。然而對於CloudNotes來講,並無採起這種方式。首先,單獨設定一個啓動界面會讓用戶感受到CloudNotes桌面客戶端很「大」(或者說很「重」),顯得並不輕量,由於每每都是一些大型的應用程序(Word、Excel、Photoshop、AutoCAD等等)纔會有這樣一個專業的啓動界面;其次,老版本的CloudNotes桌面客戶端沒有提供啓動界面,忽然出現一個啓動界面會讓老用戶感受突兀(別笑我,估計也沒幾個老用戶,但這也是作應用程序設計和開發的時候必須考慮的一個因素);再次,對於CloudNotes桌面客戶端而言,插件的加載過程仍是至關快速的,一方面並無成千上萬的插件須要加載,另外一方面,插件的加載邏輯也相對簡單,因此插件加載過程應該是一個秒級的操做,引入啓動界面倒還顯得多餘。所以,CloudNotes桌面客戶端採用了下面一條所述的方式
  • 在某個長時操做的同時加載插件:關鍵是選擇一個長時操做的時間點,從該時間點開始,異步地將插件加載到內存中。經過簡單分析,不難發如今CloudNotes桌面客戶端中,登陸界面就是一個長時操做,在這個界面下,CloudNotes桌面客戶端會等待用戶輸入用戶名和密碼,在點擊「肯定」按鈕後,會聯繫服務器進行登陸認證。整個操做過程所花費的時間將遠遠大於插件的加載時間,所以,在登陸界面啓動時,異步加載插件是瓜熟蒂落的事

CloudNotes桌面客戶端登陸界面的啓動是由LoginProvider負責的,而SingleInstanceController使用LoginProvider完成用戶登陸功能。SingleInstanceController確保在系統中僅有一個CloudNotes桌面客戶端的實例在運行,這在從此我會介紹。SingleInstanceController的OnCreateMainForm重載方法實現了擴展加載的邏輯:

protected override void OnCreateMainForm()
{
    var extensionManager = new ExtensionManager();
    var settings = DesktopClientSettings.ReadSettings();
    var loadExtensionTask = Task.Factory.StartNew(() =>
    {
        // As the extensions are loaded in another thread, setting that thread's ui culture
        // to the one read from the setting preference.
        Thread.CurrentThread.CurrentUICulture = new CultureInfo(settings.General.Language);
        extensionManager.Load();
    });


    Thread.CurrentThread.CurrentUICulture = new CultureInfo(settings.General.Language);

    var credential = LoginProvider.Login(Application.Exit, settings);
    if (credential != null)
    {
        Task.WaitAll(loadExtensionTask);
        // Instantiate your main application form
        this.MainForm = new FrmMain(credential, settings, extensionManager);
    }
}

在上面的代碼中,使用了.NET並行庫TPL的Task Factory來啓動一個並行任務,在任務的執行體中,調用extensionManager的Load方法,開始加載插件。接下來,就會由LoginProvider負責用戶的登陸過程,當登陸過程結束以後,當前線程會阻塞在Task.WaitAll這行代碼,等待插件徹底加載完成,最後就會初始化並啓動FrmMain。根據上面的分析,一般狀況下插件加載過程是很快的,所以,事實上99%的狀況下,此處Task.WaitAll調用並不會阻塞,用戶體驗仍然那麼流暢,不耽誤事兒。

經過這部份內容,咱們能夠了解到,軟件開發過程須要綜合性地考慮不少事情,不只僅是將關注點放在功能上,那些非功能性需求也無時不刻地須要咱們的「關懷」,而且,這些看似不起眼的非功能性需求,每每又是開發的難點(好比高性能需求、嚴格的安全認證機制等),甚至直接影響項目和產品的成敗。

插件配置

爲CloudNotes桌面客戶端插件提供靈活的、可擴展的插件配置系統,是插件這個課題的難點。由於不只須要考慮到最終用戶的體驗,並且還要考慮到插件開發人員的感覺。所以,CloudNotes特別提供了插件配置框架,保證可以體現插件配置系統的上述兩種職能。image

也正如上文截圖中所示,在CloudNotes桌面客戶端的標準配置界面中,新增了「擴展功能」選項卡,它列出了目前加載的全部插件,當用戶單擊左邊的插件時,與之相關的配置界面會顯示在對話框的右邊部分。由此分析,配置界面應該是插件的一個屬性。既然有了配置界面,就須要有與之對應的配置數據,更進一步,插件開發人員還應該可以爲插件提供默認的配置數據,例如咱們可讓用戶選擇筆記保存所使用的編碼(Encoding),但默認應該使用UTF-8的Encoding。討論到此處,MVC模式慢慢浮現出來,或許咱們能夠借用MVC模式的概念,並提供一個機制,可以將配置數據綁定到配置界面,或者從配置界面把配置數據收集起來以便保存到配置文件。爲了將「配置」的關注點從插件上分離開來,CloudNotes插件配置框架引入了「插件配置供應器」(Extension Setting Provider)的概念,它包含了配置界面、配置數據、默認配置的信息,而且提供從配置界面收集配置數據和將配置數據綁定到配置界面的方法。

CloudNotes.DesktopClient.Extensibility命名空間下ExtensionSettingProvider類就是插件配置供應器,它的代碼在此就再也不重複了。

ExtensionAttribute特性中,包含了指定ExtensionSettingProvider的構造函數重載,當ExtensionAttribute被應用到Extension的子類時,Extension的SettingProvider屬性就會根據ExtensionAttribute特性中指定的ExtensionSettingProvider類型來獲取它的實例,進而完成了插件對配置框架的聚合,也爲CloudNotes桌面客戶端訪問插件配置提供了橋樑。不過值得一提的是,Extension類的SettingProvider屬性採用了一種相似緩存的機制,僅作一次ExtensionSettingProvider的初始化,這是由於ExtensionSettingProvider有可能會保持那些用戶改變過但沒有保存的配置數據。

接下來的事情就比較簡單了,在FrmSettings窗體代碼中,BindExtension方法會判斷當前選中的插件是否指定了插件配置供應器,若是沒有指定,則簡單地初始化一個NoSettingsControl實例,將其顯示在界面右側,告知用戶「該擴展功能未提供任何可供設置的選項」。不然,將插件配置供應器所提供的用戶界面控件顯示在界面上,並查看本地字典緩存中是否有已經更改過的配置數據。如有,則將該數據綁定到界面上,不然就將從配置文件中讀入配置信息(若無,則取默認配置信息),並綁定到用戶界面上。

private void BindExtension(Guid extensionId)
{
    var extension = this.extensionManager.GetByKey(extensionId);
    pnlSettings.Controls.Clear();
    if (extension.SettingProvider == null)
    {
        var noSettingsControl = new NoSettingsControl();
        noSettingsControl.Dock = DockStyle.Fill;
        pnlSettings.Controls.Add(noSettingsControl);
    }
    else
    {
        pnlSettings.Controls.Add(extension.SettingProvider.SettingControl);
        if (cachedSettings.ContainsKey(extensionId))
        {
            extension.SettingProvider.BindSetting(cachedSettings[extensionId]);
        }
        else
        {
            extension.SettingProvider.BindSetting(extension.SettingProvider.ExtensionSetting);
        }
    }
}

上面代碼中cachedSettings字典保存了用戶這次打開系統配置窗體後,對插件所作的配置更改,這是爲了可以在用戶瀏覽各個插件配置的過程當中,保持每一個插件以前更改過的配置值。當界面左邊所選中的插件發生變化時,窗體會清除以前所選插件的配置界面,並顯示當前所選插件的配置界面。而在清除以前所選插件的配置界面時,該插件的相關配置信息將會被緩存下來:

private void lvExtensions_SelectedIndexChanged(object sender, EventArgs e)
{
    if (this.lvExtensions.SelectedItems.Count > 0)
    {
        var item = this.lvExtensions.SelectedItems[0];
        var extensionId = (Guid) item.Tag;
        this.BindExtension(extensionId);
    }
}

private void pnlSettings_ControlRemoved(object sender, ControlEventArgs e)
{
    if (e.Control.Tag != null)
    {
        var extension = e.Control.Tag as Extension;
        if (extension != null && extension.SettingProvider != null)
        {
            var setting = extension.SettingProvider.CollectedSetting;
            this.cachedSettings[extension.ID] = setting;
        }
    }
}

最後,當用戶單擊「肯定」按鈕時,全部插件的配置數據將被保存下來:

foreach (var extension in this.extensionManager.AllExtensions)
{
    var settingProvider = extension.Value.SettingProvider;
    if (settingProvider != null)
    {
        settingProvider.PersistSettings();
    }
}

在下一篇關於插件的開發文章中,我還會詳細介紹如何爲自定義的插件設計並實現配置功能。相信到那時讀者朋友應該對插件系統的實現會有個更好的瞭解。

總結

本文首先展現了CloudNotes桌面客戶端新版本對插件系統的支持,而後簡單介紹了CloudNotes桌面客戶端中插件的分類,並經過Shell、插件管理器和插件配置三個部分,對插件系統的設計與實現進行了必要的介紹。篇幅有限,沒有辦法在文章中對技術實現的每一個細節進行完美解釋,讀者能夠在參考源代碼的同時閱讀本文,若有問題能夠直接留言。在接下來的文章中,我會介紹如何使用Visual Studio 2013/2015開發和調試CloudNotes桌面客戶端的插件。相信到那時候,讀者對插件系統的設計與實現會有更深的認識。

相關文章
相關標籤/搜索