[C#]外掛程式設計框架 MAF 開發總結

2023-05-26 18:00:37

1. 什麼是MAF和MEF?

MEF和MEF微軟官方介紹:https://learn.microsoft.com/zh-cn/dotnet/framework/mef/

MEF是輕量化的外掛框架,MAF是複雜的外掛框架。

因為MAF有程序隔離和程式域隔離可選。我需要外掛程序隔離同時快速傳遞資料,最後選擇了MAF。

如果不需要真正的物理隔離還是建議使用簡單一點的MEF框架。

2. 如何學習MAF?

MAF其實是一項很老的技術,入門我看的是《WPF程式設計寶典》第32章 外掛模型。裡面有MAF和MEF的詳細介紹和許多樣例。

但是要深入理解還是看了很多其他的東西,下面我詳細說明,我自己理解和總結的MAF。

3. MAF框架入門

3.1 MAF框架構成與搭建

MAF框架模式是固定的,這裡做一個詳細介紹。

首先是要新增幾個新專案,下圖中不包含主專案。

Addin資料夾是放置外掛用的,其餘都是必要專案。

假設HostView專案和主專案的輸出路徑是..\Output\

然後修改每個專案的輸出資料夾,例如AddInSideAdapter專案輸出路徑可以設定為..\Output\AddInSideAdapters\

注意外掛專案輸出到Addin資料夾中的子資料夾是..\..\Output\AddIns\MyAddin\

最後專案的輸出資料夾結構是:

D:\Demo\Output\AddIns

D:\Demo\Output\AddInSideAdapters

D:\Demo\Output\AddInViews

D:\Demo\Output\Contracts

D:\Demo\Output\HostSideAdapters

來看看MAF框架模型構成。

 上圖中綠色的是被參照藍色專案所參照。例如HostSideAdapter就要參照Contract和Hostview,如下圖所示。

 注意參照時取消勾選複製本地。

 這時就完成基本專案結構的搭建。

3.2 MAF框架實現

這裡想實現宿主專案和外掛專案的雙向通訊。即外掛專案將相關函數介面在宿主實現,然後將宿主專案相關函數介面用委託類的方式註冊給外掛專案。實現雙向通訊。

用《WPF程式設計寶典》樣例程式碼來說,樣例中,外掛程式實現ProcessImageBytes處理影象資料的函數,處理同時需要向宿主專案報告處理進度,宿主中 ReportProgress函數實現進度視覺化。

MAF實現一般是先寫Contract協定,明確需要的函數介面。然後寫AddlnView和HostView。實際上這兩個是將函數介面抽象化,在介面裡函數複製過來前面加 public abstract 就行。

之後HostSideAdapter和AddInSideAdapter直接快速實現介面。

首先從Contract開始,Contract是定義介面,需要設定物件識別符號[AddInContract],且必須繼承IContract。

 [AddInContract]
    public interface IImageProcessorContract : IContract
    {
        byte[] ProcessImageBytes(byte[] pixels);

        void Initialize(IHostObjectContract hostObj);
    }

    public interface IHostObjectContract : IContract
    {
        void ReportProgress(int progressPercent);
    }

Initialize函數是提供宿主函數註冊的介面。

 然後在HostView和AddInView分別定義主程式和外掛程式的介面抽象類。

public abstract class ImageProcessorHostView
    {
        public abstract byte[] ProcessImageBytes(byte[] pixels);

        public abstract void Initialize(HostObject host);
    }

    public abstract class HostObject
    {
        public abstract void ReportProgress(int progressPercent);
    }

注意AddlnView需要設定物件識別符號[AddInBase]。

 [AddInBase]
    public abstract class ImageProcessorAddInView
    {
        public abstract byte[] ProcessImageBytes(byte[] pixels);

        public abstract void Initialize(HostObject hostObj);
    }

    public abstract class HostObject
    {
        public abstract void ReportProgress(int progressPercent);
    }

之後在HostSideAdapter實現抽象類。

注意HostSideAdapter繼承HostView的抽象類,在建構函式裡需設定ContractHandle外掛生存週期,ContractHandle不能為readonly。

  [HostAdapter]
    public class ImageProcessorContractToViewHostAdapter : HostView.ImageProcessorHostView
    {
        private Contract.IImageProcessorContract contract;
        private ContractHandle contractHandle;

        public ImageProcessorContractToViewHostAdapter(Contract.IImageProcessorContract contract)
        {            
            this.contract = contract;
            contractHandle = new ContractHandle(contract);
        }              

        public override byte[] ProcessImageBytes(byte[] pixels)
        {
            return contract.ProcessImageBytes(pixels);
        }

        public override void Initialize(HostView.HostObject host)
        {            
            HostObjectViewToContractHostAdapter hostAdapter = new HostObjectViewToContractHostAdapter(host);
            contract.Initialize(hostAdapter);
        }
    }

    public class HostObjectViewToContractHostAdapter : ContractBase, Contract.IHostObjectContract
    {
        private HostView.HostObject view;

        public HostObjectViewToContractHostAdapter(HostView.HostObject view)
        {
            this.view = view;
        }

        public void ReportProgress(int progressPercent)
        {
            view.ReportProgress(progressPercent);
        }        
    }

 

在AddInSideAdapter實現Contract介面,基本和HostSideAdapter類似,只是繼承的類不同。

[AddInAdapter]
    public class ImageProcessorViewToContractAdapter : ContractBase, Contract.IImageProcessorContract
    {
        private AddInView.ImageProcessorAddInView view;

        public ImageProcessorViewToContractAdapter(AddInView.ImageProcessorAddInView view)
        {
            this.view = view;
        }

        public byte[] ProcessImageBytes(byte[] pixels)
        {
            return view.ProcessImageBytes(pixels);
        }

        public void Initialize(Contract.IHostObjectContract hostObj)
        {            
            view.Initialize(new HostObjectContractToViewAddInAdapter(hostObj));            
        }
    }

    public class HostObjectContractToViewAddInAdapter : AddInView.HostObject
    {
        private Contract.IHostObjectContract contract;
        private ContractHandle handle;

        public HostObjectContractToViewAddInAdapter(Contract.IHostObjectContract contract)
        {
            this.contract = contract;
            this.handle = new ContractHandle(contract);            
        }
                
        public override void ReportProgress(int progressPercent)
        {
            contract.ReportProgress(progressPercent);
        }
    }

 

宿主專案中需要實現HostView裡HostObject抽象類。

 private class AutomationHost : HostView.HostObject
        {
            private ProgressBar progressBar;
            public AutomationHost(ProgressBar progressBar)
            {
                this.progressBar = progressBar;
            }
            public override void ReportProgress(int progressPercent)
            {
                // Update the UI on the UI thread.
                progressBar.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    (ThreadStart)delegate()
                {
                    progressBar.Value = progressPercent;
                }
                );                
            }
        }

 

然後是在宿主專案裡啟用外掛,並初始化AutomationHost。

string path = Environment.CurrentDirectory;            
AddInStore.Update(path);//更新目錄中Addins目錄裡的外掛
IList<AddInToken> tokens = AddInStore.FindAddIns(typeof(HostView.ImageProcessorHostView), path);//查詢全部外掛
lstAddIns.ItemsSource = tokens;//外掛視覺化
AddInToken token = (AddInToken)lstAddIns.SelectedItem;//選擇外掛
AddInProcess addInProcess = new AddInProcess();//建立外掛程序
addInProcess.Start();//啟用外掛程序
addin = token.Activate<HostView.ImageProcessorHostView>(addInProcess,AddInSecurityLevel.Internet);//啟用外掛
//如果只是想隔離程式域,就無需建立AddInProcess,啟用外掛如下
// HostView.ImageProcessorHostView addin = token.Activate<HostView.ImageProcessorHostView>(AddInSecurityLevel.Host);
automationHost = new AutomationHost(progressBar);//建立AutomationHost類
addin.Initialize(automationHost);//初始化automationHost

 

 外掛專案中實現AddInView中的抽象類。

[AddIn("Negative Image Processor", Version = "1.0", Publisher = "Imaginomics",Description = "")]
    public class NegativeImageProcessor : AddInView.ImageProcessorAddInView 
    {
        public override byte[] ProcessImageBytes(byte[] pixels)
        {
            int iteration = pixels.Length / 100;         
            for (int i = 0; i < pixels.Length - 2; i++)
            {
                pixels[i] = (byte)(255 - pixels[i]);
                pixels[i + 1] = (byte)(255 - pixels[i + 1]);
                pixels[i + 2] = (byte)(255 - pixels[i + 2]);
                if (i % iteration == 0)
                {
                    host?.ReportProgress(i / iteration);
                }
            }
            return pixels;
        }

        private AddInView.HostObject host;
        public override void Initialize(AddInView.HostObject hostObj)
        {
            host = hostObj;
        }

 

 這時宿主可以把資料傳遞給外掛程式,外掛程式中ProcessImageBytes處理資料然後通過host?.ReportProgress(i / iteration);向宿主傳遞訊息。

這裡有提供樣例程式。

專案附件.7z

4. MAF框架常見問題

4.1手動關閉外掛

AddInController addInController = AddInController.GetAddInController(addIn);
addInController.Shutdown();

此方法適應於非應用隔離的手動關閉。對於應用隔離式外掛,用此方法會丟擲異常。

如上面樣例就是應用隔離的外掛,可以根據程序id直接關閉程序。

public void ProcessClose()
        {
            try
            {
                if (process != null)
                {
                    Process processes = Process.GetProcessById(addInProcess.ProcessId);
                    if (processes?.Id > 0)
                    {
                        processes.Close();
                    }
                }
            }
            catch (Exception)
            {
            }
        }

4.2 外掛異常

System.Runtime.Remoting.RemotingException: 從 IPC 埠讀取時失敗: 管道已結束。這是外掛最常見的異常,因為外掛丟擲異常而使得外掛程式關閉。

如果是外掛呼叫非受控程式碼,而產生的異常,可以查Windows應用程式紀錄檔來確定異常。其餘能捕獲的異常儘量捕獲儲存到紀錄檔,方便檢視。

4.3 雙向通訊

實際應用過程中,往往是通過委託來將宿主相關函數暴露給一個類,然後通過在宿主程式初始化後。在外掛中範例化後就可以直接呼叫宿主的相關函數,反之同理。

這裡是通過委託暴露宿主的一個函數。

public delegate void UpdateCallBack(string message, bool isclose, int leve);
    public class VideoHost : HostAddInView
    {
        public event UpdateCallBack Updatecallback;
        public override void ProcessVideoCallBack(string message, bool isclose, int leve)
        {
            Updatecallback?.Invoke(message, isclose, leve);
        }
    }

 在外掛程式中範例化後呼叫。

private HostAddInView hostAddInView;
        public override void Initialize(HostAddInView hostAddInView)
        {
            this.hostAddInView = hostAddInView;
        }
        private void ErrorCallback(string message, bool isclose, int leve)
        {
            hostAddInView?.ProcessVideoCallBack(message, isclose, leve);
        }

5. MAF深入理解

 MAF本質是實現IpcChannel通訊,在一個期刊中有作者拋棄MAF固定結構自己實現IpcChannel,因為程式碼很複雜,就不在此詳細闡述。

如果要實現應用域隔離,自己實現IpcChannel,MAF中的應用域隔離實現也是非常好的參考資料。

MAF的7層結構主要是實現從外掛的宿主函數轉換,例如可以在將外掛程式的介面放入主介面中渲染,做出像瀏覽器一樣的開一個介面就是一個程序。將外掛中的元件在AddInSideAdapter中轉換為Stream然後在HostSideAdapter中將Stream範例化為元件。而HostView和AddInView實際上是提供兩個轉換介面,Contract是定義傳輸介面。

另外如果傳輸外掛向陣列傳遞影象資料,最後是轉換成byte[],或者使用共用記憶體。

如果有什麼遺漏和錯誤,歡迎指正,批評。