65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight 应用程序关于数据库状态更改的通知

starIconstarIconstarIconstarIconstarIcon

5.00/5 (26投票s)

2009年11月27日

CPOL

12分钟阅读

viewsIcon

127152

downloadIcon

2289

本文介绍了构建Silverlight应用程序的技术和注意事项,该应用程序可以通过轮询双工(polling duplex)方式接收有关数据库状态变化的通知。同时还使用WSHttp绑定和CLR触发器来实现该解决方案。

引言

有一大类软件应用程序,它们应该能够让用户收到其他用户所做更改的通知。用于证券交易所、医疗实践和诊所的软件都必须具备上述功能。Silverlight应用程序非常适合这些领域,因为它们既有丰富的界面,同时又是瘦客户端。

为了实现Silverlight应用程序的通知功能,我们应该实现以下架构:

图1. 应用程序架构。

The architecture of the solution that allows to notify Silverlight applications about DB chabges.

如果一个客户端更新了特定数据,那么所有其他客户端都应该收到有关已发生更改的通知。这意味着,在中间层和客户端层之间应该建立一个双向(双工)连接。

中间层在从客户端层接收到数据后应该保存数据。数据层是一个可以告知数据已成功保存的层,并且它应该通知中间层有关的更改。

数据库无法直接访问中间层,所以我们应该使用以下解决方案之一来通知它有关的更改:

  • 数据库中应该有一个特殊的表,用于存储有关数据库更改的信息。中间层应该定期读取它以检查更改,并通知客户端层。
  • 可以实现CLR SQL触发器,通过Web服务通知中间层。
  • 可以使用Microsoft Notification Service来通知中间层有关数据库的更改。

我认为第一种方案不是一个好方案。中间层 sürekli地监视数据库,不断发送请求,这是对资源的浪费。

第三种方案需要使用额外的软件,我将把它留到另一篇文章中讨论。

我将描述第二种方案,并揭示构建双工Silverlight应用程序、CLR触发器以及从CLR触发器中使用WCF服务的秘密。

背景

为了实现这样一个应用程序,我将使用以下技术:

  • Silverlight用于客户端层
  • WCF服务(轮询双工绑定/WS HTTP绑定)
  • 用于SQL Server的CLR触发器

技术要求

使用了以下软件:

  • Windows XP SP3/IIS 5.1
  • VS 2008 SP1
  • .NET 3.5 SP1
  • Microsoft Silverlight Projects 2008 版本 9.0.30730.126
  • Silverlight 3.0.40818.0
  • Silverlight Toolkit 2009年7月版
  • Silverlight 3 开发者包
  • Silverlight 3 SDK。
  • .NET RIA Services(2009年7月预览版)
  • MS SQL Server 2005

入门

我将实现一个由几个部分组成的解决方案:

  • 一个Web应用程序。它承载Silverlight应用程序和Web服务(见下文)。
  • Silverlight应用程序。此应用程序将接收有关数据库更改的通知。
  • CLR触发器。它将包含一些逻辑,用于通过WCF服务从触发器发送数据。
  • SQL Server。承载CLR触发器。

Web应用程序 - DuplexSample.Web

这个Web应用程序是Silverlight应用程序的中间层,基于标准的Silverlight导航应用程序模板(新建项目/Silverlight/Silverlight导航应用程序)。

我将实现两个WCF服务。第一个(DBNotificationService)用于对Silverlight应用程序进行回调。第二个(DBAuditService)用于从CLR触发器接收数据。

为什么我要使用这样一个复杂的架构,而不能直接使用客户端层服务(DBNotificationService)来通知Silverlight应用程序呢?答案很简单——轮询双工绑定模式(Polling Duplex Binding mode)并未为非Silverlight应用程序实现,而任何其他绑定模式都不能用于实现双工通道。所以,我决定再实现一个服务。

DBNotificationService

DBNotificationService基于轮询双工绑定协议。该协议允许在客户端层(Silverlight应用程序)和中间层(Web应用程序)之间交换数据。

该服务包括四个文件:

  • DBNotificationService.svc。包含服务元数据,用于为WCF服务的客户端部分生成源代码(代理类/接口)。
  • DBNotificationService.svc.cs。包含WCF服务的实现。
  • IDBNotificationCallbackContract.cs。包含一个回调契约的接口。方法 (void SendNotificationToClients(DBTriggerAuditData data)) 将用于从中间层向客户端发送数据。
  • IDBNotificationService.cs。包含一个服务契约的接口,描述了客户端可以调用的方法,用于订阅(void SubscribeToNotifications())或取消订阅(void UnsubscribeToNotifications())通知。

本文不是WCF服务的指南,但我将描述上述服务的实现细节。

SubscribeToNotifications()方法的实现。
public void SubscribeToNotifications()
{
  IDBNotificationCallbackContract ch = 
    OperationContext.Current.GetCallbackChannel<IDBNotificationCallbackContract>();
  string sessionId = OperationContext.Current.Channel.SessionId;

  //Any message from a client we haven't seen before 
  //causes the new client to be added to our list
  //(Basically, treated as a "Connect" message)
  lock (syncRoot)
  {
    if (!SilverlightClientsList.IsClientConnected(sessionId))
    {
      SilverlightClientsList.AddCallbackChannel(sessionId, ch);
      OperationContext.Current.Channel.Closing += new EventHandler(Channel_Closing);
      OperationContext.Current.Channel.Faulted += new EventHandler(Channel_Faulted);
    }
  }
}

我在客户端层调用上述方法以订阅来自中间层的通知。该方法的实现获取一个传入的回调通道并将其存储在列表中;同时,它还初始化传入通道的其他事件(OnFaultOnDisconnect;见下文)。

存储的通道将用于向所有客户端发送通知。

UnsubscribeToNotifications()方法的实现。
public void UnsubscribeToNotifications()
{
    ClientDisconnect(OperationContext.Current.Channel.SessionId);
}

我在客户端层调用上述方法以取消订阅来自中间层的通知。该方法的实现只是通过其标识号从列表中删除回调通道。

回调通道事件的初始化;ClientDisconnect方法的实现。
private void Channel_Closing(object sender, EventArgs e)
{
    IContextChannel channel = (IContextChannel)sender;
    ClientDisconnect(channel.SessionId);
}

private void Channel_Faulted(object sender, EventArgs e)
{
    IContextChannel channel = (IContextChannel)sender;
    ClientDisconnect(channel.SessionId);
}

private void ClientDisconnect(string sessionId)
{
    lock (syncRoot)
    {
      if (SilverlightClientsList.IsClientConnected(sessionId))
        SilverlightClientsList.DeleteClient(sessionId);
    }
}

当客户端断开连接时,这些方法会被执行。

DBNotificationService类标有以下属性:

[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, 
 InstanceContextMode = InstanceContextMode.Single)]

ConcurrencyMode = ConcurrencyMode.Multiple意味着服务实例是多线程的,开发者需要关心同步问题。因此,我在读/写Silverlight通道列表之前对其进行锁定,以防止数据不一致。

InstanceContextMode = InstanceContextMode.Single意味着所有传入的调用只使用一个InstanceContext对象。

DBAuditService

DBAuditService基于WSHttpBinding协议(也可以使用BasicHttpBinding)。我将使用这个协议在数据层(SQL Server数据库)和中间层之间建立一个通道。

因此,数据库中的任何更改都将在相应的数据库触发器中被拦截,然后触发器应使用存储的通道(回调)向中间层发送有关更改的通知。

此服务包含四个文件:

  • DBAuditService.svc。包含服务元数据,用于为WCF服务的客户端部分生成源代码(代理类/接口)。
  • DBAuditService.svc.cs。包含WCF服务的实现。
  • DBTriggerAuditData.cs。包含一个数据契约(数据传输对象),将用于在数据/中间层和中间/客户端层之间交换数据。该类包含两个字符串属性。第一个包含触发器触发的表的名称,第二个包含从触发器接收的审计数据。
  • IDBAuditService.cs。包含服务契约的接口,该接口描述了可以调用以发送审计数据的方法。

该服务只实现了一个方法:SendTriggerAuditData。它会遍历所有的客户端通道,并为每个通道执行SendNotificationToClients方法。该方法的参数是我们从触发器接收到的数据。

SendTriggerAuditData(DBTriggerAuditData data)方法的实现。
public void SendTriggerAuditData(DBTriggerAuditData data)
{
    Guard.ArgumentNotNull(data, "data");

    if (SilverlightClientsList.GetCallbackChannels().Count() > 0)
    {
      lock (syncRoot)
      {
        IEnumerable<IDBNotificationCallbackContract> channels = 
            SilverlightClientsList.GetCallbackChannels();
        channels.ToList().ForEach(c => c.SendNotificationToClients(data));
      }
    }
}

添加非Silverlight客户端

当然,不仅仅是Silverlight客户端可以收到通知。应该实现另一个服务来通知其他客户端数据库的更改。

该服务应使用WSDualHttpBinding,并且其实现方式应与DBNotificationService服务类似。在这种情况下,DBAuditService也应使用非Silverlight客户端的通道发送通知。

Silverlight应用程序 - DuplexSample

此Silverlight应用程序是基于标准模板“Silverlight导航应用程序”(新建项目/Silverlight/Silverlight导航)的客户端层。

我只是在HomePage.xaml中添加了两个控件:

<Button Content="Connect" Click="ButtonConnect_Click" 
   x:Name="ButtonConnect" Margin="10"></Button>
<ListBox Grid.Row="1" ScrollViewer.VerticalScrollBarVisibility="Visible" 
  x:Name="ListBox1"></ListBox>

第一个是Button,用户可以用它来连接/断开与服务器的连接。第二个是ListBox,它显示传入消息的文本。

在该页面的构造函数中,我只是初始化了DBNotification服务。

private DBNotificationClient client;
private ObservableCollection<string> liveDataMessages = 
                          new ObservableCollection<string>();

public Home()
{
    InitializeComponent();

    ListBox1.ItemsSource = liveDataMessages;

    client = new DBNotificationClient(new PollingDuplexHttpBinding(), 
       new EndpointAddress("https://:2877/" + 
                           "DBNotificationService/DBNotificationService.svc"));
    client.SendNotificationToClientsReceived += (sender, e) =>
      {
        DBTriggerAuditData data = e.data;
        liveDataMessages.Add(data.TableName + ": " + data.Data);
      };
}

SendNotificationToClientsReceived是一个匿名委托,当接收到来自中间层的消息时执行。

方法Subscribe/Unsubscribe只是执行WCF服务的相应方法(SubscribeToNotificationsAsync/UnsubscribeToNotificationsAsync),并定义了在连接/断开连接完成后将执行的匿名委托。

private void Subscribe()
{
    ButtonSubscribe.Content = "Subscribing...";
    client.SubscribeToNotificationsCompleted += (sender, e) =>
    {
      ButtonSubscribe.Content = "Subscribed (click to unsubscribe)";
      subscribed = true;
    };
    client.SubscribeToNotificationsAsync();
}

private void Unsubscribe()
{
    ButtonSubscribe.Content = "Unsubscribing...";
    client.UnsubscribeToNotificationsCompleted += (sender, e) =>
    {
      ButtonConnect.Content = "Unsubscribed (click to subscribe)";
      subscribed = false;
    };
    client.UnsubscribeToNotificationsAsync();
}

CLR触发器 - DuplexSample.SqlTriggers

这个项目是一个类库,只包含一个类AppUser和一个静态方法AppUserAudit。该方法根据更改的行和字段创建一条消息,并通过DBAudit服务发送它。构建日志的代码取自SQL Server文档,你可以在那里找到大量关于此功能的信息。我只是在这个方法中加入了通过服务发送审计数据的功能。

EndpointAddress endpoint = 
  new EndpointAddress(new Uri("https://:2877/" + 
                      "DBAuditService/DBAuditService.svc"));
DBAuditClient client = 
  new DBAuditClient(new WSHttpBinding(SecurityMode.None), endpoint);

DBTriggerAuditData data = new DBTriggerAuditData();
data.TableName = "[dbo].[AppUser]";
data.Data = sb.ToString();

try
{
    client.SendTriggerAuditDataCompleted += (sender, e) =>
    {
      if (e.Error != null)
        throw new ApplicationException("There was an error occured", e.Error);
    };
    client.SendTriggerAuditDataAsync(data);
}
catch (Exception ex)
{

    throw;
}

请注意,服务的地址是硬编码的。我将在下面解释我为什么这么做。

添加CLR触发器

要将CLR触发器添加到SQL Server,我必须执行以下操作:

  • 在数据库中创建一个与CLR触发器程序集(DuplexSample.SqlTriggers)相对应的程序集;
  • 创建一个基于CLR触发器的触发器。

以下SQL命令对应于上述操作:

create ASSEMBLY [DuplexSample.SqlTriggers] FROM 
    'C:\Projects\Sandbox\DuplexSample\DuplexSample.SqlTriggers
     \bin\Debug\DuplexSample.SqlTriggers.dll' 
    WITH PERMISSION_SET = UNSAFE

其中[DuplexSample.SqlTriggers]是数据库中程序集的名称,PERMISSION_SET = UNSAFE是该库的权限级别(详见下文)。

CREATE TRIGGER AppUserAudit
  ON AppUser
  FOR Insert,Update,Delete 
  AS
  EXTERNAL NAME [DuplexSample.SqlTriggers].AppUser.AppUserAudit

就这样!你的触发器已经准备好被触发了。你可以去相应的表,尝试更改数据——触发器将会执行,数据将通过WCF服务发送出去。

CLR触发器技巧

在让触发器正常工作之前,我花了很多时间。我发现了一些问题,现在我将描述它们,以简化同行的工作。

首先,CLR触发器是在SQL Server进程内加载的,程序集的App.config或其他参数(例如Assembly.Location)都不可用。因此,我甚至无法获取程序集的路径;因此,WCF服务的所有参数都应该硬编码或以其他方式指定。

默认情况下,SQL Server不支持调用CLR方法,因此需要手动开启:

EXEC sp_configure 'show advanced options' , '1';
go

reconfigure;
go

EXEC sp_configure 'clr enabled' , '1'
go

reconfigure;

EXEC sp_configure 'show advanced options' , '0';
go

SQL Server不允许添加不安全的程序集(如果你的触发器执行WCF服务,该程序集绝对是不安全的)。要允许添加不安全的程序集,应执行以下命令:

ALTER DATABASE [Silverlight.DuplexNotification]
SET TRUSTWORTHY ON

默认情况下,即使TRUSTWORTHY开启,SQL Server也不允许添加不安全的程序集,我必须使用特殊参数(PERMISSION_SET = UNSAFE)将程序集添加到数据库中(见上文)。

注意:将数据库设置为可信可能会导致安全问题,因此最好使用证书/非对称密钥方案(详情请见此处)。

CLR触发器程序集有很多相关的程序集,它们也需要安装到SQL Server中。只有当它们与CLR触发器程序集放在同一个文件夹中时,SQL Server才能自动安装它们。我将所有这些程序集作为引用添加到了CLR触发器项目中,并为每个添加的程序集将复制到本地属性设置为true。有关添加的程序集列表,请参见项目文件DuplexSample.SqlTriggers.csproj

有一个程序集需要手动添加到数据库中,因为SQL Server不会这样做:

create ASSEMBLY [Microsoft.VisualStudio.Diagnostics.ServiceModelSink] FROM 
'C:\Projects\sandbox\DuplexSample\DuplexSample.SqlTriggers\bin\
  Debug\Microsoft.VisualStudio.Diagnostics.ServiceModelSink.dll' 
WITH PERMISSION_SET = UNSAFE

如果你更改了CLR触发器,仅仅重新生成是不够的。你必须重新生成它,从数据库中删除触发器和程序集,然后重新添加它们。

如果你打算使用CLR触发器,你需要以下代码来快速删除触发器/程序集:

drop trigger AppUserAudit
GO
drop assembly [DuplexSample.SqlTriggers]
GO

调试CLR触发器是一个非常简单的过程。你只需要设置一个断点,附加到sqlserver.exe进程(主菜单 - 调试 - 附加到进程),然后尝试更改表。最神奇的是,当你调试CLR触发器时,可以同时调试Web应用程序(WCF服务的服务器部分)。

图2. 附加到SQL Server进程以调试CLR触发器。

This dialog allows to attach to the process of SQL server and start debugging CLR triggers.

有时,通知无法到达Silverlight应用程序——只需重启内置的Web服务器。有时,Silverlight应用程序会导致Internet Explorer崩溃(我不知道原因)——改用Firefox即可。

演示应用程序

应用程序的源代码包含了所有上述技术,并且已经可以编译和部署。在运行前,开发者需要更改/更新一些东西:

  • 更改端口号/为Web项目创建IIS文件夹(如果你想使用其他端口或IIS,而不是内置的Web服务器)。
  • 根据给定的脚本创建数据库。

要开始演示,我应该编译所有的库,将CLR触发器添加到数据库,运行一个或多个Silverlight应用程序并订阅通知,然后对创建了CLR触发器的表进行任何更改。

以下图片展示了应用程序如何工作:

图3. 两个Silverlight应用程序已启动,连接到服务并准备订阅通知。

Clients are ready to subscribe to notifications

图4. 两个Silverlight应用程序订阅了通知并收到了关于插入操作的信息。

Clients receive information about insert operation

图5. 两个Silverlight应用程序收到了关于更新操作的信息。

Clients receive information about update operation

图6. 一个Silverlight应用程序已断开连接,第二个已连接并收到了关于删除操作的信息。

One client is disconnected, Another one receives information about delete operation

以下SQL脚本用于更新数据库:

insert into AppUser Values ('Test User 1', '123456', 'test1@test.test')
insert into AppUser Values ('Test User 2', '654321', '')
-------
update AppUser set email = 'test2@test.test' where Name = 'Test User 2'
-------
delete from AppUser

控制台应用程序

应用程序DuplexSample.ConsoleApp被添加到解决方案中,只是为了能够模拟CLR触发器。该应用程序连接到DBAuditService并发送测试数据。这些数据会到达Web服务器,然后服务器将其发送给每个订阅的客户端。

总结

本文面向从事业务应用程序开发的开发人员和架构师。它描述了:

  • 如何实现一个基于轮询双工绑定模式的WCF服务;
  • 如何实现一个基于WS HTTP绑定模式的WCF服务;
  • 如何实现CLR触发器;
  • 如何将CLR触发器添加到数据库中;
  • 在开发和安装CLR触发器期间可能出现的问题及解决方法。

历史

  • 版本 1.0 (2009-11-27) - 初始版本。
© . All rights reserved.