C#发现之旅第十四讲 基于动态编译的VB.NET脚本引擎

本章说明

在前面章节中,笔者使用了反射和动态编译技术实现了快速ORM框架,在本章中笔者将继续使用这些技术来实现一个VB.NET的脚本引擎,使得人们在开发中能实现类似MS Office那样实现VBA宏的功能。本章配套的演示程序为/Files/xdesigner/MyVBAScript.zip.

脚本的原理

脚本,也可称为宏,是一种应用系统二次开发的技术,它能在应用系统提供的一个容器环境中运行某种编程语言,这种编程语言代码调用应用系统提供的编程接口,使得应用系统暂时“灵魂附体”,无需用户干预作而执行一些自动的操作,此时应用系统称为“宿主”。

脚本也采用多种编程语言,比如JavaScript语言,VBScript语言或者其他的,若采用VB则称为VB脚本。

下图为脚本原理图

C#发现之旅第十四讲 基于动态编译的VB.NET脚本引擎
下图为常规编程开发软件的原理图

C#发现之旅第十四讲 基于动态编译的VB.NET脚本引擎
    
    脚本相对于常规的软件开发用的编程语言有着很大的不同。首先是脚本是不能独立运行的,必须在某个应用系统搭建的容器环境中运行,脱离这个环境则脚本代码毫无作用,其逻辑和功能和应用系统的功能联系非常紧密。脚本代码不会事先编译,而是解释执行或者临时编译执行的,而且脚本代码的修改不会导致应用系统的重新编译和部署,而且脚本代码发生修改,应用系统不需要重新启动即可应用修改后的脚本代码,而且运行脚本的应用系统可以不是
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.12.0中都有,使用起来不算容易,而且在微软.NET框架2.0VsaEngine类型标记为“已过时”。在此笔者不使用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.12.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语句引入的名称空间,默认添加了SystemMicrosoft.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;

    }

}

在脚本引擎的初始化过程中,程序会默认添加上SystemMicrosoft.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语句。然后输出NamespaceModule代码块来包括了用户提供的原始代码文本。这里的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.dllSystem.dllSystem.Data.dllSystem.Xml.dllSystem.Drawing.dllSystem.Windows.Forms.dllMicrosoft.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.12.0其操作过程是有区别的。对微软.NET1.1还得调用providerCreateCompilter函数创建一个IcodeCompiler对象,然后调用它的CompileAssemblyFromSource来编译脚本,而对于微软.NET框架2.0则是直接调用providerCompileAssemblyFromSource来编译脚本的。

编译器编译后返回一个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”的全局对象,该对象大多用于和用户界面互换,并映射到应用系统主窗体。在这里笔者仿造HTMLjavascript脚本的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.NETWindows Service,它是不能和用户交互的,不能有图形化用户界面,不能调用MessageBox函数,不能使用.NET类库中System.Widnows.Forms名称空间下的大部分功能,若强行调用则会出现程序错误。这个脚本引擎设计目标是可以运行在任何程序类型中的,包括WinForm,命令行模式,ASP.NETWindows 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;

        }

    }

}

类似的,笔者定义了LeftTopWidthHeight属性用于映射主窗体的左边位置、顶边位置,宽度和高度。

借助于这些TitleLeftTopWidthHeight属性,用户就可以在脚本中获得和设置主窗体的一些属性了。

这些属性全都是和用户互换相关的功能,因此都受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#发现之旅第十四讲 基于动态编译的VB.NET脚本引擎
    在一个标准的
C# WinForm项目中笔者新建一个名为frmMain的主窗体,上面放置工具条,下面放置一个名为txtEditor的多行文本框。工具条中放上新增,打开,保存,另存为等按钮并添加事件处理以实现简单文本编辑器的功能。

主窗体中还定义了诸如Function_NewFunction_OpenFunction_Save等等函数用于实现对文档的新建、打开文件和保存文件等处理。而工具条上的按钮就是调用这些功能函数。定义这些功能函数的代码如下【袁永福原创,转载请注明出处】

/// <summary>

/// 执行新建文档

/// </summary>

public bool Function_New()

{

    if (QuerySave())

    {

        txtEditor.Text = "";

        txtEditor.Modified = false;

        strFileName = null;

        return true;

    }

    return false;

}

 

/// <summary>

/// 执行打开文件操作

/// </summary>

public bool Function_Open()

{

    if (QuerySave() == false)

    {

        return false ;

    }

    using (OpenFileDialog dlg = new OpenFileDialog())

    {

        dlg.Filter = "文本文件(*.txt)|*.txt|所有文件|*.*";

        dlg.CheckPathExists = true;

       if (dlg.ShowDialog(this) == DialogResult.OK)

        {

            System.IO.StreamReader reader = new System.IO.StreamReader(

                dlg.FileName, System.Text.Encoding.GetEncoding("gb2312"));

            txtEditor.Text = reader.ReadToEnd();

            reader.Close();

            strFileName = dlg.FileName;

            txtEditor.Modified = false;

            return true;

        }

    }

    return false;

}

 

/// <summary>

/// 执行保存文档操作

/// </summary>

/// <returns>操作是否成功</returns>

public bool Function_Save()

{

    if (strFileName == null)

    {

        using (SaveFileDialog dlg = new SaveFileDialog())

        {

            dlg.Filter = "文本文件(*.txt)|*.txt|所有文件|*.*";

            dlg.CheckPathExists = true;

            dlg.OverwritePrompt = true;

            if (dlg.ShowDialog(this) == DialogResult.OK)

            {

                strFileName = dlg.FileName;

            }

            else

            {

                return false;

            }

        }

    }

    System.IO.StreamWriter writer = new System.IO.StreamWriter(

        strFileName, false, System.Text.Encoding.GetEncoding( "gb2312" ));

    writer.Write(this.txtEditor.Text);

    writer.Close();

    this.txtEditor.Modified = false;

    return true;

}

 

/// <summary>

/// 执行另存为操作

/// </summary>

public bool Function_SaveAs()

{

    using (SaveFileDialog dlg = new SaveFileDialog())

    {

        dlg.Filter = "文本文件(*.txt)|*.txt|所有文件|*.*";

        dlg.CheckPathExists = true;

        dlg.OverwritePrompt = true;

        if (dlg.ShowDialog(this) == DialogResult.OK)

        {

            strFileName = dlg.FileName;

            System.IO.StreamWriter writer = new System.IO.StreamWriter(

                strFileName, false, System.Text.Encoding.GetEncoding("gb2312"));

            writer.Write(this.txtEditor.Text);

            writer.Close();

            this.txtEditor.Modified = false;

            return true;

        }

    }

    return false;

}

 

/// <summary>

/// 执行全选操作

/// </summary>

public void Function_SelectAll()

{

    txtEditor.SelectAll();

}

 

/// <summary>

/// 执行剪切操作

/// </summary>

public void Function_Cut()

{

    txtEditor.Cut();

}

 

/// <summary>

/// 执行复制操作

/// </summary>

public void Function_Copy()

{

    txtEditor.Copy();

}

 

/// <summary>

/// 执行粘帖操作

/// </summary>

public void Function_Paste()

{

    txtEditor.Paste();

}

/// <summary>

/// 执行删除操作

/// </summary>

public void Function_Delete()

{

    txtEditor.SelectedText = "";

}

文档对象

笔者袁某在主窗体中定义了一个DocumentClass的套嵌类型,该类型就是脚本中使用的document全局对象的类型,其代码为

/// <summary>

/// 脚本中使用的文档对象类型,本对象是对 frmMain 的一个封装

/// </summary>

public class DocumentClass

{

    /// <summary>

    /// 初始化对象

    /// </summary>

    /// <param name="frm"></param>

    internal DocumentClass(frmMain frm)

    {

        myForm = frm;

    }

 

    internal frmMain myForm = null;

    /// <summary>

    /// 设置或返回文档文本内容

    /// </summary>

    public string Text

    {

        get

        {

            return myForm.txtEditor.Text;

        }

        set

        {

            myForm.txtEditor.Text = value;

        }

    }

    /// <summary>

    /// 向文档添加文本内容

    /// </summary>

    /// <param name="text">要添加的文本内容</param>

    public void AppendText(string text)

    {

        myForm.txtEditor.AppendText(text);

    }

    /// <summary>

    /// 设置获得文档中选择的部分

    /// </summary>

    public string Selection

    {

        get { return myForm.txtEditor.SelectedText; }

        set { myForm.txtEditor.SelectedText = value; }

    }

    /// <summary>

    /// 文档文件名

    /// </summary>

    public string FileName

    {

        get { return myForm.FileName; }

    }

    /// <summary>

    /// 新建文档

    /// </summary>

    /// <returns>操作是否成功</returns>

    public bool New()

    {

        return myForm.Function_New();

    }

    /// <summary>

    /// 保存文档

    /// </summary>

    /// <returns>操作是否成功</returns>

    public bool Save()

    {

        return myForm.Function_Save();

    }

    /// <summary>

    /// 文档另存为

    /// </summary>

    /// <returns>操作是否成功</returns>

    public bool SaveAs()

    {

        return myForm.Function_SaveAs();

    }

    /// <summary>

    /// 打开文件

    /// </summary>

    /// <returns>操作是否成功</returns>

    public bool Open()

    {

        return myForm.Function_Open();

    }

    /// <summary>

    /// 剪切

    /// </summary>

    public void Cut()

    {

        myForm.Function_Cut();

    }

    /// <summary>

    /// 复制

    /// </summary>

    public void Copy()

    {

        myForm.Function_Copy();

    }

    /// <summary>

    /// 粘帖

    /// </summary>

    public void Paste()

    {

        myForm.Function_Paste();

    }

    /// <summary>

    /// 删除

    /// </summary>

    public void Delete()

    {

        myForm.Function_Delete();

    }

    /// <summary>

    /// 全选

    /// </summary>

    public void SelectAll()

    {

        myForm.Function_SelectAll();

    }

 

}//public class DocumentClass

DocumentClass类型表示记事本当前处理的文档对象。

创建全局对象容器

为了在脚本代码中使用documentwindow这样的全局对象,笔者得创建一个类型为GlobalObject的全局对象容器,定义该类型的代码如下

namespace MyVBAScript.Global

{

    /// <summary>

    /// 定义VB.NET脚本使用的全局对象容器类型

    /// </summary>

    [Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute()]

    public class GlobalObject

    {

        internal static XVBAWindowObject myWindow = null;

        /// <summary>

        /// 全局的 window 对象

        /// </summary>

        public static XVBAWindowObject Window

        {

            get { return myWindow; }

        }

 

        internal static frmMain.DocumentClass myDocument = null;

        /// <summary>

        /// 全局 document 对象

        /// </summary>

        public static frmMain.DocumentClass Document

        {

            get { return myDocument; }

        }

    }

}

在这个脚本全局对象容器类型中,笔者添加了StandardModuleAttribute特性,并定义了WindowDocument两个静态属性。未来我们将脚本要操作的window对象和document对象设置到这两个静态属性中。

和其他类型不一样,笔者设置该类型的名称空间为MyVBAScript.Global,这样是为了将全局对象和其他类型区别开来,减少VB.NET编译器的工作量。

初始化脚本引擎

在窗体的加载事件中我们初始化脚本引擎,其代码为

private void frmMain_Load(object sender, EventArgs e)

{

    //初始化窗体

 

    // 创建脚本引擎

    myVBAEngine = new XVBAEngine();

    myVBAEngine.AddReferenceAssemblyByType(this.GetType());

    myVBAEngine.VBCompilerImports.Add("MyVBAScript.Global");

    // 设置脚本引擎全局对象

    MyVBAScript.Global.GlobalObject.myWindow = new XVBAWindowObject(this, myVBAEngine, this.Text);

    MyVBAScript.Global.GlobalObject.myDocument = new DocumentClass(this);

    // 加载演示脚本文本

    string strDemoVBS = System.IO.Path.Combine(System.Windows.Forms.Application.StartupPath, "demo.vbs");

    if (System.IO.File.Exists(strDemoVBS))

    {

        System.IO.StreamReader reader = new System.IO.StreamReader(strDemoVBS, System.Text.Encoding.GetEncoding("gb2312"));

        string script = reader.ReadToEnd();

        reader.Close();

        myVBAEngine.ScriptText = script;

        if (myVBAEngine.Compile() == false)

        {

            this.txtEditor.Text = "编译默认脚本错误:"r"n" + myVBAEngine.CompilerOutput;

        }

        // 刷新脚本方法列表

        this.RefreshScriptMethodList();

    }

}

 

这里程序首先创建了一个名为myVBAEngine的脚本引擎对象,然后向它的VBCompilerImports列表添加了全局对象容器类型所在的名称空间MyVBAScript.Global

然后程序创建一个文档对象,并填充VB脚本引擎用的全局对象容器,设置它的WindowDocument的属性值。

程序试图加载应用程序所在目录下的demo.vbs文件中的内容作为默认加载的脚本代码,若成功加载并编译成功则调用RefreshScriptMethodList来更新用户界面中的可用脚本方法列表,定义RefreshScriptMethodList函数的代码如下

/// <summary>

/// 刷新“运行脚本”按钮的下拉菜单项目,显示所有可以执行的脚本方法名称

/// </summary>

private void RefreshScriptMethodList()

{

    // 情况脚本方法名称列表

    this.btnRunScript.DropDownItems.Clear();

    // 获得脚本引擎中所有的脚本方法名称

    string[] names = myVBAEngine.ScriptMethodNames;

    if (names != null && names.Length > 0)

    {

        // 将脚本方法名称添加到“运行脚本”的下拉菜单项目中

        foreach (string name in names)

        {

            ToolStripMenuItem item = new ToolStripMenuItem();

            item.Text = name;

            item.Click += new EventHandler(ScriptItem_Click);

            btnRunScript.DropDownItems.Add(item);

        }

        myStatusLabel.Text = "共加载 " + names.Length + " 个脚本方法";

    }

    else

    {

        ToolStripMenuItem item = new ToolStripMenuItem();

        item.Enabled = false;

        item.Text = "没有加载任何脚本方法";

        btnRunScript.DropDownItems.Add(item);

        myStatusLabel.Text = "没有加载任何脚本方法";

    }

}

这个函数的功能是,使用脚本引擎的ScriptMethodNames属性获得所有可用脚本方法的名称,然后添加到工具条的“运行脚本”的下拉菜单中,于是可以到达如下的界面效果。

C#发现之旅第十四讲 基于动态编译的VB.NET脚本引擎

  编辑脚本

工具条上有一个“编辑脚本”的按钮,该按钮是点击事件处理过程为

/// <summary>

/// 编辑脚本按钮事件处理

/// </summary>

/// <param name="sender"></param>

/// <param name="e"></param>

private void btnLoadScript_Click(object sender, EventArgs e)

{

    //显示脚本文本编辑对话框

    using (dlgEditScript dlg = new dlgEditScript())

    {

        int VersionBack = myVBAEngine.ScriptVersion;

        dlg.VBAEngine = this.myVBAEngine;

        dlg.ShowDialog(this);

        if( VersionBack != myVBAEngine.ScriptVersion )

        {

            // 若脚本引擎内容发生改变则刷新脚本方法下拉菜单项目

            RefreshScriptMethodList();

        }

    }

}

这段代码中使用了脚本引擎的ScriptVersion属性,脚本引擎中每进行一次编译时都会更新因此ScriptVersion属性,因此比较该属性可以判断脚本引擎中当前执行的脚本代码是否修改过。【袁永福原创,转载请注明出处】

dlgEditScript是一个脚本代码编辑对话框,其用户界面如下

C#发现之旅第十四讲 基于动态编译的VB.NET脚本引擎

   运行脚本

该文本编辑器中,只能点击工具条的“运行脚本”的下拉菜单的某个项目才能运行脚本方法。在属性该下拉菜单的RefreshScriptMethodList中为每个菜单项目的点击事件绑定了ScriptItem_Click方法,该ScriptItem_Click代码为

/// <summary>

/// 运行脚本的下拉菜单项目点击事件处理

/// </summary>

/// <param name="sender"></param>

/// <param name="args"></param>

private void ScriptItem_Click(object sender, System.EventArgs args)

{

    ToolStripMenuItem item = (ToolStripMenuItem)sender;

    try

    {

        // 调用脚本执行指定名称的脚本方法

        myVBAEngine.Execute(item.Text, null, true);

    }

    catch (Exception ext)

    {

        System.Console.WriteLine("执行脚本 " + item.Text + " 错误:" + ext.ToString());

        MessageBox.Show(this, "执行脚本 " + item.Text + " 错误:" + ext.Message);

    }

}

ScriptItem_Click方法中,首先获得用户点击的菜单项目,然后调用脚本引擎的Execute方法来执行脚本,菜单项目显示的文本就是脚本方法的名称。

演示用脚本代码说明

程序目录下有一个demo.vbs的文本文件,该文件内容就是演示实用的VBA.NET脚本。该脚本代码为

 

sub 显示当前使用的脚本代码()

    document.text = window.engine.ScriptText

end sub

 

sub 插入当前时间()

    document.Selection = DateTime.Now().ToString("yyyy年MM月dd日HH:mm:ss")

end sub

 

sub 屏幕状态()

    window.alert("屏幕大小:" & window.ScreenWidth & " * " & window.ScreenHeight _

        & vbcrlf & "窗体位置:" & window.left & " " & window.top _

        & vbcrlf & "窗体大小:" & window.Width & " * " & window.height )

end sub

 

sub ShowText()

    window.alert( document.text )

end sub

 

sub MoveWindow()

    window.left = 100

end sub

 

sub ShowFileName()

    window.alert( "当前文件名为:" & document.FileName )

end sub

 

sub AniMoveWindow()

    window.left = window.left - 10

    if( window.left > 10 )

        window.SetTimeout( 500 , "AniMoveWindow" )

    end if

end sub

 

 

dim Rate as double

sub 模拟显示正铉曲线()

    Rate = Rate + 0.1

    if( Rate > 50 )

        exit sub

    end if

    dim strText as new string( " " , 50 + cint( math.sin( Rate ) * 30 ))

    document.AppendText( vbcrlf & strText & "######" )

    window.SetTimeout( 100 , "模拟显示正铉曲线")

    window.Title = math.sin( Rate )

end sub

 

 

dim strTitle as string = "《C#发现之旅》系列课程的VB.NET脚本演示袁永福编写版权所有2008年"

dim TitleCount as integer

sub 在标题栏显示移动字幕()

    TitleCount = TitleCount + 1

    if( TitleCount > strTitle.Length )

        TitleCount = 0

        exit sub

    end if

    window.Title = strTitle.SubString( strTitle.Length - TitleCount ,   TitleCount )

    window.SetTimeOut( 100 , "在标题栏显示移动字幕")

end sub

这里说明一下“模拟显示正铉曲线”这个脚本方法,首先定义一个Rate的全局变量作为计数器,每执行一次该计数器加一,若超过50则退出方法,脚本中使用sin函数计算出空白字符串的长度生成一个空白字符串,然后使用文档对象的AppendText方法向当前编辑的文档添加空白字符和结尾字符,这里脚本调用window对象的SetTimeout方法来延期调用这个脚本方法自己。于是这个脚本方法每隔100毫秒执行一次,并使用文本模拟显示正铉曲线,若显示了50次则停止执行。

 这样袁某就完成了一个简单的文本编辑器程序,而且该程序能使用VBA.NET脚本引擎来扩展功能,能方便的进行二次开发。

部署脚本引擎

在实际开发中,开发人员可以将XVBAEngineC#代码拷贝到应用程序中即可添加脚本功能,也可以将修改本C#工程的属性使其单独编译成一个DLL然后供其他.NET程序使用。部署起来非常方便。

小结

在本次课程中,笔者使用动态编译技术实现了VBA.NET脚本引擎,目前很多商业软件,比如OFFICEVS.NET等等都具有二次开发用的VBA脚本技术,使用本课程介绍的知识我们也可以为应用系统配置功能强大的脚本引擎,这能比较大的提高应用系统的灵活性。脚本引擎技术是一个非常实用的软件开发技术,值得推广。

你可能感兴趣的:(VB.NET)