程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#發現之旅第十四講 基於動態編譯的VB.NET腳本引擎(上)

C#發現之旅第十四講 基於動態編譯的VB.NET腳本引擎(上)

編輯:關於C#

腳本的原理

腳本,也可稱為宏,是一種應用系統二次開發的技術,它能在應用系 統提供的一個容器環境中運行某種編程語言,這種編程語言代碼調用應用系統提供的編程接 口,使得應用系統暫時“靈魂附體”,無需用戶干預作而執行一些自動的操作, 此時應用系統稱為“宿主”。

腳本也采用多種編程語言,比如JavaScript 語言,VBScript語言或者其他的,若采用VB則稱為VB腳本。

下圖為腳本原理圖

下圖為常 規編程開發軟件的原理圖

腳本相對 於常規的軟件開發用的編程語言有著很大的不同。首先是腳本是不能獨立運行的,必須在某 個應用系統搭建的容器環境中運行,脫離這個環境則腳本代碼毫無作用,其邏輯和功能和應 用系統的功能聯系非常緊密。腳本代碼不會事先編譯,而是解釋執行或者臨時編譯執行的, 而且腳本代碼的修改不會導致應用系統的重新編譯和部署,而且腳本代碼發生修改,應用系 統不需要重新啟動即可應用修改後的腳本代碼,而且運行腳本的應用系統可以不是DLL,而是 純粹的EXE。

腳本語言大多是動態語言,所謂動態語言就是程序代碼在編寫時已經假 設操作的對象的類型,成員屬性或方法的信息,而編譯器不會進行這方面的檢查。C#不是動 態語言,是靜態語言,因為它在編譯時會嚴格的檢查代碼操作的對象的類型,成員信息,稍 有不對則會報編譯錯誤。VB.NET源自VB,是動態語言,它在編譯時不會嚴格的檢查對象的類 型及其成員信息,執行後期綁定,而是在運行時檢查,若運行時發現對象類型和成員信息錯 誤,則會報運行時錯誤。腳本技術應當非常靈活和自由,袁某人覺得此時使用C#這種靜態語 言不是明智之舉,而應當使用類似VB.NET這樣的動態語言。

而常規的軟件開發而生成 的軟件大多是事先編譯好的,和應用系統是獨立的,軟件是調用應用系統的功能而不是應用 系統的一部分。軟件代碼修改會導致軟件的重新編譯和部署,應用系統必須提供DLL格式的程 序集文件。

微軟的很多軟件產品有有VBA的功能,比如MS Office,甚至VS.NET集成開 發環境也有VBA宏的功能。腳本提供給應用系統二次開發的能力,而且這種二次開發能力簡單 靈活,部署方便。

在應用方面腳本技術帶來的最大好處就是簡單靈活,部署方便。腳本代碼以純文本的格式 進行存儲,修改方便,而且腳本修改後,應用系統無需重新啟動而能立即使用新的腳本,腳 本代碼中能實現比較復制的邏輯控制,能響應應用系統的事件,能一定程度上擴展應用系統 的功能,這有點類似數據庫中的存儲過程。

但腳本功能運行在應用系統提供的容器環 境中,其功能是受到嚴格限制的,一些腳本還受到系統權限的限制。因此腳本只能有限的擴 展應用系統的功能,若所需功能比較復雜,腳本可能無法實現,此時還得依賴傳統編程。不 過在很多情況下,腳本還是能發揮很大的作用。【袁永福原創,轉載請注明出處】

VB.NET腳本原理

VB.NET腳本就是采用VB.NET語法的腳本。VS.NET集成開發環 境提供的宏也是采用VB.NET語法。微軟.NET框架提供了一個腳本引擎,那就是在程序集 microsoft.visualbasic.vsa.dll中的類型Microsfot.VisualBasic.Vsa.VsaEngine,該類型 在微軟.NET框架1.1和2.0中都有,使用起來不算容易,而且在微軟.NET框架2.0中VsaEngine 類型標記為“已過時”。在此筆者不使用VsaEngine類型來實現VB.NET腳本,而是 使用動態編譯技術來實現腳本引擎。

使用動態編譯技術實現VB.NET腳本引擎的原理是 ,程序將用戶的腳本代碼字符串進行一些預處理,然後調用 Microsoft.VisualBasic.VBCodeProvider類型的CompileAssemblyFromSource函數進行動態編 譯,生成一個臨時的程序集對象,使用反射技術獲得程序集中的腳本代碼生成的方法,主程 序將按照名稱來調用這些腳本代碼生成的方法。若用戶修改了腳本代碼,則這個過程重復一 次。

VB.NET腳本引擎設計

這裡筆者將用倒推法來設計VB.NET腳本引擎,也就 是從預期的最終使用結果來反過來設計腳本引擎。

主程序將按照名稱來調用腳本方法 ,很顯然VB.NET代碼編譯生成的是一個.NET程序集類庫,為了方便起見,筆者將所有的 VB.NET腳本方法集中到一個VB.NET腳本類型。筆者將腳本方法定義為靜態方法,主要有兩個 好處,首先腳本引擎不必生成對象實例,其次能避免由於沒有生成對象實例而導致的空引用 錯誤,這樣能減少腳本引擎的工作量。

在VB.NET語法中,可以使用代碼塊 “public shared sub SubName()”來定義一個靜態過程,但筆者不能要求用戶在 編寫VB.NET腳本代碼時使用“public shared sub SubName()”的VB.NET語法,而 只能使用“sub SubName()”這樣比較簡單的語法。同樣用戶在腳本中定義全局變 量時不能使用“private shared VarName as TypeName”的語法,而是簡單的使 用“dim VarName as TypeName”的語法。這時筆者可以使用VB.NET語法的模塊的 概念。在VB.NET語法中,將代碼塊“Module ModuleName ……. End Module”中的所有的代碼編譯為靜態的。比如把“sub SubName”編譯成 “public shared sub SubName()”,把“dim VarName as TypeName”編譯為“public shared VarName as TypeName”。這樣借助 VB.NET模塊的概念就能解決了這個問題。

在一些腳本中筆者經常可以看見類似 “window.left”或者“document.location”的方式來使用全局對象 ,若筆者在VB.NET中直接使用“window.left”之類的代碼必然報 “window”對象或者變量找不到的編譯錯誤。

“window”全局 變量一般映射到應用程序的主窗體。比如“window.left”表示主窗體的左端位置 ,“window.width”標准主窗體的寬度等等。【袁永福原創,轉載請注明出處】

“document”或者“window”等全局對象是映射到文檔或者主 窗體等實例對象的,因此它們的成員不能定義成靜態,為了能實現在腳本代碼中直接使用類 似“window.left”的方法來直接使用全局對象,筆者又得使用VB.NET的一個語法 特性。在Microsoft.VisualBasic.dll中有一個公開的特性類型 “Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute”,該 特性是隱藏的,可能不是微軟推薦使用,但在微軟.NET框架1.1和2.0都有這個特性類型,功 能也是一樣的。對於一般的編程該特性是用不著的,它可附加在某個類型上,VB.NET編譯器 會認為附加了該特性的類型的靜態屬性值就是全局對象。比如筆者定義了一個GlobalObject 類型,附加了StandardModuleAttribute特性,它有一個名為Document的靜態屬性,在對於腳 本中的“document.Location”代碼塊,VB.NET編譯器會針對 “document”標識符檢索所有附加了StandardModuleAttribute的類型的靜態屬性 ,最後命中GlobalObject類型,於是會自動擴展為 “GlobalObject.Document.Location”的代碼。這個過程是在編譯時進行的,在 實際運行中不再需要進行這樣的查找,這樣的語法特點是C#所沒有的。上述的這些特點使得 VB.NET語法更適合作為腳本的語法。

類似全局對象,在VB.NET語法中具有全局函數的 功能,比如對於Asc函數,它實際上是類型Microsoft.VisualBasic.Strings的一個靜態成員 函數,但在VB.NET中可以在任何時候任何地方直接使用,VB.NET編譯器會將代碼中的Asc函數 自動擴展為“Microsoft.VisualBasic.Strings.Asc”。這個過程是在編譯時進行 的,而運行時不再需要這樣的擴展。

.NET框架自帶VB.NET編譯器,它就是在.NET框架 安裝目錄下的vbc.exe,在筆者的系統中VB.NET編譯器的路徑是 “C:"WINDOWS"Microsoft.NET"Framework"v2.0.50727"vbc.exe”,參考MSDN中關 於VB.NET編譯的命令行的說明,它支持一個名為“imports”的命令行參數指令。 比如可以這樣調用VB.NET編譯器“vbc.exe /imports:Microsoft.VisualBasic,system,system.drawing 其他參數”,該參數的功 能是從指定的程序集導入名稱空間。在VB.NET編譯器命令行中使用imports指令和在VB.NET代 碼中使用Imports指令是不一樣的。在源代碼中使用Imports指令是用於減少代碼編寫量,而 在命令行中使用imports指令是啟動指定名稱空間下的全局對象和全局函數,若一個類型附加 了StandardModuleAttribute特性,而且定義了一些靜態函數和屬性,但並沒有在編譯器命令 行中導入帶類型所在的名稱空間,則VB.NET編譯器不會感知到該類型中定義的全局對象和全 局函數,因此在編寫VB.NET代碼時必須使用“類型名稱.靜態屬性或函數的名稱” 的方式來調用全局對象和全局函數。比如若沒有在VB.NET編譯器的命令行參數中使用 “/imports:Microsoft.VisualBasic”參數,則Asc函數不再是全局函數,若在代 碼中直接使用Asc函數則必然報編譯錯誤,而必須使用 “Microsoft.VisualBasic.Strings.Asc”的方式來使用,即使源代碼中使用了 “Imports Microsoft.VisualBasic”,也只能用“Strings.Asc”的 方式來使用函數。

如上所述,借助於StandardModuleAttribute特性和編譯器命令行 參數imports,筆者就可以實現VB.NET的全局對象和全局函數了。

根據上述說明,筆 者設計如下的參與動態編譯的VB.NET腳本代碼的結構

Option Strict Off
Imports System
Imports Microsoft.VisualBasic
Namespace  NameSpaceXVBAScriptEngien
    Module mdlXVBAScriptEngine

         sub 腳本方法1()
            'VB.NET代碼
         end sub

        sub 腳本方法2()
             'VB.NET代碼
        end sub

    End  Module
End Namespace

其中斜體部分就是用戶提供的原始腳本代碼, 而開頭和結尾部分是腳本引擎自動添加的,這樣能減少腳本引擎的使用難度。

在腳本 引擎自動添加的代碼中使用了Imports語句引入的名稱空間,默認添加了System和 Microsoft.VisualBasic兩個名稱空間,為了方便使用,可以讓用戶添加其他的名稱空間,比 如腳本代碼中大量使用了System.Drawing名稱空間,則可以使用Imports語句導入 System.Drawing名稱空間來減少腳本代碼量。

軟件開發

筆者新建一個 XVBAEngine類型,該類型實現了腳本引擎的功能。腳本引擎包含了參數控制屬性,代碼生成 器,動態編譯,分析和調用臨時程序集等幾個子功能。

參數控制屬性

筆者為 腳本引擎類型定義了幾個屬性用於保存腳本引擎運行所必備的基礎數據。這些屬性中最重要 的屬性就是用戶設置的原始腳本代碼文本。定義該屬性的代碼如下

///  <summary>
/// 腳本代碼改變標記
/// </summary>
private bool bolScriptModified = true;

/// <summary>
/// 原始的VBA腳本文本
/// </summary>
private string  strScriptText = null;
/// <summary>
/// 原始的VBA腳本文本
/// </summary>
public string ScriptText
{
     get
    {
        return strScriptText;
    }
    set
    {
        if (strScriptText != value)
        {
            bolScriptModified =  true;
            strScriptText = value;
         }
    }
}

在這裡ScriptText屬性表示用戶設置的原始的VBA 腳本代碼,實際參與動態編譯的腳本代碼和原始設置的原始的VBA腳本代碼是不一致的。當用 戶修改了腳本代碼文本,則會設置bolScriptModified變量的值,腳本引擎運行腳本方法時會 檢查這個變量的值來判斷是否需要重新動態編譯操作。

此外袁某人還定義了其他的一 些控制腳本引擎的屬性,其定義的代碼如下

private bool bolEnabled =  true;
/// <summary>
/// 對象是否可用
///  </summary>
public bool Enabled
{
    get
     {
        return bolEnabled;
    }
    set
    {
        bolEnabled = value;
    }
}

private bool bolOutputDebug = true;
/// <summary>
/// 腳本在運行過程中可否輸出調試信息
/// </summary>
public  bool OutputDebug
{
    get
    {
         return bolOutputDebug;
    }
    set
    {
         bolOutputDebug = value;
    }
}

編譯腳本

筆者為腳本引擎編寫了Compile函數用於編輯腳本。編譯腳本的過程大體分為生成腳 本代碼文本、編譯腳本編譯、分析腳本程序集三個步驟。

生成腳本代碼文本

VB.NET腳本引擎使用的動態編譯技術,而動態編譯技術的第一個部分就是代碼生成器 ,腳本大部分代碼都是由主程序提供的,因此其代碼生成器也就是將原始的腳本代碼進行一 些封裝而已。【袁永福原創,轉載請注明出處】

根據上述對運行時腳本的設計,用戶 可以導入其他的名稱空間,於是腳本引擎定義了SourceImports屬性來自定義導入的名稱空間 ,定義該屬性的代碼如下

/// <summary>
/// 源代碼中使用的 名稱空間導入
/// </summary>
private StringCollection  mySourceImports = new StringCollection();
/// <summary>
///  源代碼中使用的名稱空間導入
/// </summary>
public  StringCollection SourceImports
{
    get
    {
         return mySourceImports;
    }
}

在腳本引 擎的初始化過程中,程序會默認添加上System和Microsoft.VisualBasic兩個名稱空間。隨後 程序使用以下代碼來生成實際參與編輯的腳本代碼文本

// 生成編譯用的完整 的VB源代碼
string ModuleName = "mdlXVBAScriptEngine";
string nsName  = "NameSpaceXVBAScriptEngien";
System.Text.StringBuilder mySource =  new System.Text.StringBuilder();
mySource.Append("Option Strict Off");
foreach (string import in this.mySourceImports)
{
     mySource.Append(""r"nImports " + import);
}
mySource.Append (""r"nNamespace " + nsName);
mySource.Append(""r"nModule " +  ModuleName);
mySource.Append(""r"n");
mySource.Append (this.strScriptText);
mySource.Append(""r"nEnd Module");
mySource.Append(""r"nEnd Namespace");
string strRuntimeSource =  mySource.ToString();

這段代碼功能也比較簡單,首先輸出“Option Strick Off”語句,然後使用mySourceImports輸出若干個Imports語句。這裡的 mySourceImports是一個字符串列表,用於存放引用的名稱空間,比如“System” ,“Microsoft.VisualBasic”等等,用於組成VB.NET腳本的Imports語句。然後 輸出Namespace和Module代碼塊來包括了用戶提供的原始代碼文本。這裡的strSourceText就 是用戶提供的原始代碼文本。最後變量 strRuntimeSource中就包含了實際運行的VB.NET代碼 文本。

編譯腳本

程序生成完整的VB.NET腳本代碼文本後就可以編譯了,為了 提高效率,這裡袁某定義了一個靜態myAssemblies的哈希列表變量,定義該變量的代碼如下

/// <summary>
/// 所有緩存的程序集
///  </summary>
private static Hashtable myAssemblies = new  Hashtable();

該列表緩存了以前編輯生成的程序集,鍵值就是腳本文本,鍵 值就是程序集。若緩存區中沒有找到以前緩存的程序集那腳本引擎就可以調用VB.NET編譯器 編輯腳本了。

為了豐富腳本引擎的開發接口,筆者使用以下代碼定義了 ReferencedAssemblies屬性。

/// <summary>
/// VB.NET編譯 器參數
/// </summary>
private CompilerParameters  myCompilerParameters = new CompilerParameters();
/// <summary>
/// 引用的名稱列表
/// </summary>
public StringCollection  ReferencedAssemblies
{
    get
    {
         return myCompilerParameters.ReferencedAssemblies;
    }
}

ReferencedAssemblies保存了編輯腳本時使用的程序集,在初始化腳本引擎 時,系統已經默認向該列表添加了mscorlib.dll、System.dll、System.Data.dll、 System.Xml.dll、System.Drawing.dll、System.Windows.Forms.dll、 Microsoft.VisualBasic.dll等.NET框架標准程序集,用戶可以使用該屬性添加第三方程序集 來增強腳本引擎的功能。

在前面的說明中,為了實現全局對象和全局函數,需要在VB.NET編譯器的命令上中使用 imports指令導入全局對象和全局函數所在的名稱空間,為此筆者定義了一個 VBCompilerImports的屬性來保存這些名稱空間,定義該屬性的代碼如下

///  <summary>
/// VB編譯器使用的名稱空間導入
///  </summary>
private StringCollection myVBCompilerImports = new  StringCollection();
/// <summary>
/// VB編譯器使用的名稱空間導 入
/// </summary>
public StringCollection VBCompilerImports
{
    get
    {
        return  myVBCompilerImports;
    }
}

在初始化腳本引擎時程序會 在VBCompilerImports列表中添加默認的名稱空間Microsoft.VisualBasic。

准備和執 行編譯的腳本代碼和一些參數後,腳本引擎就來編譯腳本代碼生成臨時程序集了,筆者使用 以下的代碼來進行編譯操作

// 檢查程序集緩存區
myAssembly =  (System.Reflection.Assembly)myAssemblies[strRuntimeSource];
if (myAssembly  == null)
{
    // 設置編譯參數
     this.myCompilerParameters.GenerateExecutable = false;
     this.myCompilerParameters.GenerateInMemory = true;
     this.myCompilerParameters.IncludeDebugInformation = true;
    if  (this.myVBCompilerImports.Count > 0)
    {
        //  添加 imports 指令
        System.Text.StringBuilder opt = new  System.Text.StringBuilder();
        foreach (string import in  this.myVBCompilerImports)
        {
             if (opt.Length > 0)
            {
                 opt.Append(",");
            }
             opt.Append(import.Trim());
        }
         opt.Insert(0, " /imports:");
        for (int iCount = 0;  iCount < this.myVBCompilerImports.Count; iCount++)
         {
            this.myCompilerParameters.CompilerOptions =  opt.ToString();
        }
    }//if

    if  (this.bolOutputDebug)
    {
        // 輸出調試信息
        System.Diagnostics.Debug.WriteLine(" Compile VBA.NET script  "r"n" + strRuntimeSource);
        foreach (string dll in  this.myCompilerParameters.ReferencedAssemblies)
        {
             System.Diagnostics.Debug.WriteLine("Reference:" + dll);
        }
    }

    // 對VB.NET代碼進行編譯
    Microsoft.VisualBasic.VBCodeProvider provider = new  Microsoft.VisualBasic.VBCodeProvider();
#if DOTNET11
    // 這段 代碼用於微軟.NET1.1
    ICodeCompiler compiler =  provider.CreateCompiler();
    CompilerResults result =  compiler.CompileAssemblyFromSource(
         this.myCompilerParameters,
        strRuntimeSource );
#else
    // 這段代碼用於微軟.NET2.0或更高版本
     CompilerResults result = provider.CompileAssemblyFromSource(
         this.myCompilerParameters,
        strRuntimeSource);
#endif
    // 獲得編譯器控制台輸出文本
     System.Text.StringBuilder myOutput = new System.Text.StringBuilder();
     foreach (string line in result.Output)
    {
         myOutput.Append(""r"n" + line);
    }
     this.strCompilerOutput = myOutput.ToString();
    if  (this.bolOutputDebug)
    {
        // 輸出編譯結果
        if (this.strCompilerOutput.Length > 0)
         {
            System.Diagnostics.Debug.WriteLine("VBAScript  Compile result" + strCompilerOutput);
        }
    }

    provider.Dispose();

    if  (result.Errors.HasErrors == false)
    {
        // 若沒 有發生編譯錯誤則獲得編譯所得的程序集
        this.myAssembly =  result.CompiledAssembly;
    }
    if (myAssembly != null)
    {
        // 將程序集緩存到程序集緩存區中
         myAssemblies[strRuntimeSource] = myAssembly;
    }
}

在這段代碼中,首先程序設置編譯器的參數,並為VB編譯器添加引用的程序 集信息,VB.NET編譯器有個名為imports的命令行參數用於指定全局名稱空間。用法為 “/imports:名稱空間1,名稱空間2”,在編譯器命令行中使用imports參數和在代 碼文本中使用imports語句是有所不同的。

然後程序創建一個VBCodeProvider對象開 始編譯腳本,對於微軟.NET框架1.1和2.0其操作過程是有區別的。對微軟.NET1.1還得調用 provider的CreateCompilter函數創建一個IcodeCompiler對象,然後調用它的 CompileAssemblyFromSource來編譯腳本,而對於微軟.NET框架2.0則是直接調用provider的 CompileAssemblyFromSource來編譯腳本的。

編譯器編譯後返回一個CompilerResults 的對象表示編譯結果,若發生編譯錯誤程序就輸出編譯錯誤信息。若編譯成功則程序使用編 譯結果的CompileAssembly屬性獲得編輯腳本代碼生成的臨時程序集對象了。然後把程序集對 象緩存到myAssemblies列表中。

分析臨時程序集

調用編譯器編譯腳本代碼後 成功的生成臨時程序集後,腳本引擎需要分析這個程序集,獲得所有的可用的腳本方法,其 分析代碼為

if (this.myAssembly != null)
{
    // 檢 索腳本中定義的類型
    Type ModuleType = myAssembly.GetType(nsName +  "." + ModuleName);
    if (ModuleType != null)
    {
        System.Reflection.MethodInfo[] ms = ModuleType.GetMethods (
            System.Reflection.BindingFlags.Public
             | System.Reflection.BindingFlags.NonPublic
             | System.Reflection.BindingFlags.Static);
        foreach  (System.Reflection.MethodInfo m in ms)
        {
             // 遍歷類型中所有的靜態方法
            // 對 每個方法創建一個腳本方法信息對象並添加到腳本方法列表中。
             ScriptMethodInfo info = new ScriptMethodInfo();
             info.MethodName = m.Name;
            info.MethodObject  = m;
            info.ModuleName = ModuleType.Name;
             info.ReturnType = m.ReturnType;
             this.myScriptMethods.Add(info);
            if  (this.bolOutputDebug)
            {
                 // 輸出調試信息
                 System.Diagnostics.Debug.WriteLine("Get vbs method """ + m.Name +  """");
            }
        }//foreach
         bolResult = true;
    }//if
}//if

在這段 代碼中,程序首先獲得腳本模塊的類型,在這裡類型全名為 “NameSpaceXVBAScriptEngien. mdlXVBAScriptEngine”,然後使用反射獲得該 類型中所有的公開或未公開的靜態成員方法對象,對於其中的每一個方法創建一個 ScriptMethodInfo類型的腳本方法信息對象來保存這個方法的一些信息,將這些信息保存到 myScriptMethods列表中供以後調用。

筆者配套定義了ScriptMethodInfo類型和 myScriptMethods列表,定義它們的代碼如下

/// <summary>
///  所有腳本方法的信息列表
/// </summary>
private ArrayList  myScriptMethods = new ArrayList();
/// <summary>
/// 腳本方 法信息
/// </summary>
private class ScriptMethodInfo
{
    /// <summary>
    /// 模塊名稱
    ///  </summary>
    public string ModuleName = null;
     /// <summary>
    /// 方法名稱
    ///  </summary>
    public string MethodName = null;
     /// <summary>
    /// 方法對象
    ///  </summary>
    public System.Reflection.MethodInfo MethodObject  = null;
    /// <summary>
    /// 方法返回值
     /// </summary>
    public System.Type ReturnType =  null;
    /// <summary>
    /// 指向該方法的委托
    /// </summary>
    public System.Delegate  MethodDelegate = null;
}

使用腳本方法信息列表,腳本引擎調用腳 本方法時就不需要使用反射查找腳本方法了,只需要在腳本方法信息列表中快速的查找和調 用。

調用腳本

腳本引擎前期完成的大量的工作就是為了最後能調用腳本,為 此筆者定義了、Execute函數用於調用指定名稱的腳本方法。定義該函數的代碼如下

/// <summary>
/// 執行腳本方法
///  </summary>
/// <param name="MethodName">方法名稱 </param>
/// <param name="Parameters">參數</param>
/// <param name="ThrowException">若發生錯誤是否觸發異常 </param>
/// <returns>執行結果</returns>
public  object Execute(string MethodName, object[] Parameters, bool  ThrowException)
{
    // 檢查腳本引擎狀態
    if  (CheckReady() == false)
    {
        return null;
    }
    if (ThrowException)
    {
         // 若發生錯誤則拋出異常,則檢查參數
        if (MethodName ==  null)
        {
            throw new  ArgumentNullException("MethodName");
        }
         MethodName = MethodName.Trim();
        if (MethodName.Length ==  0)
        {
            throw new  ArgumentException("MethodName");
        }
        if  (this.myScriptMethods.Count > 0)
        {
             foreach (ScriptMethodInfo info in this.myScriptMethods)
             {
                // 遍歷所有的腳本方法信 息,不區分大小寫的找到指定名稱的腳本方法
                if  (string.Compare(info.MethodName, MethodName, true) == 0)
                 {
                    object result  = null;
                    if (info.MethodDelegate  != null)
                    {
                         // 若有委托則執行委托
                         result = info.MethodDelegate.DynamicInvoke (Parameters);
                    }
                     else
                    {
                        // 若沒有委托則直接動態執行方 法
                        result =  info.MethodObject.Invoke(null, Parameters);
                     }
                    // 返回腳本方法返回值
                    return result;
                 }//if
            }//foreach
         }//if
    }
    else
    {
        //  若發生錯誤則不拋出異常,安靜的退出
        // 檢查參數
         if (MethodName == null)
        {
             return null;
        }
        MethodName =  MethodName.Trim();
        if (MethodName.Length == 0)
         {
            return null;
         }
        if (this.myScriptMethods.Count > 0)
         {
            foreach (ScriptMethodInfo info in  this.myScriptMethods)
            {
                 // 遍歷所有的腳本方法信息,不區分大小寫的找到指定名稱的腳本方法
                if (string.Compare(info.MethodName, MethodName,  true) == 0)
                {
                     try
                    {
                         // 執行腳本方法
                         object result = info.MethodObject.Invoke (null, Parameters);
                        // 返 回腳本方法返回值
                        return  result;
                    }
                     catch (Exception ext)
                     {
                        // 若發生錯誤則輸 出調試信息
                         System.Console.WriteLine("VBA:" + MethodName + ":" + ext.Message);
                     }
                     return null;
                }//if
             }//foreach
        }//if
    }//else
     return null;
}//public object Execute

這裡函數參數為要調用 的腳本方法的名稱,不區分大小寫,調用腳本使用的參數列表,還有控制是否拋出異常的參 數。在函數裡面,程序遍歷myScriptMethods列表中所有以前找到的腳本方法的信息,查找指 定名稱的腳本方法,若找到則使用腳本方法的Invoke函數執行腳本方法,如此陳旭就能調用 腳本了。

為了豐富腳本引擎的編程接口,筆者還定義了HasMethod函數來判斷是否存 在指定名稱的腳本方法,定義了ExecuteSub函數來安全的不拋出異常的調用腳本方法。

Window全局對象

在很多腳本中存在一個名為“window”的全局對 象,該對象大多用於和用戶界面互換,並映射到應用系統主窗體。在這裡筆者仿造HTML的 javascript腳本的window全局對象來構造出自己的window全局對象。

參考javascript 中的window全局對象,對筆者有參考意義的類型成員主要分為映射到屏幕大小或者主窗體的 位置大小的屬性,還有延時調用和定時調用的方法,還有顯示消息框或輸入框的方法。

筆者建立一個XVBAWindowObject類型作為Window全局對象的類型。

成員屬性

筆者首先定義一個UserInteractive屬性,該屬性指定應用系統是否能和用戶桌面交 互。定義該屬性的代碼如下

protected bool bolUserInteractive =  true;
/// <summary>
/// 是否允許和用戶交互,也就是是否顯示用戶 界面
/// </summary>
/// <remarks>當應用程序為ASP.NET或者 Windows Service程序時不能有圖形化用戶界面,因此需要設置該屬性為 false.</remarks>
public bool UserInteractive
{
    get { return  bolUserInteractive; }
    set { bolUserInteractive = value; }
}

一些應用系統,包括ASP.NET和Windows Service,它是不能和用戶交互的 ,不能有圖形化用戶界面,不能調用MessageBox函數,不能使用.NET類庫中 System.Widnows.Forms名稱空間下的大部分功能,若強行調用則會出現程序錯誤。這個腳本 引擎設計目標是可以運行在任何程序類型中的,包括WinForm,命令行模式,ASP.NET和 Windows Service。因此在這裡筆者定義了UserInteractive屬性用於關閉window全局對象的 某些和用戶互換相關的功能,比如顯示消息框,延時調用和定時調用等等,主動關閉這些功 能對應用系統的影響是不大的。

筆者還定義了其他的一些屬性,其定義的代碼如下

protected string strSystemName = "應用程序";
///  <summary>
/// 系統名稱
/// </summary>
public string  SystemName
{
    get
    {
        return  strSystemName;
    }
    set
    {
         strSystemName = value;
    }
}

protected XVBAEngine  myEngine = null;
/// <summary>
/// 腳本引擎對象
///  </summary>
public XVBAEngine Engine
{
    get {  return myEngine; }
}

protected  System.Windows.Forms.IWin32Window myParentWindow = null;
///  <summary>
/// 父窗體對象
/// </summary>
public  System.Windows.Forms.IWin32Window ParentWindow
{
    get {  return myParentWindow; }
    set { myParentWindow = value; }
}

/// <summary>
/// 屏幕寬度
///  </summary>
public int ScreenWidth
{
    get
     {
        if (bolUserInteractive)
        {
            return  System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width;
        }
        else
        {
            return  0;
        }
    }
}

///  <summary>
/// 屏幕高度
/// </summary>
public int  ScreenHeight
{
    get
    {
        if  (bolUserInteractive)
        {
            return  System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height;
        }
        else
        {
             return 0;
        }
    }
}

這裡的 ParentWindow屬性表示應用系統的主窗體。

延時調用和定時調用

在Window全 局對象中,筆者使用System.Windows.Forms.Timer對象實現了延時調用和定時調用,由於定 時器對象屬於用戶互換相關的功能,因此延時調用和定時調用受到UserInteractive屬性的影 響。筆者使用以下代碼來實現延時調用的功能

private string  strTimeoutMethod = null;
private System.Windows.Forms.Timer myTimer;
/// <summary>
/// 設置延時調用
/// </summary>
///  <param name="MinSecend">延時的毫秒數</param>
/// <param  name="MethodName">調用的方法名稱</param>
public void SetTimeout (int MinSecend, string MethodName)
{
    // 若不支持和用戶互換 則本功能無效
    if ( bolUserInteractive == false)
         return;
    if (myEngine == null)
        return;
    if (myIntervalTimer != null)
    {
        //  取消當前的演示處理
        myIntervalTimer.Stop();
    }
    strTimerIntervalMethod = null;
    if (myTimer ==  null)
    {
        // 若定時器不存在則創建新的定時器對象
        myTimer = new System.Windows.Forms.Timer();
         myTimer.Tick += new EventHandler(myTimer_Tick);
    }
    // 設置定時器
    myTimer.Interval = MinSecend;
     // 設置腳本方法名稱
    strTimeoutMethod = MethodName;
     // 啟動定時器
    myTimer.Start();
}
///  <summary>
/// 清除延時調用
/// </summary>
public  void ClearTimeout()
{
    if (myTimer != null)
     {
        // 停止定時器
        myTimer.Stop();
    }
    // 清空延時調用的腳本方法名稱
     strTimeoutMethod = null;
}

/// <summary>
/// 延時 調用的定時器事件處理
/// </summary>
/// <param  name="sender"></param>
/// <param  name="e"></param>
private void myTimer_Tick(object sender,  EventArgs e)
{
    myTimer.Stop();
    if (myEngine !=  null && strTimeoutMethod != null)
    {
         // 獲得腳本方法
        string m = strTimeoutMethod.Trim ();
        strTimeoutMethod = null;
        if  (myEngine.HasMethod(m))
        {
            //  若腳本引擎中定義了該名稱的腳本方法則安全的執行該腳本方法
             myEngine.ExecuteSub(m);
        }
    }
}

SetTimeout函數用於實現延時調用,它的參數為延時調用的毫秒數和腳本方 法名稱。在該函數中程序初始化一個名為myTimer定時器,設置它的Interval屬性為指定的毫 秒數,然後啟動該定時器。而在myTimer的定時事件處理中程序停止myTimer定時器,然後調 用腳本引擎的ExecuteSub函數運行指定名稱的無參數腳本方法。使用SetTimeout只會運行一 次腳本方法,在調用SetTimeout函數准備延時調用後可以調用ClearTimeout函數來立即取消 延時調用。

筆者使用以下代碼來實現定時調用的功能

///  <summary>
/// 定時調用使用的定時器控件
/// </summary>
private System.Windows.Forms.Timer myIntervalTimer = null;
///  <summary>
/// 定時調用的腳本方法的名稱
/// </summary>
private string strTimerIntervalMethod = null;

///  <summary>
/// 設置定時運行
/// </summary>
///  <param name="MinSecend">時間間隔毫秒數</param>
/// <param  name="MethodName">方法名稱</param>
public void SetInterval(int  MinSecend, string MethodName)
{
    if (bolUserInteractive ==  false)
    {
        // 若不能和用戶互換則退出處理
         return;
    }
    // 檢查參數
    if  (MethodName == null || MethodName.Trim().Length == 0)
    {
        return;
    }
    if (this.myEngine ==  null)
    {
        return;
    }

     if (myTimer != null)
    {
        //取消當前的延時 調用功能
        myTimer.Stop();
    }
     strTimeoutMethod = null;

    if (myEngine.HasMethod (MethodName.Trim()) == false)
        return;
     strTimerIntervalMethod = MethodName.Trim();

    if  (myIntervalTimer == null)
    {
        // 初始化定時調 用的定時器控件
        myIntervalTimer = new  System.Windows.Forms.Timer();
        myIntervalTimer.Tick += new  EventHandler(myIntervalTimer_Tick);
    }

     myIntervalTimer.Interval = MinSecend;
}
/// <summary>
///  清除定時運行
/// </summary>
public void ClearInterval()
{
    if (myIntervalTimer != null)
    {
         // 停止定時調用
        myIntervalTimer.Stop();
    }
    strTimerIntervalMethod = null;
}
///  <summary>
/// 定時調用的定時器事件處理
/// </summary>
/// <param name="sender"></param>
/// <param  name="e"></param>
private void myIntervalTimer_Tick(object  sender, EventArgs e)
{
    if (myIntervalTimer != null)
    {
        // 設置定時調用的腳本方法名稱
         strTimerIntervalMethod = strTimerIntervalMethod.Trim();
    }
    if (strTimerIntervalMethod == null
        ||  strTimerIntervalMethod.Length == 0
        || myEngine ==  null
        || myEngine.HasMethod(strTimerIntervalMethod) ==  false)
    {
        if (myIntervalTimer != null)
        {
            // 若沒找到指定名稱的腳本方法則停 止定時調用
            myIntervalTimer.Stop();
         }
        return;
    }
    // 安全的執行指定 名稱的腳本方法
    myEngine.ExecuteSub(strTimerIntervalMethod);
}

SetInterval函數用於實現定時調用,它的參數為兩次調用之間的時間間隔, 以及腳本方法名稱。在該函數中程序初始化一個名為myIntervalTimer的定時器,設置它的 Interval屬性為指定的時間間隔,然後啟動該定時器。在myIntervalTimer的定時事件處理中 程序調用腳本引擎的ExecuteSub函數運行指定名稱的無參數腳本方法。SetInterval會無休止 的定時調用腳本方法,直到調用ClearInterval函數終止定時調用。

延時調用和定時 調用是相互排斥的過程,啟動延時調用會停掉定時調用,而啟用定時調用會停掉延時調用。

映射應用程序主窗體

Window全局對象定義了一些屬性用於映射應用程序主窗 體,筆者定義一個Title屬性應用映射主窗體的文本,其代碼如下

///  <summary>
/// 窗體標題
/// </summary>
public string  Title
{
    get
    {
         System.Windows.Forms.Form frm = myParentWindow as  System.Windows.Forms.Form;
        if (frm == null)
         {
            return "";
        }
         else
        {
            return  frm.Text;
        }
    }
    set
     {
        System.Windows.Forms.Form frm = myParentWindow as  System.Windows.Forms.Form;
        if (frm != null)
         {
            frm.Text = value;
         }
    }
}

類似的,筆者定義了Left,Top、Width和Height 屬性用於映射主窗體的左邊位置、頂邊位置,寬度和高度。

借助於這些Title、Left 、Top、Width和Height屬性,用戶就可以在腳本中獲得和設置主窗體的一些屬性了。

這些屬性全都是和用戶互換相關的功能,因此都受UserInteractive屬性控制。若ASP.NET程 序和Windows Service程序使用的腳本調用這些屬性將不會產生任何效果。對於WinForm程序 ,運行腳本前應當將主窗體設置到window全局對象的ParentWindow屬性上。

顯示消息框

Window全局對象還定義了一些函數用於顯示一些消息對話框實現用戶互換。主要代 碼為

/// <summary>
/// 將對象轉化為用於顯示的文本
/// </summary>
/// <param name="objData">要轉換的對象 </param>
/// <returns>顯示的文本</returns>
private  string GetDisplayText(object objData)
{
    if (objData ==  null)
        return "[null]";
    else
         return Convert.ToString(objData);
}

/// <summary>
/// 顯示消息框
/// </summary>
/// <param  name="objText">提示信息的文本</param>
public void Alert(object  objText)
{
    if (bolUserInteractive == false)
         return;
    System.Windows.Forms.MessageBox.Show(
         myParentWindow,
        GetDisplayText(objText),
         SystemName,
         System.Windows.Forms.MessageBoxButtons.OK,
         System.Windows.Forms.MessageBoxIcon.Information);
}
///  <summary>
/// 顯示錯誤消息框
/// </summary>
///  <param name="objText">提示信息的文本</param>
public void  AlertError(object objText)
{
    if (bolUserInteractive ==  false)
        return;
     System.Windows.Forms.MessageBox.Show(
        myParentWindow,
        GetDisplayText(objText),
        SystemName,
         System.Windows.Forms.MessageBoxButtons.OK,
         System.Windows.Forms.MessageBoxIcon.Exclamation);
}

///  <summary>
/// 顯示一個提示信息框,並返回用戶的選擇
///  </summary>
/// <param name="objText">提示的文本 </param>
/// <returns>用戶是否確認的信息</returns>
public bool ConFirm(object objText)
{
    if  (bolUserInteractive == false)
        return false;
     return (System.Windows.Forms.MessageBox.Show(
         myParentWindow,
        GetDisplayText(objText),
         SystemName,
         System.Windows.Forms.MessageBoxButtons.YesNo,
        System.Windows.Forms.MessageBoxIcon.Question)
        ==  System.Windows.Forms.DialogResult.Yes);
}

/// <summary>
/// 顯示一個信息輸入框共用戶輸入
/// </summary>
///  <param name="objCaption">輸入信息的提示</param>
/// <param  name="objDefault">默認值</param>
/// <returns>用戶輸入的信 息</returns>
public string Prompt(object objCaption, object  objDefault)
{
    if (bolUserInteractive == false)
         return null;
    return dlgInputBox.InputBox(
         myParentWindow,
        GetDisplayText(objCaption),
         SystemName,
        GetDisplayText(objDefault));
}

/// <summary>
/// 顯示一個文本選擇對話框
///  </summary>
/// <param name="objCaption">對話框標題 </param>
/// <param name="objFilter">文件過濾器 </param>
/// <returns>用戶選擇的文件名,若用戶取消選擇則返回空引 用</returns>
public string BrowseFile(object objCaption, object  objFilter)
{
    using (System.Windows.Forms.OpenFileDialog  dlg
               = new  System.Windows.Forms.OpenFileDialog())
    {
         dlg.CheckFileExists = true;
        if (objCaption != null)
        {
            dlg.Title =  this.GetDisplayText(objCaption);
        }
        if  (objFilter != null)
            dlg.Filter = GetDisplayText (objFilter);
        if (dlg.ShowDialog(myParentWindow) ==  System.Windows.Forms.DialogResult.OK)
            return  dlg.FileName;
    }
    return null;
}
///  <summary>
/// 顯示一個文件夾選擇對話框
/// </summary>
/// <param name="objCaption">對話框標題</param>
///  <returns>用戶選擇了一個文件夾則返回該路徑,否則返回空引用</returns>
public string BrowseFolder(object objCaption)
{
    using  (System.Windows.Forms.FolderBrowserDialog dlg
                = new System.Windows.Forms.FolderBrowserDialog())
    {
         if (objCaption != null)
        {
             dlg.Description = this.GetDisplayText(objCaption);
         }
        dlg.RootFolder =  System.Environment.SpecialFolder.MyComputer;
        if  (dlg.ShowDialog(myParentWindow) == System.Windows.Forms.DialogResult.OK)
            return dlg.SelectedPath;
        else
            return null;
    }
}

調用這些 方法,腳本能顯示簡單的消息框,顯示文件選擇對話框或文件夾選擇對話框以實現和用戶的 互換。當前這些函數都受到UserInteractive屬性的控制。

這裡定義了一個Alert方法 用於顯示一個簡單的消息框,在VB中可以調用MsgBox方法來實現相同的功能,但MsgBox方法 是VB運行庫的方法,不受UserInteractive屬性的控制,因此不建議使用,而使用Alert方法 。

測試腳本引擎

腳本引擎設計和開發完畢後,袁某就可以編寫應用程序來測 試使用腳本引擎了。在這裡筆者仿造Windows記事本開發了一個簡單的文本編輯器,其用戶界 面如下。

在一個標准的C# WinForm項目中筆者新建一個名為frmMain的主窗體,上面放置工具條, 下面放置一個名為txtEditor的多行文本框。工具條中放上新增,打開,保存,另存為等按鈕 並添加事件處理以實現簡單文本編輯器的功能。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved