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





5.00/5 (26投票s)
本文介绍了构建Silverlight应用程序的技术和注意事项,该应用程序可以通过轮询双工(polling duplex)方式接收有关数据库状态变化的通知。同时还使用WSHttp绑定和CLR触发器来实现该解决方案。
引言
有一大类软件应用程序,它们应该能够让用户收到其他用户所做更改的通知。用于证券交易所、医疗实践和诊所的软件都必须具备上述功能。Silverlight应用程序非常适合这些领域,因为它们既有丰富的界面,同时又是瘦客户端。
为了实现Silverlight应用程序的通知功能,我们应该实现以下架构:
如果一个客户端更新了特定数据,那么所有其他客户端都应该收到有关已发生更改的通知。这意味着,在中间层和客户端层之间应该建立一个双向(双工)连接。
中间层在从客户端层接收到数据后应该保存数据。数据层是一个可以告知数据已成功保存的层,并且它应该通知中间层有关的更改。
数据库无法直接访问中间层,所以我们应该使用以下解决方案之一来通知它有关的更改:
- 数据库中应该有一个特殊的表,用于存储有关数据库更改的信息。中间层应该定期读取它以检查更改,并通知客户端层。
- 可以实现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);
}
}
}
我在客户端层调用上述方法以订阅来自中间层的通知。该方法的实现获取一个传入的回调通道并将其存储在列表中;同时,它还初始化传入通道的其他事件(OnFault
、OnDisconnect
;见下文)。
存储的通道将用于向所有客户端发送通知。
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服务的服务器部分)。
有时,通知无法到达Silverlight应用程序——只需重启内置的Web服务器。有时,Silverlight应用程序会导致Internet Explorer崩溃(我不知道原因)——改用Firefox即可。
演示应用程序
应用程序的源代码包含了所有上述技术,并且已经可以编译和部署。在运行前,开发者需要更改/更新一些东西:
- 更改端口号/为Web项目创建IIS文件夹(如果你想使用其他端口或IIS,而不是内置的Web服务器)。
- 根据给定的脚本创建数据库。
要开始演示,我应该编译所有的库,将CLR触发器添加到数据库,运行一个或多个Silverlight应用程序并订阅通知,然后对创建了CLR触发器的表进行任何更改。
以下图片展示了应用程序如何工作:
以下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) - 初始版本。