2002-09-16 11:17
XML网络服务:使用SOAP和ASP.NET创建可复用网络部件
Andrew Clinick
Microsoft 公司
2001年9月10日
今年的早些时候,我介绍了于.NET 的Microsoft? Visual Studio? for Applications (VSA)
和脚本,并且介绍了如何使用这些技术来使你的.NET
应用程序变得可以被自定义的。由于每个人对应用程序应该是什么样子好像都有些微不同的想法,而不管它的.NET如何,因此,对一个.NET应用程序进行定义就成为一件多少有些不讨好的事情。然而,通常一种技术包含在大多数.NET应用程序-网络服务的定义中。因此,这个月我将介绍如何使用应用于.NET
Framework的VSA和脚本
来创建可自定义的网络服务,而特别是如何能帮助创建一个下层结构,使你的用户可以使用并配置可以对这个网络服务进行自定义的脚本代码,并且从创建这个下层结构获利。
为什么要创建可自定义的应用程序?
在介绍使用Visual Studio for Applications
来创建可自定义的应用程序的技术细节前,我觉得我们最好回到前面并且看一看为什么在创建.NET
应用程序时创建为可自定义的应用程序是如此重要。如果你已经对这个概念有所认识,那么就简单地跳过这节。
当设计和编写一个应用程序时,你始终面对着你的用户的要求,要为他们提供一个可以解决他们的问题并且通常使他们的生活更方便的程序。然而,不管你在创建这个程序的过程中付出多大努力去收集需求并和用户交流,总是会有所完成的程序将不能符合所有用户的需求的结论而放弃。需求不断地变化是一贯如此的 (该死!) -因为一旦用户开始认真地使用你的程序,他们就会不可避免地想到新的功能,这些功能是他们希望你的程序能实现来帮助他们工作。
如果你试图去编写将被不止一个公司使用的软件,那么决定满足哪个用户的需求,解决哪个用户的问题就变得十分困难。例如,如果你正在编写一个订单处理系统,那么你能够提供一个100%满足你的客户的恰当的商业规则的机会是什么?对于一个没有自定义的普通程序片断不可能同时满足所有用户的需求。
你怎样选择来使你的程序可自定义的对你的用户,对你的升级能力,同时对你的产品的销售都会产生重大的影响。你有许多可选的选择项目(重要性没有特殊顺序):
不提供自定义的能力
出于我已经给出的所有原因,我将假定这不是一个很好的选择。
为应用程序提供源代码,这样你的客户就可以对应用程序进行修改来满足他们的需要
这提供了一些好处,其中从根本上是为你的客户提供了完全的灵活性并且控制你的产品该如何执行。这也许是一个可行的并且有吸引力的选项。然而,因此又冒出了许多问题,包括升级到新版本和保护你的知识产权的问题。我在这里不讨论知识产权(没有我的参与,在互联网上也有足够的人在讨论这一点),但是升级问题对我们所有人都有影响。
例如,我们假设你提供了你的产品的1.0版的源代码,而你的一些用户更改了某些特殊部件的执行。(我将用订单处理系统的税务计算模块作为例子。)我们假定现在内部的执行事不同的,而这个模块的外部界面被改为与客户的内部发票处理系统集成在一起。好的消息是,用户现在已经成功地实现把你的系统和他们的集成在一起,而且他们很高兴。
在预计的时间内,你开始工作于你的产品的2.0版本,并且用户也因为你添加的新功能而对购买新版本很有兴趣。接下来你会去安装产品的2.0版本,但是发现由于税务计算模块已经被更改,除了试图把你添加的更改和那些由用户做的更改合并到一起外,没有其他容易一些的方法来进行升级。
从表面上看,这好像不是一个主要的问题,但是当你增加系统各处的更改时,这就很快变成一个问题了。试想如果为了满足客户的需要对整个系统的各部分都进行小更改。如果你试图保留用户已经对系统所作的自定义的,那么提供一个从1.0版到2.0版的升级就变得令人吃惊的困难。实际上,这很快就会导致不能进行升级,因此,无论用户是否希望升级到最新版本,他们都被限制在你的软件的老版本-而你也被困于试图去对你的软件的无数不同版本进行支持。
为你的应用程序提供脚本解决方案
把脚本创建到你的应用程序中提供了一种机制,使你可以不用把你的应用程序的源代码公布给你的用户就可以进行自定义的。它也允许你在你的应用程序中设计详细的自定义的点。如果你要把在自定义的投资最大化,那么为你的应用程序设计自定义的点大概是你的应用程序设计中最重要的部分(并且有时要占用最多的时间)。一个好的自定义的设计过程将不仅仅在自定义的你的系统时节省时间,并且使你能够对那些要进行自定义的和什么时候进行自定义的进行控制。
脚本方法优于源代码的最大好处就是自定义的可以在产品升级时得以保存。例如,改变税务计算模块执行的脚本可以在所安装的模块的2.0版本中继续使用(当然,假设目标模块是向后兼容的),这是由于自定义的是脚本的一部分而不是这个模块的完全实现。
Visual Studio for Applications提供了一种机制,它从把应用于.NET Framework的脚本集成到你的应用程序中获利。也就是,它为你的客户提供了一个全特性,集成的开发环境来编写和调试脚本。
为客户提供一种方法来把模块联接到你的应用程序中
这是一种有趣的选择,并且提供了许多脚本解决方案的好处,但是也给与你,应用程序开发者和你的应用程序的客户很多挑战。这项选择有些类似于由Microsoft?
Office和Visual Studio
所提供的“添加”模块。你的应用程序提供的集成界面部件需要一种机制,就像在适当的时候把添加的部件加载到你的应用程序中那样。
如果承担应用程序自定义的任务的大多数人都是有经验的开发者,他们熟悉创建部件并且对接口有很好的理解,那么使用这种方法就是一种值得考虑的选择。出于这个原因,我认为这对脚本来说是一种免费赠送的特性,并且不应当被看作是与把脚本添加到你的应用程序中相抵触。这就是说,当开发一个应用程序时就会涉及到许多挑战-特别是许多执行解决方案时涉及的工作。你需要开发一个集成接口,一种把模块加载到你的应用程序中的机制,并且提供这种集成机制的相关文档。这并不需要很困难,但是如果你选择脚本方法,它就是你不需要的东西。
开发可自定义的网络服务
为了说明怎样使用.NET Framework脚本和Visual Studio
for
Applications来创建一个可自定义的网络服务,我将建立一个用于scripthappens.com的简单的计算网络服务。这个网络服务部件被故意做得很简单,因此我就可以全神贯注于介绍如果集成脚本,而不是陷于处理税务计算所需要的代码中。这个网络服务有两个方法CalculateTax
和 CalculateDiscount, 并且将被scripthappens.com 电子商务网站的处理系统调用。服务自身完全在Microsoft?
Visual Basic? .NET 中执行,但是用任何一种.NET语言都可以很好地开发。网络服务提供了一个.NET
Framework脚本引擎运行脚本来对系统运行进行自定义的,对脚本编写者给予帮助,提供了一个代表了网络服务接受到服务要求的状态的对象模型。
网络服务被设计成要从其他网络应用程序调用,包括所有在服务器和用户机上的网络应用程序(Internet Explorer网络服务行为)。由于使用完全是基于网络设计的,所以我认为所集成的Visual Studio for Applications 集成开发环境 (VSA IDE)可以通过网络接口被调用也是很重要的。为了实现这一点,我建立了一个简单的系统,它使用XML和MIME文件映射,可以通过在一个网络浏览器上的网页来启动VSAIDE.后面更多地讨论这个系统如何工作。
例子使用了Visual Studio for Applications 软件开发工具包(VSASDK)来主机.NET Framework引擎脚本和VSA IDE 。VSA SDK的关键设计点之一是允许应用程序通过VSA来确定用户编写的脚本代码的存储格式和位置。为了实现这个目的,与SDK结合的主类向应用程序开发者提供了一个机制,可以嵌入到开发者自己所坚持的机制中。这是通过编写一个部件来实现的,这个部件是一个使用固定接口的代码提供者,ICodeProvider ,在VSA SDK中定义。VSA SDK中的主类将把这个代码管理器用于应用程序中所有的固定和修补的脚本代码中,而且都在设计时(从VSA IDE当中)和运行过程中脚本被加载时。
由于编写VSA IDE中的脚本的人很可能与存储和使用这个脚本代码的人使用不同的机器,所以代码管理器可以被远程调用就十分重要了。因此,VSA SDK被设计成远程使用代码管理器,就像网络服务通过HTTP协议使用SOAP。这个示例应用程序使用一个代码管理器作为VSA IDE的网络服务,而在运行时要加载代码来对网络服务进行自定义的时,在本地作为一个.NET部件。
图 1.
Scripthappens.com 计算网络服务的结构
现在你该很希望对系统怎样装配到一起而成为一个整体有一个通常的理解,下面我就将进入执行的细节。
执行网络服务
计算网络服务作为一个标准.NET网络服务被执行,calculate.asmx
,和两个方法在使用了WebMethod属性的类中被定义为功能。WebMethod属性告诉ASP.NET在运行时放开这个方法,因此它可以被一个网络服务调用-迄今为止没有任何特例。
为了使网络服务可以被自定义的,一个脚本引擎需要被加载来运行任何用户编写的脚本代码。在我最近的专栏中,我直接使用.NET Framework接口脚本,并且每次都调用源脚本代码。这是很好的事情,由于它是一个客户应用程序,并且脚本代码的性能并不是这个应用程序主要考虑的问题。由于这是一个将用在一个服务器上的网络服务,无论如何,运行时的性能都是关键。出于这个原因,我将使用.NET Framework引擎脚本的能力来加载预编译代码-特别是可以只调用予编译代码的轻-重量脚本引擎(light-weight script engine)(我在上个月当作调用程序提到了这个引擎)。很幸运的是VSA SDK提供了运行时集成类,它使运行引擎成为小菜一碟,因此我使用它把脚本代码集成到网络服务中。
VSA SDK运行类被设计来提供一个IVsa接口的抽象,并且提供一个有效的途径来把引擎集成到你的应用程序中。由于你的应用程序很可能在一大堆部件中集成这个脚本引擎,因此我们尽量保证代码所需要使用的类的数量保持最少。我特别希望集成可以用三行代码完成。(三行代码对迷你代码来说好像是很合适。虽然为什么三行代码是重要数字已经迷失在VSA设计过程的迷雾中。)
计算网络服务的构造函数创建了一个运行库和代码管理器的实例,它将被用来给部件加载编译过的脚本。我将在后面文章中介绍代码管理器是怎样执行的,但是它是一个执行ICodeProvider 的类,并且已经被包括在项目中,所以就只是一个创建新实例的问题了。
运行时类有许多重载构造函数。既然这样,我就使用了我认为将是最常用的构造函数,它使用自定义的名字和将要用的名称。名称大概是VSA中最重要的概念之一,因为它对于.NET Framework脚本和VSA IDE来说都是至关重要的。实质上,名称是脚本代码的唯一标识,而你应当注意的是你如何去构造它。名称对于通过代码管理器保存和重新找回任何代码也是很重要的。一个代码管理器实质上是为你的应用程序把一个名称转换为存储机制。例如,这个例子中的代码管理器把com.scripthappens://calculate 转换为在当前工作目录下的一个层次文件夹vsa projects\calculate 。
一旦运行时类的实例被创建,所有剩下的事情就是来提供一个代码管理器的实例,类将使用这个实例来找回任意脚本代码,告诉类去调用这段代码,并且接下来运行这段代码-所有工作都在三行代码中完成!(好的,因此我不会对创建的代码管理器或类的实例进行技术,而他们不会记录在我的代码计数器的行数记录当中。)
Try
'
创建代码管理器的新实例
myCodeProvider = New
DiskCodeProvider()
'
创建运行时类的新实例
myRTClass = New Runtime("calculator",
"com.scripthappens://calculate")
'
把代码管理器设置为磁盘代码管理器的实例
myRTClass.SetCodeProviderLocal(myCodeProvider)
' 加载已编译的代码
myRTClass.Load(Nothing)
' 运行代码
myRTClass.Run()
Catch e As
Exception
'
抛出一个相当没用的意外来告诉用户某些工作开始了
' 致命错误
Throw New Exception("Unable to load the
customization")
End Try
网络服务现在已经准备好运行任何用户认为合适的脚本了,但是迄今为止我们没有为脚本提供任何对象模型来照着编写。运行时类提供了两个方法来把对象添加到引擎中:AddObject 和AddEventSourceObject 。AddObject 把对象添加到引擎的全局范围中,但是脚本代码不能响应任何由这些对象激发的事件。AddEventSourceObject 把对象添加到类或者模块中,并且不使人惊奇的是,源于这个对象的事件可以被描述。
为了保持简单化,计算网络服务有一个激发事件的对象,称为CalculateObjectModel (是不是很容易记住的名字?)。在这里我将与你一起在最前面,这里,在Beta 2版中有一个错误,为什么运行时类要检查是否你提供的对象实例非空。本应当引起注意在构造函数中把eventsourceobject 添加到引擎中,但是我需要使用对象中的构造函数来设置内部只读的属性,基于把数据传送到网络的方法。由于那些数据在构造时是不可得的,我必须把addeventsourceobject 调用添加到每个方法中。Visual Studio for Applications 最终版本没有对实例的数据进行检查,这就允许我把一个空实例传入并且在方法被调用时创建实例。
正确获得对象模型
当设计这个网络服务时,我试着去想关于用户怎样才能进行自定义的来符合他们的要求。同样的,我花了许多时间来设计对象模型。这个我最终得到的模型是一个在税务计算进行前后都会激发事件的模型。我认为这将给出更多的灵活性,由于它将运行主要的事情-执行你自己的税务计算例程-并且也允许脚本作者做一些他们希望的后面的计算处理。
一旦我决定用这个前和后处理事件模型,我就开始考虑关于我如何装配这些对象模型来使脚本编写员的生活更轻松些-不影响到网络服务的外部设计。一个重要的设计目标是,在提供一个可以简单地从脚本语言调用美国网络方法来访问内部状态的对象模型同时,确保网络服务在实际执行中保持本来的无国籍(stateless)。为了达到这个目的,我创建了一个新的类,它执行脚本编写者将会看到的对象模型,并且在美国网络方法中使用它。
为了说明这个,我将解释我怎样在网络服务中执行CalculateTax 方法。这个方法相当简单。它得出一个总数,和进行税务计算所得到的状态,作为证明。(我知道这是一个非常美国中心化的方法,但是它毕竟只是个演示。)为了确保网络服务在执行中保持本来的无国籍,总数和状态信息不会送到方法的外部。无国籍编程对于可测量性来说有重要的好处,但是并不需要把变成变得复杂-特别是如果你不是一个有经验的程序员,这对于那些编写脚本来自定义的你的应用程序的人来说总是个问题。
为了帮助脚本编写者,我创建了一个简单的对象模型,它把当前网络方法的状态作为只读属性表现出来(这些数值在类的构造函数中设置)和有很多可以用来返回脚本自定义的结果的可读/写的属性。除了属性之外,这个对象也将给出脚本编写者将用来对其变写代码的事件模型。我为这个前和后处理事件使用了一个简单名字转换BeforeEventName 和 AfterEventName 。
Public Class CalculateObjectModel
'
处理数据的成员变量
Private m_amount As
Decimal
Private m_state As String
Private m_taxAmount As Decimal
Private m_discount As
Decimal
Private m_custID As Integer
' 定义事件
Event BeforeCalculateTax()
Event AfterCalculateTax()
Event
BeforeCalculateDiscount()
Event
AfterCalculateDiscount()
'
用来设置总数和状态的构造函数
Public Sub New(ByVal amount As Decimal,
ByVal state As String)
m_amount =
amount
m_state =
state
m_custID =
Nothing
End Sub
' 用来设置总数和custID
的构造函数
Public Sub New(ByVal amount As Decimal, ByVal custID
As Integer)
m_amount =
amount
m_custID =
custID
m_state =
Nothing
End Sub
'
为了让编写人员更简单编写代码的一些属性
Public Property Discount() As
Decimal
Get
Return
m_discount
End
Get
Set(ByVal Value As
Decimal)
m_discount = Value
End
Set
End Property
Public Property
Tax() As Decimal
Get
Return
m_taxAmount
End
Get
Set(ByVal Value As
Decimal)
m_taxAmount = Value
End
Set
End Property
Public ReadOnly
Property Amount() As Decimal
Get
Return
m_amount
End
Get
End Property
Public ReadOnly
Property state() As String
Get
Return
m_state
End
Get
End Property
Public ReadOnly
Property CustID() As Integer
Get
Return
m_custID
End
Get
End Property
End Class
为脚本提供对象模型
一个设计得很好的对象模型是伟大的,但是如果它不提供脚本引擎就是没有用处的,而对象中的事件也不会按正确的顺序被激发。网络服务中的CalculateTax
方法在我的例子中不会真正地做很多事情,与创建对象模型的一个实例不同,在对象模型中激发事件BeforeCalculateTax
,从对象返回税务总数,并且激发AfterCalculateTax 事件。很明显,一个真正的网络服务将在激发事件间做更多的事情。
在方法的执行过程中,我我遇到了一项有趣的挑战,而我认为当你开始编写你的对象模型时也会遇到这个问题。也就是:我如何在一个类的新实例中产生事件?通常,你会使用RaiseEvent 方法来激发事件,但是不幸的是用来激发事件的代码没有运行在对象模型的实例中,因此在对象模型和调用代码间就需要一些阶层的简介联系。
最初,我认为这应该是很容易解决的-只是在对象模型中执行FireEvent 方法并且把你想激发的事件的名称传递过去就行了,并且它就是那样。像这样做工作正常,但是有一个不幸的附加效果:方法FireEvent 暴露给脚本编写者,这将导致一些相当有趣的死锁状态。试想如果BeforeCalculateTax 的事件句柄与BeforeCalculateTax同时调用方法FireEvent -无限循环和咬牙切齿就跟着发生了。幸运的是,这可以简单地通过把方法FireEvent 设定为内部的或Visual Basic用法中的“友”来避免,这样做的意思是,方法会被运行在同一集合中的代码所看到而对于脚本代码是不可见的。
Public Function CalculateTax(ByVal amount As Decimal,_
ByVal
state As String) As Double
'
为脚本创建对象模型的实例
'
并且在构造函数中设置总数和状态
CalcOM = New
CalculateObjectModel(amount, state)
Try
' 用VSA
Beta
2编程在引擎运行前不能有EventSourceObject的空实例
myRTClass.AddEventSourceObject("CalculateCustomization",
"CalcOM",
CalcOM)
'
需要重置引擎来确保可以得到新eventsourceobject
myRTClass.Reset()
' 再次运行代码
myRTClass.Run()
Catch e As
Exception
' 抛出意外
Throw e
End Try
'
在对象模型中调用FireEvent方法来激活BeforeCalculateTax事件
CalcOM.FireEvent("BeforeCalculateTax")
'
当然实际上你很可能在这里做一些通常的计算
'
把$1加到税上来演示发生了一些事情
CalcOM.Tax +=
1
'
在对象模型中调用FireEvent方法来激发AfterCalculateTax事件
CalcOM.FireEvent("AfterCalculateTax")
'
从对象模型返回税务计算的总数
Return
CalcOM.Tax
End Function
当网络服务被调用时,脚本代码将真正的执行税务计算,并且服务将通过对象模型中的税属性从脚本返回结果。所有现在剩下的都是把脚本编写者的能力添加到实际编写脚本和把它存到服务器当中。
为脚本提供开发环境
.NET Framework和VSA的脚本提供的超过Microsoft?
Windows?脚本的一个主要好处是有完全特性的VSA
IDE,它为脚本编写者创建它们自己的自定义的脚本提供了第一流的编辑和调试环境。scripthappens.com 网络服务无论在编辑和调试脚本代码方面都从VSA
IDE获得完全的好处。把VSA IDE集成到你的应用程序中是通过为你要使用的脚本语言设计时引擎而完成的。(在Visual Studio for
Applications 1.0版中,我们只有时间去为Visual Basic .NET 设计事件引擎。)
设计时引擎的关键设计点是它应该和用来控制像代码项目和对象的脚本引擎(IVsa )有相同的接口(因此你就不必为了公共功能学习不同的主接口)。当然它应该也装配一系列设计时接口(IVsaDT ),来提供对VSA IDE的访问。VSA SDK提供了一个设计时集成类,它使得对VSA IDE集成变得容易,很大程度上与运行时类使得主管运行一个脚本引擎变得一样的方法相同。
由于网络服务没有一个特定的Windows用户,我设计了一个机制藉此VSA IDE可以从一个网络浏览器来例示。由于VSA IDE可以被一系列接口所控制,在网页中从脚本调用IDE就不是一个选择了。Windows脚本只能通过IDispatch 调用对象,并且VSA IDE可以做一些潜在不安全的事情(像从磁盘上读写),因此这将不能与大多数网站的主要安全框架在一起工作。为了提供从一个浏览器对VSA IDC的访问,我已经从国际互联网探索器的MIME处理特性得到好处来创建.NET Windows Forms应用程序,解释一个XML文件(由网络服务器产生),并且使用IvsaDT接口来启动VSA IDE。在我介绍程序怎样做这些事情的细节之前,对Microsoft? Internet Explorer MIME句柄做些解释在这种请况下就变得重要了。
Internet Explorer使用内置的Windows能力来允许MIME所包含的类型与应用程序联合使用。MIME包括的类型是你在Windows中应该很熟悉的相似文件扩展名映射的超集。我创建一个MIME包含类型,VSA配置文件,并且用用扩展名.vsa代表包含类型。 使用可以从文件夹选项控制面板小应用程序(applet)得到的文件类型编辑器,我把MIME包含的类型与运行VSA IDE的.NET应用程序结合在一起。结果是,无论何时一个.vsa或VSA配置文件MIME包含类型被下载,VSA IDE主应用程序都将被运行,将解释文件中的XML,并且使用包含在XML中的信息来加载脚本代码并展示VSA IDE。这个方法当然不是尖端科学,并且在许多方面它的技术非常低。然而它为从一个网络浏览器内部运行VSA IDE提供一个可扩展的和一流的解决方案。
这种方法有一个很明显的问题,然而:你怎样把MIME映射和应用程序添加到你的用户机器上,如果他们在建立你的应用程序之前就下载了.vsa文件呢?幸运的是,Windows为这个提供了一个很好的解决方案,但是为了运行,它要求你的用户在有一个域服务器的内部局域网上面工作。
如果你的机器联接到一个域,这样当一个用户下载一个已经告知管理器的文件类型的文件时你就可以告知MIME和文件扩展名管理器,并且他们不会安装软件,Windows会自动为他们安装管理器。这意味着你可以确保你的用户在下载一个.vsa类型文件时总是会另VSA IDE显露出来。需要更多关于安装这个解决方案的信息,可以查看Step-by-Step Guide to Software Installation and Maintenance 。
管理VSA IDE
现在我们已经知道MIME管理器怎么能提供一种机制来帮助通过一个网络浏览器管理VSA
IDE,那么让我们看一看,.NET Windows Forms程序实际是怎样使用VSA SDK 设计时类与VSA
IDE和代码管理器进行信息交换,来找到并存储源脚本和已编译的代码。
这个例子中的主应用程序的关键是在.vsa文件中包含的XML。我们所选择用于例子的XML计划提供了所有需要加载脚本代码和脚本对象模型的信息。很明显,由于网络服务的对象模型是相当知名的,因此我们本应该很把它牢固地嵌入到应用程序中。无论如何,我们认为试图开发一个你可以当作你将来的Visual Studio for Applications项目的基础的通用系统是重要的。例如,如果我们为scripthappens.com真正开发这个系统,有一个我们可以在将来的项目中再次使用的解决方案将是件很不错的事情。
XML模式例子
<application
name="calculator"
targeturl="http://localhost/ScriptHappens/default.aspx"
moniker="com.scripthappens://calculate"
language="Microsoft.VisualBasic.Vsa"
codeProviderURL="http://localhost/scripthappens/codeprovider.asmx"
>
<reference
name="ScriptHappens"
assembly="C:\\Inetpub\\wwwroot\\scripthappens\\bin\\scripthappens.dll"
/>
<class name="Calculate"
>
<event name="calculateObjectModel"
type = "scripthappens.calculateObjectModel" />
</class>
</application>
这个程序的主要部分是应用程序元素。里面包含所有程序相关的信息,包括名称和自定义的名字,这应该与运行时集成相似。除了运行时,targetURL和代码管理器网络服务外,设计时引入了许多概念。targetURL 用于用户在他们的VSA IDE中运行他们的代码的时候。由于你的应用程序实际上将控制用户编写的脚本代码的运行,而不是IDE,所以VSA IDE不能只是运行代码。
为了解决这个问题,IDE将回调主应用程序来做加载脚本代码所需要的事情。由于这个例子展示了如何在网络服务中使用脚本,targetURL 就被添加到调用计算网络服务的网页中。这意味着在VSA IDE中运行代码将造成计算网络服务被启动,而任何用户添加到他/她的脚本代码中的断点将造成IDE在网络服务运行时调试他们的脚本。
VSA Design-Time 类使用一个代码管理器来处理脚本代码的持久性,但是代替使用局部实例,它将把代码管理器作为网络服务来调用。对于VSA SDK来说这是关键的设计点。我们希望确保代码的持久性已经是可能的事情,并且特别是它要可以处理远程代码存储,而这些代码也许是在防火墙后面,因为这对我们来说是关键的情节。为了通过一个网络服务调用代码管理器,所有你所需要的是使用方法SetCodeProviderRemote 并且给执行IcodeProvider 的网络服务提供URL。在例子中,我已经让代码管理器在codeprovider.asmx 中执行。这里我们感兴趣的是,计算网络服务在局部使用代码管理器。那就是,它不会通过SOAP创建实例,但是使用与网络服务相同的执行。
设立设计时(Design Time) 引擎
从应用程序部件获得的信息会在创建设计时类时使用。设计时类与运行时间类的设计多少有些不同。它需要可以允许主应用程序管理任何引擎,因为VSA
IDE将被用来编辑系统中的代码,并且这里将会有多于一个的引擎在系统中使用来提供自定义的。例如,如果在解决方案中有两个网络服务,那么每个网络服务中就会有一个脚本引擎。VSA
IDE将不得不对两个脚本项目都进行引导。结果是,设计时类提供了一个简单的方法来创建多个设计时引擎。比通过引擎中的接口提供一个抽象要好,它只是传回一个引擎的实例,你将依靠Ivsa和IvsaDT对其进行编程。为了说明这一点,我将不去注意代码的要求来设立设计时引擎并运行它。
XML程序将在主应用程序的Windows Form的加载事件中进行解析。为了对XML进行解析,我使用了XML解析器,它作为.NET Framework的一部分使用,简单地通过应用程序部件的属性重复,并为后面使用设计时类存储数据。为了提供一些什么在进行的用户反馈,程序使用Windows Form Progress Bar 控件并且把过程数值设置为只读属性。这相当简单,但是相当有效。
'
打开在命令行输入的XML文件
Dim xml As
XmlDocument
xml = New
XmlDocument()
xml.Load(args(1))
Dim node As
XmlNode
node = xml.SelectSingleNode("application")
Me.ProgressBar1.Value =
5
'
忽略whitespace
' myXML.WhitespaceHandling =
WhitespaceHandling.None
Me.ProgressBar1.Value = 10
'
得到名字
custName =
node.Attributes.ItemOf("name").Value
Me.ProgressBar1.Value = 15
'
得到targeturl
targetUrl =
node.Attributes.ItemOf("targeturl").Value
Me.ProgressBar1.Value =
20
' 得到名称
moniker =
node.Attributes.ItemOf("moniker").Value
Me.ProgressBar1.Value = 25
'
得到引擎语言
language =
node.Attributes.ItemOf("language").Value
Me.ProgressBar1.Value = 30
'
得到代码管理器URL
codeProviderURL =
node.Attributes.ItemOf
("codeProviderURL").Value
Me.ProgressBar1.Value = 35
一旦所有应用程序部件的信息都被解析,程序就会有足够的信息来开始使用设计时类。在scripthappens.com 类中,我创建了一个类,它扩展了VSA 设计时类来使得集成更简单。我当然推荐你在使用设计时类的时候做相似的事情。这个类有一个把所有信息包含在应用程序部件中的构造函数,因此它只关系到创建类的一个实例并把所有消息传送到构造函数中。
' 创建webhost类的实例
myhost = New dthost(Me, custName, moniker,
targetUrl, language,
codeProviderURL)
Me.ProgressBar1.Value = 60
类dthost
的构造函数使用代码管理器URL为设计时类设置代码管理器来加载脚本代码。作为防范,类要进行检查来看看URL是否为空和它是否使用局部代码管理器。一旦代码管理器被设立,设计时类就有了所有需要用来加载或创建一个VSA项目的信息,因此可以很安全地创建一个新的设计时引擎。设计时类被设计为可以很轻松地通过提供一个引擎的集合来处理多个引擎,VsaEngines
。这个集合有一个方法,create
,它将返回一个新引擎并且添加到这个集合中。在这个例子中,为了保持简单,这里只使用了一个引擎,但是有一个集合将使你的生活稍微轻松一些。
'------------------------------------------------------------------
'
为这个引擎设置代码管理器
'------------------------------------------------------------------
If "" = strCodeProviderURL
Then
SetCodeProviderLocal(New
DiskCodeProvider())
Else
SetCodeProviderRemote(strCodeProviderURL)
End If
一旦代码管理器被设置完毕,主类就将查看这里是否已经提供了这个名称的存在地项目。这实现起来相当简单,只是调用基类VSA SDK DT中的方法 LoadEngineSource 并且抓住任何意外。如果项目已经存在,那么我们就做得差不多了,因为项目中包含了对象模型和引用所需要的所有信息。然而如果项目不存在,那么我们就需要从集合中移走老的引擎,并且继续白手起家创建新的引擎。新的引擎将从现在开始使用来添加代码,对象模型和参考。在这阶段,除了我们要对引擎进行初始化就没有别的什么事情了,因此代码调用引擎中的方法InitNew 。这告诉引擎已经进行了初始化,而它可以继续并且完成创建新引擎的过程。
Try
'------------------------------------------------------------------
'
试图加载我们的引擎
'------------------------------------------------------------------
LoadEngineSource(strMoniker, Nothing)
'------------------------------------------------------------------
'
如果成功把newEngine设置为False
'------------------------------------------------------------------
newEngine = False
Catch e As
Exception
'------------------------------------------------------------------
'
加载失败这是一个新类型引擎
'------------------------------------------------------------------
VsaEngines.Remove(strMoniker)
dtEngine = VsaEngines.Create(strMoniker, strLanguage,
Nothing)
engine = dtEngine
engine.InitNew()
'------------------------------------------------------------------
'
设置引擎名称
'------------------------------------------------------------------
engine.Name =
strCustName
End Try
在主应用程序中,有一个对dtClass 的接口中的属性NewEngine 的简单检查。如果是一个新引擎,那么我们需要把参考和对象模型添加到引擎中;否则,就没有事情要做,因为项目已经被加载了。
把参考和对象模型添加到引擎中
这里我们有一个已经准备好为项目添加源代码、对象模型和参考的DT引擎。主应用程序使用的XML程序包含所有将被添加到VSA项目中的参考信息,因此这是需要添加到引擎中的第一样东西。把参考添加到设计时引擎与有脚本的.NET
Framework引擎十分相同。所有的引擎都执行IVsa 。这只是用VsaItemType.Reference 的项目类型调用CreateItem
并把项目中的AssemblyName 设置为集合的路径的一种方式。XML包含要加载的参考的名字和路径,因此这就相当简单了。为了使这更简单,类dtHost
通过方法AddReference提供了这样的功能,它取得了名字和路径。
'获得参考
nodeList =
node.SelectNodes("reference")
For Each subNode In
nodeList
refName =
subNode.Attributes.ItemOf("name").Value
refAssembly =
subNode.Attributes.ItemOf("assembly").Value
myhost.AddReference(refName,
refAssembly)
Next
一旦所有的参考被添加盗引擎中,所有要去做的事情就是添加对象模型,脚本编写者将用它来编程。向引擎中添加对象是通过调用引擎的一个代码项目中的方法AddEventSource 而实现的。在例子中使用的XML中,所有的对象都是事件源对象,它们将被添加到类项目中。为了帮助这些动作的执行,类dtHost 提供了一个创建和简介事件源对象的代码项目的抽象。
Dim nodeList As
XmlNodeList
nodeList = node.SelectNodes("class")
Dim classNode As
XmlNode
Dim stepit As Integer
Dim eventNodeList As XmlNodeList
stepit = 20 / nodeList.Count
For Each classNode In
nodeList
className =
classNode.Attributes.ItemOf("name").Value
myhost.AddClass(className)
eventNodeList =
classNode.SelectNodes("event")
Dim eventNode As XmlNode
For Each eventNode In
eventNodeList
eventName =
eventNode.Attributes.ItemOf("name").Value
eventType =
eventNode.Attributes.ItemOf("type").Value
myhost.AddEvent(className, eventName,
eventType)
Next
Me.ProgressBar1.Value = Me.ProgressBar1.Value +
stepit
Next
对象模型在服务器上的装配和性能
在运行服务器上的脚本时决定如果把对象模型添加到引擎中是很重要的,这里脚本代码的速度和可测量性都是很重要的。一个VSA引擎可以用两种方式添加对象:作为一个全局对象(不能激发事件)和作为一个事件源对象。全局对象令人惊讶的在脚本代码的全局都有效,而结果是定义好不变的。静态对象在一个单线程环境中运行时是很好的,但是对于多线程环境的关注也是很重要的考虑。
在一个多线程的环境中,这样的代码在ASP.NET服务器中运行,那里在任何时刻都潜在地会有许多单脚本的实例在运行,并且用脚本声明的静态变量将被几乎所有脚本所共享。因此,如果一个全局对象在脚本中,那么这个运行的脚本的所有实例都将共享相同的对象实例。
为了说明这个潜在的问题,试想如果脚本中有一个名为“title”的简单字符串属性定义的全局对象。脚本的第一个实例把title属性设置为“Hello World,”,并且会返回属性的数值。在只有一个脚本实例在运行时,这是很好的。现在再设想一个相同的情节,但是这次是在设置属性和返回数值之间,第二个引擎加载了一些但脚本稍微不同的相同对象模型。第二个脚本的第一行把title属性设置为“hi from script 2”。由于在第一个脚本返回属性的数值时,对象模型的实例被两个脚本所共享,那么就会得到“hi from script 2”。两个脚本在同一时刻都试图去访问属性时,事情也许会变得更糟-不是很好的景象。
幸运的是,这里有一种方法上下文隔离,来解决多多线程环境中的静态变量的问题。上下文隔离确保静态变量不能在所有实例中被共享,而Visual Studio for Applications 使用这种机制来确保你不会陷入我前面所描述的竞争问题。上下文隔离通过为每个线程创建静态变量来管理这些。运行在每个线程中的脚本代码把静态代码看作与以前一样,但是它看到的是为线程提供的上下文隔离的拷贝。
不幸的是,使用上下文隔离不是没有代价的。为每个静态变量创建一份拷贝,并且提供一个下部基础来处理所有这些拷贝,招致要考虑性能的降低。在我们的性能测试站点,我们看到与使用实例变量相比性能下降了50%。
为了避免使用静态变量,并且避免他们的性能影响,看来要采用依靠服务器的方法了。由于创建在服务器上有效运行的自定义的代码是我们这版中的一员,因此幸运的是这是我们已经加到Visual Studio for Applications的东西。这里有两种方法让你可以把对象模型添加到得到静态声明变量的VSA:全局对象,和模型中包含的事件源对象。然而,添加到类项目中的事件源对象得到实例变量,它们将没有任何性能问题。
如果你对模块中的事件源对象必须是静态的感到疑惑;原因就是在模块中声明的所有变量都是静态的。因此-并且这将很有希望不会造成惊奇-我极力推荐你在类中给你为了在服务器上运行脚本提供的全部对象模块使用事件源对象。如果你使用静态对象,那么VSA确保变量的什么包含ContextStatic 属性,它告诉.NET创建上下文隔离,这样就不会有脚本代码陷入竞争的问题了。(这一定会比它需要的慢很多。)因此所有例子中的事件源对象都被添加到类中来给出一个很好的例子。
Me.ProgressBar1.Value =
90
myhost.InitCompleted()
Me.ProgressBar1.Value =
100
Button1.Visible = True
在使用引擎前,所有还需要做的事情是通过调用方法InitCompleted 来告诉它初始化已经完成。
'
对引擎做的所有事情都已经完成,因此调用InitCompleted
dtEngine.InitCompleted()
介绍IDE
现在VSA设计时引擎已经得到所有所需的信息来允许开始编辑脚本,因此主应用程序需要通知引擎使IDE可见。幸运的是,这非常非常容易实现,只要调用引擎中的方法ShowIDE
就可以了。为了使这个过程显得有些活力,类dtHost 提供了一个方法Show 去调用方法ShowIDE
,而把它包含在一个catch块中来俘获任何意外并且为使用类dtHost
的应用程序抛出适当的意外。在显示了IDE之后,主应用程序把自己最小化来使用户可以看到VSA IDE。
'显示VSA
IDE
myhost.Show()
'把自己最小化使用户可以看到VSA
IDE
Me.WindowState = FormWindowState.Minimized
编写脚本代码
可以得到给一个应用程序编写脚本的全特性开发环境所需要的事情是你不再必须做你自己的(这通常是作为文本框的改变而结束。)。同样,因为IDE为你提供了所有信息,而你也不用为可以得到哪些对象而担心,因此编写脚本是如此的简单。
当显示调用把VSA IDE第一次显示在用户面前时,将向用户介绍所有从XML添加的项目信息,而一个代码窗口将显示添加到项目中的类。
图2. VSA IDE
显示项目和类
为了编写脚本代码来自定义的计算对象,脚本编写着者必须选择他或她想要描述的事件。在VSA IDE中做这件事情是相当简单的;只是从代码编辑器上面的对象列表中选择calculateObjectModel,而接下来从事件列表中选择事件。
图 3. 对象列表框
图4. 事件信息
选择事件将造成一个事件句柄被自动添加到类中,脚本编写者就可以用来编写脚本了。
图5. 所添加的事件句柄
由于VSA IDE给为VSA引擎提供的对象提供了完全的Microsoft? IntelliSense?支持,因此对应用程序提供的对象模块进行访问也是相当简单的。用户只需要输入对象的名称和域,VSA IDE就会提供所有对象成员的列表。如果你已经使用过Visual Basic for Applications (VBA),这对你来说不应当陌生,但是对于一个脚本用户,这是一个主要步骤。
图6.
calculateObjectModel的成员信息
保存代码
一旦你满意你编写的代码,你就需要能保存它。Visual Studio for
Applications 和.NET
Framework脚本的一个关键好处是用来运行脚本的应用程序可以确定这些代码存储在哪里。所有开发者都对点击保存安钮感到厌烦。当你点击保存,VSA
IDE将回调主应用程序并且提供你已经写好的所有代码,因此主应用程序可以保存它。你的应用程序选择保存你的应用程序代码的地方对你完全是不隐瞒的。许多人选择把它保存在数据库中。为了保持事情的简单,scripthappens.com
计算网络服务把它的代码存储在磁盘上scripthappens vroot 的文件夹中。
VSA SDK关注大多数要保存代码的下层结构,来用代码管理器真正实现代码的延续。你所有的代码所需要做的是确定是保存为源代码形式还是编译后的形式。通常,你会希望保存编译后的代码,特别是当编写服务器自定义的时。在例子的代码中,类dtHost 提供了一个先保存源代码,编译这个源代码(只是在还没有编译时),最后再保存编译后的状态的方法SaveVsaEngine 。
Sub SaveVsaEngine()
Try
'------------------------------------------------------------------
' 保存源状态
'------------------------------------------------------------------
SaveEngineSource(engine.RootMoniker, Nothing)
'------------------------------------------------------------------
'
编译并保存自定义的代码
'------------------------------------------------------------------
If engine.Compile()
Then
SaveEngineCompiledState(engine.RootMoniker,
Nothing)
End If
Catch e As
Exception
MsgBox("Error Saving Engine") ' &
e.StackTrace)
End Try
End Sub
当方法SaveEngineSource 和 SaveEngineCompiledState 在类VSA SDK DT 中被调用时,这个类调用代码管理器方法PutSourceCode 和 PutBinaryCode 依靠SOAP,来存储代码。
运行代码
已经编写了脚本代码并且被你的代码管理器保存到你代码仓库。所有接下来要做的就是运行代码并且看它如何工作了。
由于脚本代码在服务器上运行,确保它以正确的方式运行就十分重要了。你的应用程序将运行脚本,所以VSA IDE依靠主应用程序来确保脚本运行得正确。XML程序定义在应用程序部件中包含了一个targetURL 属性来帮助解决这个问题。
例子中的dtHost 类使用targetURL 的数据来告诉VSA运行时类在用户要运行或调试脚本时该启动哪个URL。设计时类执行IVSADTSite ,它在用户运行代码并启动URL时被VSA IDE回调。当URL被启动时,用户输入调用网络服务所需的消息并且提交它。当消息被提交给网络服务时,网络服务就将被实例化,然后加载已编译的脚本代码并运行脚本代码。
由于你不用做运行代码之外的其他事情,调试脚本就相当自由了。与只是运行脚本唯一不同的地方是脚本编写者要选择他们希望中断和运行代码的行。运行又造成URL被加载,而脚本在网络服务被调用时被加载;当脚本代码运行时,VSA IDE已经准备好在断点中断代码。
图7. 调试脚本代码
存储并找回脚本代码
在本文的各处,我已经提到VSA运行时和设计时类似如何使用代码管理器存储和找回脚本代码,但是我还没有详细介绍如何执行代码管理器。为了正确运行代码管理器,就需要完全的脚本诊所(一个我将在以后做的东西)。然而,我将在这里复习一下基础知识,这样呢就可以知道例子中的代码管理器是如何使用的了。
一个代码管理器是一个.NET部件,它执行ICodeProvider 接口并把名称翻译为应用程序所使用的存储形式。例如,一个代码管理器会把名称转换为一系列SQL服务器或XML文档中的查询。这个接口被设计来允许加载,存储并删除源和二进制代码。设计将保持简单和无国籍,这样部件就可以在本地调用或作为网络服务。
为了保持设置过程的简单,这篇文章的例子中的代码管理器巴所有脚本代码保存到磁盘上。所有源和二进制脚本都被存储在网络服务器中的scripthappens vroot 的vsa项目文件夹中。代码管理器使用名称中的应用程序信息,并且在vsa项目文件夹中创建文件夹,因此com.scripthappens://calculate 最后成为vsa projects\calculate 。当代码管理器被从设计时主类中调用时,VSA项目,代码项目,调试信息和已编译的程序保存在文件夹中。
图8.
scripthappens的代码存储
当应用程序运行时,运行时主类将使用代码管理器来从calculate文件夹加载已编译二进制脚本。
总结
Visual Studio for Applications 和.NET
Framework脚本为你提供了一个很好的工具,来创建你或你的客户可以进行自定义的来满足变化的需要的应用程序。我希望这篇文章给你提供了如何创建可自定义的.NET应用程序的很好的介绍。特别是,我希望你会知道怎样使用VSA来创建可自定义的网络服务并且提供从网络浏览器对VSA
IDE的访问。在这里,我将感谢Wayne King ,VSA
SDK组中的一个测试员,他把我的一些模糊白板上的设计概念如此好地应用到例子中。那么,我们真的很希望听到你的反馈,欢迎在VSA新闻组上与我们联系,或者在msscript@microsoft.com 。
代码门诊部
Andrew Clinick 是一个Microsoft
Programmability组的资深的程序管理员,因此,如果涉及到脚本,他总会想办法进行处理。