COM+ 和 .NET - 实用的方法 - 第三部分






4.83/5 (16投票s)
2004 年 3 月 3 日
10分钟阅读

76353
审视 COM+ 和 .NET
(f) 当您的应用程序更改状态时通知其他人
松散耦合事件 (LCE) 是 COM+ 功能之一,可能非常有用。LCE 使 COM+ 组件能够发起事件,最终会激发未知客户端。客户端应通过使用 COM+ MMC 或动态方式注册为 COM+ 事件组件订阅者。发布者调用 COM+ 事件组件的一个方法,该方法会导致事件被发送并激发所有订阅者。事件组件订阅者和发布者之间的契约是单个接口,所有契约方都应实现该接口。
LCE 可用于创建对组件或应用程序的通知,而无需预先知道这些应用程序或组件是谁。为了使这些应用程序或组件能够收到通知,它们需要实现该事件接口并订阅 COM+ 事件组件。所有其他任务都由 COM+ 处理。
为了模拟 LCE 的强大功能,我们将创建一个机制,使期望的 Web 应用程序能够收到通知,当某个数据库表被某个 Web 应用程序或其他应用程序更改时。为了启用该机制,我们需要控制所有应用程序对数据库的访问。此任务可以通过创建一个实现 Select、Insert、Update 和 Delete 功能的新 COM+ 服务器应用程序来实现。每个应用程序中的每个数据层类在发送 SQL 语句到数据库时都应使用此类。该数据库管道类实际上就是发布者。每个数据库命令最终都会调用一个注册为 COM+ 库的类,并充当事件类。每个想要接收通知的 Web 应用程序都需要实现一个订阅者类,并将其(订阅者类)注册为事件订阅者。
让我们从事件组件开始——LCEventLibrary.Dll。它是一个简单的程序集,包含接口和一个类,该类注册为 COM+ 库,使用 EventClass 属性充当事件类。请注意,此类不应实现任何内容,也不应直接调用。COM+ 使用此类来调用订阅者。
图 5.0
public interface IMySink
{
void OnDBChange(string Action, string DBEntity);
}
[EventClass(FireInParallel = true)]
public class MyEventClass : ServicedComponent,IMySink
{
public void OnDBChange(string Action, string DBEntity)
{
throw(new
NotImplementedException("You should not call an event class directly! "));
}
}
为了使 Web 应用程序能够订阅 MyEventClass,我们将创建一个服务类 (TransientSubscription),它封装了所有应该写入以动态添加订阅者的行。动态订阅可以通过操作 COM+ 元数据来完成。为了操作 COM+ 元数据,我需要使用 COM+ COM + 1.0 Admin Type Library,因此我使用 tlbimp.exe 为该 COM 库创建托管包装器。TransientSubscription 公开用于添加和删除订阅者的静态函数。要添加订阅者,应发送唯一的名称、事件类类型和订阅者类。可以通过循环订阅者类的接口找到 Sink 接口。
static public void Add(string subName,Type eventClass,Type sinkInterface,
string method,object subscriber)
{
Type sinkType = subscriber.GetType();
if(method != "")
{
MethodInfo info = sinkInterface.GetMethod(method);
}
//Adding a new transient subscription to the catalog
ICOMAdminCatalog catalog;
ICatalogCollection transientCollection;
ICatalogObject subscription = null;
catalog = (ICOMAdminCatalog)new COMAdminCatalog();
transientCollection =
(ICatalogCollection)catalog.GetCollection("TransientSubscriptions");
subscription = (ICatalogObject)transientCollection.Add();
subscription.set_Value("Name",subName);
subscription.set_Value("SubscriberInterface",subscriber);
string eventClassString = "{"+eventClass.GUID.ToString()+"}";
subscription.set_Value("EventCLSID",eventClassString);
string sinkString = "{" +sinkInterface.GUID.ToString()+ "}";
subscription.set_Value("InterfaceID",sinkString);
subscription.set_Value("MethodName",method);
subscription.set_Value("FilterCriteria","");
subscription.set_Value("PublisherID","");
transientCollection.SaveChanges();
}
订阅者类 (MySubscriber
) 作为 Web 应用程序的一部分实现。MySubscriber
实现 IMySink
接口,并编写当事件类调用其订阅者时应调用的代码。在此演示中,我只是更新应用程序变量,表示发生了某些更改。一个需要解决的问题是更新 Application 对象。我不能使用 HTTPContext.Current
,因为订阅者调用不与任何请求关联。为了解决这个问题,我添加了一个构造函数,它将 HttpApplicationState
作为参数并将其设置为内部成员。
public class MySubscriber : LCEventLibrary.IMySink
{
private System.Web.HttpApplicationState App;
public MySubscriber()
{
}
public MySubscriber(System.Web.HttpApplicationState inApp)
{
App = inApp;
}
public void OnDBChange(string Action, string DBEntity)
{
App["DbChange"] = true;
}
}
实际的订阅发生在 Application_start
事件中。
m_Subscriber = new MySubscriber(System.Web.HttpContext.Current.Application);
TransientSubMgr.TransientSubscription.Add
("MySub",typeof(LCEventLibrary.MyEventClass),m_Subscriber);
Application["DbChange"] = false;
PublisherClass
程序集包含充当发布者的 PubClass
类。PubClass
作为数据库的单一连接点,因此注册为具有所有数据库操作方法的 COM+ 应用程序。每个方法都会创建一个基于 IMySink
接口的 MyEventClass
实例,并调用具有正确参数的 OnDBChange
。UpdateData
从 SQL 语句中提取表名并将其作为参数发送。
public class PubClass : System.EnterpriseServices.ServicedComponent
{
public PubClass()
{
}
public void UpdateData(string SQL)
{
LCEventLibrary.IMySink sink;
sink = new LCEventLibrary.MyEventClass();
string[] arr = SQL.Split (' ');
sink.OnDBChange("Update",arr[1]);
}
为了激活发布者,我在 Web 应用程序中添加了一个新页面 (CallPublisher.aspx)。该页面包含两个按钮,一个用于调用 PubClass UpdateData
方法,另一个用于刷新页面。在 page_load
中,我检查应用程序的 DbChange 变量。如果设置为 true,则执行其中一个数据库操作,并将通知打印到渲染的 HTML 中。
private void Page_Load(object sender, System.EventArgs e)
{
if ((bool)Application["DbChange"] == true)
{
Response.Write ("DB data changed!");
}
}
private void Button1_Click(object sender, System.EventArgs e)
{
PublisherClass.PubClass oPC = new PublisherClass.PubClass();
oPC.UpdateData("update MyTable set MyField=value");
}
private void Button2_Click(object sender, System.EventArgs e)
{
}
首次运行页面将显示带有两个按钮的表单。单击“更新”按钮将导致发布者调用事件类方法,然后调用所有订阅者。该 Web 应用程序中的订阅者类将更新给定的应用程序变量。现在按下刷新按钮将显示包含更改通知文本的页面。
(g) 无需培育 COM+ 树即可采摘 COM+ 的果实(COM+ 1.5 / CLR 1.1)
COM+ 1.5 引入了一种新的工作范式,使类构建者能够使用 COM+ 服务的一部分,而无需将类注册为 COM+ 应用程序(无组件服务 - SWC)。这种能力是为性能是其主要关注点的 C++ 开发人员提供的。C++ 开发人员可以使用 CServiceConfig
类来查询其中一个服务的接口,然后使用 API 将所选的 COM+ 服务用作内联块或批量模式。
尽管 .NET 应用程序可以使用互操作性来使用 SWC,但 CLR 1.1 在 EnterpriceServices 命名空间中引入了一个新类(ServiceConfig),该类允许使用 .NET 对象来使用 COM+ SWC。ServiceConfig 简单地封装了所需的 API 函数以启用 SWC。使用此类非常简单。您需要创建一个实例,通过设置属性来设置您想要使用的功能,使用 Enter 和 Leave 来标记将使用这些 COM+ 功能的块,并在该块内编写代码。
public void HandleUser(string UID)
{
DataLayerClass oDLC = new DataLayerClass();
try
{
ServiceConfig sc = new ServiceConfig();
sc.Transaction = TransactionOption.RequiresNew;
sc.TransactionTimeout = 30;
sc.TrackingAppName = "SWC Test";
sc.TransactionDescription = "Test";
sc.TrackingEnabled = true;
//start block
ServiceDomain.Enter(sc);
// cal data layer to set DB
oDLC.DeleteUser(UID);
ContextUtil.SetComplete();
}
catch(Exception ex)
{
ContextUtil.SetAbort();
}
finally
{
// end block
ServiceDomain.Leave();
}
}
正如我提到的,SWC 仅启用部分 COM+ 服务。更准确地说,这些是 SWC 可能使用的 COM+ 服务
- COMTIIntrinsic。
- IISIntrinsic。
- 上下文切换。
- 隔离级别。
- 分区。
- 并行程序集。
- 同步
- 线程池。
- 事务。
尽管列表很长,但最常用的服务,如对象池、JITA、排队组件和松散耦合事件,都不包含在内。正如您在本文中所见,这些 COM+ 功能用于解决日常问题,并将您的应用程序变成更健壮和可用的。
(h) 从 COM+ 服务器应用程序通知 ASP.NET。
如果您确信将类注册为 COM+ 服务器应用程序可以使您的应用程序变得更稳定和可用,那么您还需要记住我之前提到的限制。造成限制的一个原因是需要在 COM+ 组件和调用者应用程序之间进行 Remoting。如果您调用 COM+ 组件,CLR 会为您处理 Remoting,但如果您需要从 COM+ 应用程序调用应用程序,则需要做一些工作才能启用它。
一个常见的场景是从 COM+ 应用程序回调到调用应用程序。当 COM+ 应用程序需要调用您的应用程序时,现在轮到您的应用程序充当 Remoting 服务器。为了使您的应用程序能够充当 Remoting 服务器,您需要注册 TCP 端口。
让我们看一个实际情况,看看如何使用 COM+ 服务器应用程序和回调事件来完成它。假设您需要从数据库读取大量数据,处理这些数据并将所有数据显示给用户。如果我们遵循默认的页面处理模式,我们将获取所有数据、格式化它并将它作为单个缓冲区发送给客户端。我们将向最终用户提供一个非常缓慢的应用程序。当我们需要处理大量数据时,最好读取总数据的一部分,格式化它并直接发送给用户。使用这种范例将使用户感觉系统运行良好。用户可以快速看到一些数据,随着时间的推移,他可以看到越来越多的数据,直到所有数据都发送完毕。
为了启用这种页面处理序列,我们需要 A- 将缓冲设置为 false,以便我们可以发送数据块。B- 使用 DataReader 从数据库读取数据,收集记录块,格式化它们并将它们发送给客户端。为了使我们的应用程序更稳定,最好将数据读取放在一个将被注册为 COM+ 应用程序的类中。为了使 COM+ 组件能够将数据块发送到应用程序,我们将实现回调事件,这些事件将通知调用者何时数据块已准备好格式化和发送。
示例项目由一个新的 Web 应用程序 (ASPNET_COMPLUSE_CALLBACK)、一个常规程序集 (ComPlusFacade) 和一个 COM+ 服务器应用程序类 (ComPlusLib) 组成。ComPlusFacade 的任务是获取 COM+ 组件事件并将接收到的数据写入输出缓冲区。ComPlusFacade 是一个单独的程序集,因为调用者应用程序和被调用的 COM+ 组件都需要在 GAC 中,以便它们能够相互使用。在运行此示例之前,不要忘记将这些程序集注册到 GAC。
图 6.0
ComPlusLib 包含两个类:XMLArg,它被装饰为 Serializable
属性,以启用事件参数在 Remoting 通道上传输。
[Serializable()]
public class XMLArg : EventArgs
{
string XMLBuffer;
public XMLArg()
{
}
public XMLArg(string XMLBufferArg)
{
this.XMLBuffer = XMLBufferArg;
}
public string XMLBufferArg
{
get
{
return this.XMLBuffer;
}
set
{
this.XMLBuffer = value;
}
}
public override string ToString()
{
return this.XMLBuffer;
}
}
clsComPlusLib 是一个典型的 COM+ 组件,包含声明的事件,该事件在 ProcessData
方法的 for 循环中每次迭代时都会激发。
[ProgId("clsComPlusLib"),
Transaction(TransactionOption.NotSupported),
MustRunInClientContextAttribute(false),
EventTrackingEnabledAttribute(true),
JustInTimeActivation(true),
Synchronization(SynchronizationOption.Required),
Serializable
]
public class clsComPlusLib : ServicedComponent
{
public new event EventHandler DataArrive;
public clsComPlusLib()
{
}
public void ProcessData()
{
for (int i=0 ; i<100000 ; i++)
{
EventArgs XML = new XMLArg ("i=" + i + "<br>\n");
this.DataArrive(null,XML);
}
}
}
ComPlusLib 将由 CLR 注册到 COM+,您只需要通过拖放或使用 gacutil.exe 将其注册到 GAC。
ComPlusFacade 是此示例中有趣的部分。ComPlusFacade 是 WEB 应用程序和 COM+ 服务器应用程序之间的粘合剂。FacadeObjByRef 也被装饰为 Serialization,以启用类在 Remoting 上传输。为了使 FacadeObjByRef 能够充当 Remoting 服务器,它继承自 MarshalByRefObject
并声明了 TcpChannel
类型成员。
[Serializable()]
public class FacadeObjByRef : MarshalByRefObject
{
protected ComPlusLib.clsComPlusLib eventHandler =
new ComPlusLib.clsComPlusLib();
protected System.IO.Stream outStream;
public static TcpChannel theChannel;
类构造函数将 Stream 作为参数。发送的流应该是页面的响应输出流,它将被用于将接收到的数据写入客户端。在构造函数中,我注册了 TcpChannel
以允许 remoting 运行时选择一个可用端口,并设置 COM+ 组件事件的事件处理程序。
public FacadeObjByRef(System.IO.Stream outStream)
{
lock(this)
{
if (theChannel == null)
{
theChannel = new TcpChannel(0);
ChannelServices.RegisterChannel(theChannel);
}
}
if (textEncoder == null)
{
textEncoder = Encoding.UTF8;
}
this.outStream = outStream;
this.eventHandler.DataArrive += new
System.EventHandler(this.DataArive);
}
ProcessData 是 Web 页面将调用的公共方法,它同步调用 COM+ 应用程序以开始工作。
public void proccessData()
{
eventHandler.ProcessData();
this.CloseStream();
this.cleanUp();
}
当 COM+ 应用程序工作时,会激发一个事件,并由 DataArrive 处理。DataArrive 将事件参数转换为字节数组,并使用持有调用页面响应流缓冲区的内部成员将该字节数组发送到客户端。
public void DataArive(object sender, System.EventArgs e) { byte[] toWrite = textEncoder. GetBytes(((ComPlusLib.XMLArg)e).XMLBufferArg + "\n"); outStream.Write(toWrite, 0, toWrite.Length); }
ComPlusFacade 在 Web 应用程序进程下运行,您只需要将其注册到 GAC。
ASPNET_COMPLUSE_CALLBACK Web 应用程序只有一个 aspx 页面 (webform1.aspx),该页面禁用默认的缓冲行为,并创建 FacadeObjByRef
类的对象,然后调用 ProcessData
方法。
private void Page_Load(object sender, System.EventArgs e)
{
Response.BufferOutput = false;
ComPlusFacade.FacadeObjByRef oObj = new
ComPlusFacade.FacadeObjByRef (Response.OutputStream );
oObj.proccessData ();
}
现在我们只需要运行 ASPNET_COMPLUSE_CALLBACK 并观察它的行为。正如我解释的那样,页面会很快显示出来,显示正在不断到达并添加到浏览器 HTML 中的数据块。
结论
本文旨在展示如何在开发 Web 应用程序时使用 COM+ 服务来解决常见的编程任务。我们首先看到了 COM+ 服务器和库应用程序对性能的影响,以及使用服务器应用程序带来的限制。
在理解了性能影响之后,我试图解释并展示使用 COM+ 服务器应用程序对应用程序的稳定性、健壮性和监视的巨大贡献。通过解决组件长时间初始化、组件长时间任务、通知等日常任务,我们可以看到如何使用 COM+ 服务,特别是 COM+ 服务器服务来节省代码编写和程序员的工作量。
COM+ 可以提供强大的服务,帮助您快速创建复杂而稳定的应用程序。使用 COM+ 服务的主要缺点是性能损失。我们看到,我们可以使用无组件服务 (Services without Components) 的一部分而没有性能损失,并且 COM+ 库应用程序的性能损失是可以接受的。提供大部分有趣服务的 COM+ 服务器对性能有不利影响,这主要是由于其 DCOM 的使用。
在使用 COM+ 服务之前,最好创建一个概念应用程序并使用 ACT 进行测试,以查看使用 COM+ 服务器是否符合应用程序的性能目标。我希望下一版本的 Windows,如 Longhorn,能够提供不基于 DCOM 的 COM+ 服务,并使更多应用程序能够使用 COM+ 服务器应用程序。