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

将实时数据流式传输到 Excel

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (32投票s)

2013年10月2日

CPOL

7分钟阅读

viewsIcon

120789

downloadIcon

3802

这是一个极简的示例,展示了如何使用 WCF、Rx 和 Excel-DNA 将实时数据流式传输到 Excel 客户端。

引言

在商业领域,Excel 几乎受到所有人的喜爱,因为它能够让最终用户完成工作。当然,它也有其缺点,其中之一是它是一个相对独立的应用程序。本项目提出了一种连接 Excel 到服务器的方法,使其能够从服务器接收实时更新。这可以用于经典的股票行情应用程序,但还有更多应用。虽然我不能诚实地说它很简单,但它肯定比用 C++ 编写服务器和 RTD 类要容易得多。

背景

本项目结合了多种技术来实现最终结果

  • 在通信方面,它使用了双工 WCF,这使得服务器可以向客户端发送数据。
  • 在客户端(Excel)方面,它使用了一个用 C# 编写的插件,然后通过 Excel-DNA 进行封装,以便在 Excel 中使用。
  • 在插件内部,它依赖 Rx 将通过 WCF 调用发送的数据点公开为一个值流,Excel 使用这个流来更新单元格。

使用代码

要查看效果,请执行以下操作:

  1. 重新生成整个 RTDExcelAddIn **解决方案。**这将引入所有 NuGet 包,并确保最新的插件版本与 Excel-DNA 一起打包。
  2. 按 F5 启动服务器(这应该是默认的启动项目)。或者,您可以右键单击 RTDServer 项目,然后选择“调试”>“启动新实例”。
  3. 导航到 ...\Projects\RTDExcel\RealTimeData\bin\Release(或 Debug)目录,然后双击 RealTimeData-AddIn-packed.xll。这将启动 Excel 并加载插件。
  4. 在 Excel 打开的情况下,按 CTRL-N 打开一个新的工作簿,然后在任何单元格中键入 =GetValues(),您将看到更新流式传输进来。您也可以在“插入函数”对话框中找到该函数。

工作原理

让我们先对所有涉及的组件进行一个高层次的概述,然后再在单独的段落中详细介绍它们。

下图展示了应用程序的基本构建块。

从左侧开始,我们有服务器。这是一个托管 RTDServer 类的 C# 控制台应用程序。它公开了一个基于 IServer 接口的 WCF 端点,我们稍后将讨论该接口。

右侧是客户端插件,它也是用 C# 编写的,托管 IClient 端点。两者通过双工 WCF 进行通信。当客户端调用服务器上的 Register 时,服务器将打开并存储一个指向客户端的回调通道,该通道允许服务器在数据可用时立即发送数据(而不是客户端轮询服务器以获取新数据)。

橙色块代表 Excel-DNA,这是一个库,允许您用 C# 编写类库,然后将其作为插件在 Excel 中使用。它提供了样板胶水代码,可以非常高效地让 Excel 调用您的 C# 代码。

Excel-DNA 利用 Excel 中的 实时数据 (RTD) 功能来通知 Excel 发生更改,并使用 Rx 来传播这些更改。Rx 在这里非常适合,因为我们实际上希望响应这个流入的值流。

在接下来的段落中,我将重点介绍每个项目中值得关注的部分。

RTD 服务器

如果您之前做过 WCF,那么 RTDServer 应该相当熟悉。它唯一的作用是接收并响应传入的连接请求,并每 100 毫秒向所有客户端发送一个随机数。

该项目包含用于实现服务器和客户端端点的两个接口,并在 RTDServer 类中实现了 IRTDServer 接口。

 [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    class RTDServer : IRTDServer {
... 
}

请注意,InstanceContextMode 设置为“single”。因为我们希望服务器不仅响应请求,还要每 100 毫秒主动发送随机数,所以我们不能让 WCF 为每个传入的调用都新建一个 RTDServer (这通常是它的行为),而是希望 WCF 托管一个单例实例,并使其在计时器和连接内部保持活动状态。

这里有两个有趣的 **方法**:Register 创建了一个指向调用它的客户端的回调通道,而 SendData EventHandler 响应 timer.Elapsed 事件来向所有客户端发送一个新的随机数。

// for clients to make themselves known
public void Register() {
    Console.WriteLine("We have a guest...");
    var c = OperationContext.Current.GetCallbackChannel<IRTDClient>();
    if (!clients.Contains(c)) 
        clients.Add(c);
}

客户端将调用 IRTDServer 接口上的 Register 方法来设置到服务器的连接。在此方法中,服务器通过 OperationContext 类获取一个 CalBakcChannel,并将其存储在一个名为 clients List<IRTDClient> 中。

每当又一个 100 毫秒过去时,计时器就会引发由 SendData 处理的 Elapsed 事件。这会生成一个新的随机数,遍历 List 中的所有客户端,并将新值发送给它们。这似乎不重要,但想象一下,这就是您重仓股票的新价格。那么您也想知道。

void SendData(object sender, ElapsedEventArgs e) {
            
    // create random number
    var next = rnd.NextDouble();
    Console.WriteLine("Sending " + next);
            
    // send number to clients
    int ix = 0;
    while (ix < clients.Count) {// can't do foreach because we want to remove dead ones
        var c = clients[ix];
        try { 
            c.SendValue(next);
            ix++;
        }
        catch (Exception e2) { Unregister(c); }
    } 
}

我没有简单地使用 foreach 遍历所有客户端的原因是,客户端可能会在不通知我们的情况下出现故障或消失。如果发生这种情况,我们会收到一个错误,并且我们希望将其从列表中删除。这无法在 foreach 循环内部完成,这就是我们使用此方法的原因。

RTD Excel 插件

这里的 **事情** 变得更有趣了。首先,ExcelAddIn 项目依赖于两个 NuGet 包:Excel-DNA 和 Rx。

Excel-DNA 包允许我们创建一个标准的 .Net(可以是任何语言)类库,并允许 Excel 非常高效地调用其方法。是的,您可以通过 C# 构建非常高级的计算并在 Excel 中使用它们。这将使您的代码比用 VBA 进行相同计算的速度快几个数量级,并且它将 .Net 的所有优点带入 Excel,正如我们在这个项目中将愉快地利用的那样。

在这个项目中,它实际上非常透明,我们给我们的方法一个 ExcelFunction 属性,Excel-DNA 会自动处理其余 **事情**。唯一需要付出更多努力的是通过 Rx 设置实时数据。

Rx 和 Excel-DNA

好的。在回顾了将随机数推送到客户端的基础 **知识** 后,事情变得有趣起来了。我们将做的是将方法调用转换为事件,将事件转换为事件流,然后将 Excel 连接到该流。

第一步是创建一个 EventHandler,然后在每次从客户端调用 SendData 方法时调用它。

// event boilerplate stuff
public delegate void ValueSentHandler(ValueSentEventArgs args);
public static event ValueSentHandler OnValueSent;
public class ValueSentEventArgs : EventArgs {
    public double Value { get; private set; }
    public ValueSentEventArgs(double Value) {
        this.Value = Value;
    }
}
// this gets called by the server if there is a new value
public void SendValue(double x) {
    // invert method call from WCF into event for Rx
    if (OnValueSent != null)
        OnValueSent(new ValueSentEventArgs(x));
}

下一步是将这些单独的事件转换为事件流,即所谓的 Observable。这是通过一个接受 observer 作为参数的函数完成的,并将该 observer 连接到我们上面声明的 OnDataSent 事件。

static Func<IObserver<double>, IDisposable> Event2Observable = observer => {
    OnValueSent += d => observer.OnNext(d.Value);   
    return Disposable.Empty;                        
};

工作原理如下。Func<IObserver<double>, IDisposable> 意味着这是一个函数委托,它接受一个 IObserver<double> 并返回一个 IDisposable。返回 IDisposable 是因为在序列完成后,可以调用它来清理任何需要清理 **的东西**。由于我们将永远继续下去,我们将返回 Disposable.Empty

在 **里面** 还有另一件 **事情**:OnValueSent += d => observer.OnNext(d.Value); 这是 **有趣** 的 **部分**,每次触发 DataSent 事件时,它都会调用 observer 上的 OnNext。这就是将单个事件链接成流的方式。

最后一步在这里完成,其中上面的函数被传递给 Excel-DNA。

 [ExcelFunction("Gets realtime values from server")]
public static object GetValues() {
            
    // a delegate that creates an observable over Event2Observable
    Func<IObservable<double>> f2 = () => Observable.Create<double>(Event2Observable);
    //  pass that to Excel wrapper   
    return RxExcel.Observe("GetValues", null, f2);          
}

RxExcel.Observe 方法需要一个 Func<IObservable<T>> 来执行其 RTD **魔术**,所以这就是我们要 **给** 它的。第一条语句创建了返回一个使用 Event2Observable 委托创建的 Observable 的 Func。

最后一步是将它与创建它的函数名一起传递到 RxExcel.Observe 中。Excel-DNA 使用它来设置与调用函数的单元格的连接。

关注点

本项目旨在做到最少,仅包含一个原型来使 **想法** 可行,而不分散 **核心** 的注意力。

最后,我对将所有这些 **东西** 串联起来所需的代码量感到非常惊喜,我想我可以将事件替换为纯委托,因为我们没有向插件类外部公开任何 **东西**。我将在本文章的下一次迭代中进行处理。

请告诉我它是否对您有帮助。如果您有任何问题,当然可以在评论中留下。而且,如果您喜欢,请不要忘记评分。

历史

2013 年 10 月 2 日:首次上传

© . All rights reserved.