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

Silverlight 4(带 HTTP 双工和 SQL 通知)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (7投票s)

2011年1月15日

CPOL

4分钟阅读

viewsIcon

47838

downloadIcon

2034

当数据库发生更改时,立即将更新推送到你的浏览器。

引言

如果你曾经尝试过创建一种从数据库到服务再到浏览器的推送机制,这本身就已经够复杂的了,而拥有一个嵌入式 Silverlight 浏览器应用程序则会增加一些难度。

如果你在网上搜索这类应用程序,你会发现大量关于 Silverlight 和 Duplex 或 Silverlight 和 SQL 通知 (SQL Notifications) 的文章,但关于 Silverlight 结合 Duplex 和 SQL 通知 (SQL Notations) 的内容却少得可怜。

背景

最初,这个问题源于无法在网上找到针对此场景的完整文章的沮丧。有些文章接近解决方案,但并非完全一致。Silverlight 还会存在一段时间(或者直到 HTML5 被完全接受和成熟),而这种推送机制将有助于其他开发人员使用此类技术。

使用代码

项目结构布局

SilverlightDuplexSql/FolderStructure.JPG

图 1

从图 1 可以看到应用程序的结构。有一个 Silverlight 项目 "SLDuplexDependency"(它将包含在 "SLDuplexDependency.Web" 项目的测试页面中)。这个 Silverlight 项目有一个服务引用,指向名为 "BrowserMaintenanceService" 的服务。此服务用于跟踪连接到服务器的浏览器。Silverlight 项目中第二个名为 "SqlServiceReference" 的服务将维护来自 SQL Server 的回调(通知),并处理将发送回客户端的任何新结果。当数据库上执行更新、插入或删除操作时,此服务将收到通知(会发送一个小 XML 通知到回调方法),然后它将使用静态类/方法以 JSON 格式(因为 Silverlight 没有 XML 解析器)通知浏览器。浏览器(Silverlight C#)中的回调方法将处理 JSON 并重新绑定数据网格。

主项目 "SLDuplexDependency.Web" 中的一个服务 "DBNotificationService" 用于处理浏览器连接到服务器并对来自服务器的 SQL 通知表示兴趣时的事件。另一个服务 "BrowserDuplex" 最初由 Silverlight 应用程序使用,用于建立初始连接并将通知绑定到服务器——在首次绑定使用后(`SqlDependency` 必须在每次通知后重新创建),主服务 "SqlEventsService" 将负责所有通知和 `SqlDependency` 对象的重新绑定。

有一个名为 `SilverlightClientsList` 的静态类,它将维护浏览器会话 ID 的记录,并且服务可以调用其静态方法将数据推送到客户端。

Silverlight

此方法调用 "DBNotificationService" 服务以注册此浏览器对 SQL Server 表通知的兴趣。

/// Subscribes this instance of the browser,
void Subscribe()
{            
    client.SubscribeToNotificationsCompleted += (sender, e) => { };
    client.SubscribeToNotificationsAsync();
}

此方法用于注册浏览器并将它们的会话 ID 存储在静态列表中。每个会话将有自己的关闭事件与之关联。

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            
    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);
        }
    }
}

双工

此方法是在数据库发生更改时用于回调的方法。

private void dependency_OnDataChangedDelegate(object sender, SqlNotificationEventArgs e)
{
    if (e.Type != SqlNotificationType.Change) return;

    ObservableCollection<Employee> myAuthors = 
      this.GetEmployeeList(DateTime.Now.AddYears(-20), DateTime.Now.AddYears(1));

    string c = SerializeObject(myAuthors);
    this.SendTriggerAuditData(c);

    //the notification is now used up - so now we remove
    //the event handler from the dependency object
    //(another one will have been bound to it again)
    SqlDependency dependency = sender as SqlDependency;
    dependency.OnChange -= 
      new OnChangeEventHandler(dependency_OnDataChangedDelegate);
}

此方法将使用新数据启动回调到浏览器的过程。

public void SendTriggerAuditData(string data)        
{
    // loop through channels (browsers) and make a call to their callback method
    if (SilverlightClientsList.GetCallbackChannels().Count() > 0)
    {
        lock (syncRoot)
        {
            IEnumerable<IDBNotificationCallbackContract> channels = 
                   SilverlightClientsList.GetCallbackChannels();
            channels.ToList().ForEach(c => c.SendNotificationToClients(data));
        }
    }
}

SQL 通知 / SqlDependency

以下方法是用于查询数据库并创建 SQL 依赖关系的通用方法。

public ObservableCollection<WcfSqlNotifications.DuplexServiceReference.Employee> 
       GetEmployeeList(DateTime startDate, DateTime endDate)
{
    employees = 
      new ObservableCollection<WcfSqlNotifications.DuplexServiceReference.Employee>();

    //the connection string to your database
    string connString = @"Data Source=YouDatabaseServer;Initial" + 
      @" Catalog=dependencyDB;Persist Security Info=True;" + 
      @"User ID=NotificationsUser;Password=password";

    string proc = "SelectCredentialsWithinDOBRange";
    //the name of our stored procedure

    if (!CheckUserPermissions()) return null;
    //first we need to check that the current user
    //has the proper permissions, otherwise display the error

    this.employees = new ObservableCollection<
       WcfSqlNotifications.DuplexServiceReference.Employee>();
    bool success = SqlDependency.Start(connString);
    // create the dependency reference on the database

    using (SqlConnection sqlConn = new SqlConnection(connString))
    {
        using (SqlCommand sqlCmd = new SqlCommand())
        {

            SqlParameter myParm1 = sqlCmd.Parameters.Add(
                         "@StartDate", SqlDbType.DateTime, 20);
            myParm1.Value = startDate;
            SqlParameter myParm2 = sqlCmd.Parameters.Add(
                           "@EndDate", SqlDbType.DateTime, 20);
            myParm2.Value = endDate; 

            sqlCmd.Connection = sqlConn;
            sqlCmd.Connection.Open();

            //tell our command object what to execute
            sqlCmd.CommandType = CommandType.StoredProcedure;
            sqlCmd.CommandText = proc;
            sqlCmd.Notification = null;

            SqlDependency dependency = new SqlDependency(sqlCmd);
            dependency.OnChange += 
              new OnChangeEventHandler(dependency_OnDataChangedDelegate);

            if (sqlConn.State != ConnectionState.Open) sqlConn.Open();

            using (SqlDataReader reader = sqlCmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    WcfSqlNotifications.DuplexServiceReference.Employee author = 
                       new WcfSqlNotifications.DuplexServiceReference.Employee();
                    author.ID = reader.GetInt32(0);
                    author.FirstName = reader.GetString(1);
                    author.SecondName = reader.GetString(2);
                    author.Address = reader.GetString(3);
                    author.DOB = reader.GetDateTime(4).ToString();
                    this.employees.Add(author);
                }
            }
        }
        return this.employees;
    }
}

需要一个方法来确保用户能够在服务器上发起查询。

private bool CheckUserPermissions()
{
    try
    {
        SqlClientPermission permissions = 
                 new SqlClientPermission(PermissionState.Unrestricted);
        //if we cannot Demand() it will throw an exception
        //if the current user doesnt have the proper permissions
        permissions.Demand();
        return true;
    }
    catch { return false; }
}

SQL Server

将以下数据库文件附加到你的 SQL Server (2008) 数据库。

在你的新附加的数据库上运行以下脚本。

USE dependencyDB;
GO
CREATE QUEUE dependencyDBQueue;
CREATE SERVICE dependencyDBService ON QUEUE dependencyDBQueue (
  [http://schemas.microsoft.com/SQL/Notifications/PostQueryNotification]);
GRANT SUBSCRIBE QUERY NOTIFICATIONS TO GeneralUser;
ALTER DATABASE dependencyDB SET SINGLE_USER WITH ROLLBACK IMMEDIATE
ALTER DATABASE dependencyDB SET ENABLE_BROKER
ALTER DATABASE dependencyDB SET MULTI_USER
GO

注意:如果上面的代码挂起,请停止 SQL 服务然后重试。

  1. 创建存储过程(使用已启用 Service Broker 的数据库)时需要注意的事项 - http://msdn.microsoft.com/en-us/library/ms181122(SQL.100).aspx
  2. 如何启用你的数据库作为 Service Broker - http://www.dreamincode.net/forums/topic/156991-using-sqldependency-to-monitor-sql-database-changes/
  3. 什么是 "Service Broker" - http://msdn.microsoft.com/en-us/library/bb522889(SQL.100).aspx

测试应用程序

  1. 确保 (dependencyDB) 数据库已启用 Service Broker。
  2. 确保你在连接字符串中的用户已在数据库中创建(并具有足够的权限)- 如果只是将数据库附加到服务器,通常已自动创建。
  3. 在 VS2010 中运行 Silverlight 应用程序(使用修改后的连接字符串)- 应该会出现一个显示详细信息的网格。
  4. 转到数据库并打开 credentials 表进行编辑。
  5. 编辑其中一行数据。
  6. 结果:编辑的行的详细信息应反映在浏览器网格中。
  7. 进一步测试:打开多个浏览器并执行相同操作 - 更改将推送到所有浏览器。

优点和缺点

优点

  1. 它是跨数据库平台的 - 有一个 `OracleDependency` 对象(http://download.oracle.com/docs/ cd/B19306_01/win.102/b14307/OracleDependencyClass.htm)。
  2. 高可伸缩性 - 使用后台线程调用 WCF,并使用 Win2008 服务器和 IIS7 - 连接数可达数千。
  3. 代码可以定制以提高效率,这样只有更新的记录会被传回客户端,而不是所有记录都被重新绑定。
  4. 可以极大地帮助应用程序的缓存机制 - WCF 可以被通知何时发生更新并刷新其缓存。

缺点

  1. Mono WCF 在 .NET 2 中 - `SqlDependency` 在 .NET 3.5+ 中,所以无法将此解决方案移植到 Linux 环境。
  2. 在存储过程中可以使用的 SQL 有限制(http://msdn.microsoft.com/en-us/library/ms181122(SQL.100).aspx)。
© . All rights reserved.