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

实时电子表格

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2016 年 5 月 28 日

CPOL

5分钟阅读

viewsIcon

21889

实时电子表格,通过 Web Service 更新和检索数据。使用 SQL Server 数据库。如果有两个或更多用户正在处理同一个电子表格,他们可以看到其他用户正在做什么。鼠标指向的单元格通过填充斜纹来高亮显示,已保存的数据通过填充绿色高亮显示。

应用演示 - YouTube 视频

请在此处找到关于本文所述应用程序功能的现场演示

https://www.youtube.com/watch?v=DO6BQpYu--0

引言

简要介绍一款使用 C#、VS2015、SQL Server 2014、SignalR 和 Telerik UI 组件开发的实时电子表格。

UI 组件是 Telerik RadSpreadsheet for Silverlight。完全相同的概念可以扩展到该 UI 组件的 Web 版本。

数据通过 Web Service 更新和检索,负责接收请求并发送响应。

请求被转发到应用程序服务层 (C#),该层负责管理适当的更新/插入/删除操作。

最后,Entity Framework 用于将实体映射到数据层。

本文介绍的主要功能

如果有两个或更多用户正在处理同一个电子表格,他们可以看到其他用户正在做什么。鼠标指向的单元格通过填充斜纹来高亮显示,已保存的数据通过绿色填充高亮显示。

系统还有能力直接调用数据库中的操作。操作由相应的单元格触发。一旦触发操作,数据库就可以调用 Web Service 并执行适当的数据更改。

这些操作也会同时广播给正在处理同一电子表格的所有用户。

背景

我建议阅读几篇关于 SignalR 的文章

http://www.asp.net/signalr/overview/getting-started/tutorial-getting-started-with-signalr

http://stackoverflow.com/questions/15128125/how-to-use-signalr-hub-instance-outside-of-the-hubpipleline

SignalR 初始化 - 客户端

网上有很多文章解释如何设置 SignalR。我按照以下说明创建了连接和集线器。方法被添加到集线器;它们可以由Web Service (例如稍后描述的“broadCastUpdateResponse”) 或由服务器端的MainHub (例如稍后描述的“HighlightCell”)调用。

//SignalR                
string serverUri = WebServer + "/signalr/hubs";
connection = new HubConnection(serverUri, true);
hub = connection.CreateHubProxy("MainHub");

在...之前包含以下脚本很重要

    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-2.2.0.js"></script>
    <script src="signalr/js" type="text/javascript"></script>

Jquery 必须在 SignalR 之前包含,否则会出现以下错误

 JavaScript runtime error: jQuery was not found. Please ensure jQuery is referenced before the SignalR client JavaScript file.

连接通过调用 Start() 方法建立,这是一个异步调用。然后需要使用 await 前缀。

为了调试连接状态更改,添加以下几行也很方便

connection.StateChanged += (change) =>
                {
                    System.Diagnostics.Debug.WriteLine("hubConnection.StateChanged {0} => {1}", change.OldState, change.NewState);  
                    if (change.NewState == ConnectionState.Connecting)
                    {
                        statusCallBack callBack = new statusCallBack(UpdateStatus);
                        this.Dispatcher.BeginInvoke(callBack, "hubConnection.Connecting");
                    }
                    if (change.NewState == ConnectionState.Connected)
                    {
                        
                        statusCallBack callBack = new statusCallBack(UpdateStatus);
                        this.Dispatcher.BeginInvoke(callBack, "hubConnection.Connected");
                    }
                    if (change.NewState == ConnectionState.Disconnected)
                    {
                        connection.Start().Wait();                        
                        statusCallBack callBack = new statusCallBack(UpdateStatus);
                        this.Dispatcher.BeginInvoke(callBack, "hubConnection.Disconnected.. reconnectin");
                    }
                }; 

SignalR 初始化 - 服务器端

必须获取 Microsoft.Owin.Cors 包,以便在 OWIN 中间件中能够进行跨域资源共享 (CORS)。

创建以下 OWINStartup 类,如第一篇文章中所述。

 public class OWINStartup
    {
        public void Configuration(IAppBuilder app)
        {
            // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888
            // Branch the pipeline here for requests that start with "/signalr"
            app.Map("/signalr", map =>
            {
                // Setup the CORS middleware to run before SignalR.
                // By default this will allow all origins. You can 
                // configure the set of origins and/or http verbs by
                // providing a cors options with a different policy.
                map.UseCors(CorsOptions.AllowAll);
                var hubConfiguration = new Microsoft.AspNet.SignalR.HubConfiguration
                {
                    // You can enable JSONP by uncommenting line below.
                    // JSONP requests are insecure but some older browsers (and some
                    // versions of IE) require JSONP to work cross domain
                    // EnableJSONP = true
                };
                // Run the SignalR pipeline. We're not using MapSignalR
                // since this branch already runs under the "/signalr"
                // path.
                map.RunSignalR(hubConfiguration);
            });
        }
    }

应用程序功能说明

用户更新单元格 - 客户端

用户可以浏览电子表格,一旦找到需要更新的单元格,他会输入相应的值。由于电子表格单元格具有 CellPropertyChanged 事件,一旦触发该事件,以下代码将简单地调用 Web Service 将所需数据更新到数据库中。

  private async void  Cells_CellPropertyChanged(object sender, CellPropertyChangedEventArgs e)
        {            
         int i = e.CellRange.FromIndex.RowIndex;
         int j = e.CellRange.FromIndex.ColumnIndex;
         for (i = e.CellRange.FromIndex.RowIndex; i <= e.CellRange.ToIndex.RowIndex; i++)
               for (j = e.CellRange.FromIndex.ColumnIndex; j <= e.CellRange.ToIndex.ColumnIndex; j++)
               {
                 CellRange cr = radSpreadsheet.ActiveWorksheet.Cells[i, j].CellRanges.FirstOrDefault();
                 if (cr != null)
                 {
                      bool updated = await ViewModel.UpdateValue(cr);
                 }
               }                                    
        }

用户更新单元格 - 服务器端

Web Service 接收请求并调用 ApplicationService,后者更新相应的数据表。

更新完成后,WebService 将响应序列化回客户端。

      [HttpPost]
      [Route("Instances/Update/{id}")]
      public async Task<IHttpActionResult> Update(int id, UpdateInstanceRequest request)
        {         
            UpdateInstanceResponse response = await _service.UpdateInstance(request);
          
            string serializedResponse =  Microsoft.AspNet.SignalR.Json.JsonSerializerExtensions.Stringify(new Newtonsoft.Json.JsonSerializer(), response);
            
            // Get SignalR context
            var context = Microsoft.AspNet.SignalR.GlobalHost.ConnectionManager.GetHubContext<MainHub>();
            
            // Invoke the Client method
            context.Clients.All.updateResponse(serializedResponse);
            
            if (response == null)
            {
                return NotFound();
            }
            else
            {
                return Ok(response);
            }
        }

请注意,SignalR 允许调用客户端上的方法,并最终从服务器初始化上下文。

在这种情况下,我将消息广播给所有客户端。通常可以将客户端分成组,然后将消息发送给其中的一部分。

响应 - 客户端

服务器调用的动作 (“updateResponse”) 添加到集线器并在客户端实现,以便在单元格由用户更新时响应 Web Service 调用。

 hub.On<string>("updateResponse", (output) =>
 {
        this.Dispatcher.BeginInvoke(() =>
        {
         //Parse Json                     
         var getAnonymousType = new { Instance = new { Attribute = new { Id = 0, Type = 0 }, ModifiedTime = new DateTime(), Id = (long)0, Value = "", Locked = false } };                        
         var instanceUpdated = Newtonsoft.Json.JsonConvert.DeserializeAnonymousType(output, getAnonymousType);
                     
         //Get ViewModel and Cell
         SpreadsheetViewModel svm = (SpreadsheetViewModel)this.DataContext;                     

         //Updating Cell via SignalR
         svm.updateCellSignalR(instanceUpdated.Instance.Id, instanceUpdated.Instance.Value, instanceUpdated.Instance.ModifiedTime, instanceUpdated.Instance.Locked);

               });
         }); 

对象使用 JsonConvert 进行反序列化,并将结果传递给 ViewModel。

调用 Spreadsheet ViewModel 中的 “updateCellSignalR” 方法,以查找是否存在具有已更新实例 ID 的单元格。在这种情况下,实际单元格会更新并填充正确的颜色(示例中为“绿色”)。

 public void updateCellSignalR(long inst_id, object value, DateTime modified_time, bool locked)
 {
      Cell refreshCell = CurrentSheet.Cells.Where(w => w.ID == attr_inst_id).FirstOrDefault();
      if (refreshCell != null)
      {
          refreshCell.ModifiedTime = modified_time;
          refreshCell.IsLocked = locked;                 
          setCellValue(refreshCell);                
          setCellPropertyColour(refreshCell, true);                 
       }
  }

单元格高亮 - 客户端 - 第 1 部分

用户可以使用箭头键或鼠标点击自由地在电子表格单元格之间移动。在这种情况下,我添加了一个方法,如果单元格被鼠标点击指向(箭头键的情况也是同样的原理),该方法就会高亮显示该单元格。

 

该方法仅检索单元格的行和列,并使用集线器调用服务器上的 “SelectedCell” 方法,传递活动工作表的名称和单元格的位置。

 private async void PresenterMouseDown(object sender, MouseButtonEventArgs e)
  {
          int row = this.radSpreadsheet.ActiveWorksheetEditor.Selection.Cells.CellRanges.FirstOrDefault().FromIndex.RowIndex;
          int column = this.radSpreadsheet.ActiveWorksheetEditor.Selection.Cells.CellRanges.FirstOrDefault().FromIndex.ColumnIndex;

          if(connection.State == ConnectionState.Connected)
              hub.Invoke("SelectedCell", this.radSpreadsheet.Workbook.Sheets.ActiveSheet.Name, row, column );
         }

单元格高亮 - 服务器端

MainHub 包含由客户端调用的 “SelectedCell” 方法的实现。

此方法仅将信息转发给所有“其他”客户端。这里的“其他”是指除调用者之外的所有客户端。

   public void SelectedCell(string sheetname, int row, int col)
   {
       // Call the broadcastMessage method to update clients.
       Clients.Others.HighlightCell( sheetname,  row,  col);
   }

单元格高亮 - 客户端 - 第 2 部分

客户端上的集线器实现包含由服务器调用的 “HighlightCell” 方法的实现。

此方法仅检查当前工作表名称是否相同,并用斜纹填充相应的单元格。

      hub.On<string, int, int>("HighlightCell",
         (sheetname, row, col) =>
           {
               this.Dispatcher.BeginInvoke(() =>
               {
                  //Get ViewModel and Cell
                  SpreadsheetViewModel svm = (SpreadsheetViewModel)this.DataContext;
                  svm.highlightCellSignalR(sheetname, row, col);
              });
           });

 

    private Cell _previousHighlight;
        internal void highlightCellSignalR(string sheetname, int row, int col)
        {
            Cell refreshCell;
            if (SpreadSheetWorkbook.ActiveSheet.Name == sheetname)
            {
                refreshCell = CurrentSheet.Cells.Where(w => w.RowIndex == row && w.ColumnIndex == col).FirstOrDefault();
                if (refreshCell != null)
                {                  
                    TelerikExcel.PatternFill diagonalStripePatternFill = new TelerikExcel.PatternFill(TelerikExcel.PatternType.DiagonalStripe, Color.FromArgb(120, 231, 76, 60), Color.FromArgb(120,241, 196, 15));
                    _spreadSheetWorkbook.ActiveWorksheet.Cells[row, col].SetFill(diagonalStripePatternFill);
          
                     if (_previousHighlight != null)
                        setCellPropertyColour(_previousHighlight, false);

                    this._previousHighlight = refreshCell;                    
                }
            }
        }

请注意,需要存储先前高亮的单元格,以便将其填充恢复为原始状态。

 

关注点

以上几点描述了如何在客户端和服务器上设置 SignalR。它指出使用 Cors 以在 OWIN 中间件中启用跨域资源共享 (CORS)。

然后,它说明了如何使用 SignalR 构建实时应用程序,并使用了 Telerik RadSpreadsheet。

集线器以“经典”方式使用,其中客户端调用服务器,服务器将消息广播回所有客户端(类似于简单的“聊天”房间机制)。

然后,同一集线器被 WebService 使用,以将消息广播给所有连接的客户端,从而共享更新的数据。它必须使用 Globalhost 实例化集线器上下文,但它演示了如何在集线器管道之外实例化集线器。

在视频的最后一部分,WebService 由数据库本身调用,因此您可以看到电子表格的值在 Web Service 收到来自数据库的请求并将其响应发送回所有客户端后立即发生变化。

这不是很棒吗?:)

 

 

© . All rights reserved.