使用 SignalR 上的 MVVM 模式简化实时 Web 应用程序 - 第 1 部分
SignalR 非常适合构建实时 Web 功能。MVVM 非常适合开发前端。如果它们可以一起使用会怎样?本技巧将展示如何做到这一点,以一个简单的项目为例,在 Web 浏览器上制作实时图表。
引言
关于 MVVM 模式的文章已经很多了,这是理所当然的,因为它带来了显著的优势,使您的应用程序更容易开发、测试和维护。然后是 SignalR,一个 .NET 库,它将 Web 上复杂的推送技术机制抽象为简单的函数调用。
我编写了一个轻量级的 C#/.NET/JavaScript 库,并将其发布为一个免费的开源项目,名为 dotNetify,它将这两者结合起来。前提很简单——我希望将我的 Web 应用程序的前端架构设计为由视图和视图模型组成,并且它们通过自动化、声明式绑定进行通信,这是 MVVM 的标志。当然,已经有基于 JavaScript 的 Web 框架可以做到这一点,但关键是:我希望视图模型在服务器上,用 .NET 编写。为什么?好吧,
- 我不想编写服务层及其 AJAX 调用和 RESTful Web API,只是为了来回传输数据——这是一项繁琐的工作,如果自动化绑定可以以同样高的带宽效率为我完成这项工作,那不是很好吗?
- 我使用 C#/.NET 已经非常高效,有些表示逻辑问题我可以轻松优雅地用 .NET 解决,但如果我使用那些 JavaScript 框架,我知道这会给我带来很多麻烦。
当然,挑战在于开发这种自动化、双向绑定,它可以在 Web 上工作,并且只调度更新以提高带宽效率,这就是 SignalR 的用武之地。它已经能够促进 Web 上的双向通信,所以我只需要在其之上构建一个 MVVM 风格的抽象。
另一个优秀的开源库 Knockout(更新:也支持 React!) 为我提供了在 HTML 视图上进行声明式绑定的语法和机制。Knockout 有在浏览器上运行的视图模型的概念,所以我做了对我来说最合乎逻辑的事情
- 从我编写的 .NET 视图模型自动生成客户端视图模型
- 让 Knockout 促进 HTML 视图和自动生成的客户端视图模型之间的绑定
- 并让 SignalR 促进客户端视图模型与服务器端 .NET 视图模型之间的绑定
现在,我的 HTML 视图和 .NET 视图模型之间实现了端到端的自动化绑定;太棒了!
我添加的另一个部分是提供在客户端注入 JavaScript 代码的功能,以允许我完全控制此通信过程。我称之为“代码隐藏”。外面有很多很棒的 JavaScript UI 组件,但当然它们不提供可绑定属性——所以我需要代码隐藏在幕后工作,以促进我在 HTML 视图上声明的绑定。
所以,我想我已经把所有拼图都组装好了。为了演示其效果,我编写了一个简单的应用程序,在浏览器上显示一个图表,每秒更新一次数据。
实时图表 Web 应用程序
Github 存储库中的源代码只是一个基本的 ASP.NET MVC 解决方案,它是从 Visual Studio 2015 模板创建的(也兼容 VS 2013)。我打包了 dotNetify
库并将其发布到 Nuget.org,因此当您编译它时,它会首先拉取库文件及其所有依赖项。
现在我们来谈谈视图模型。这是一个与 UI 无关的视图抽象,我将在其中编写我的表示逻辑。对于实时图表应用程序,视图模型非常简单
ViewModel
public class LiveChartVM : BaseVM
{
private Timer _timer = new Timer(1000);
private Random _random = new Random();
public double[] Data
{
get { return Get<double[]>(); }
set { Set(value); }
}
public LiveChartVM()
{
// Create initial data for the chart.
Data = new double[20];
for (int i = 0; i < 20; i++)
Data[i] = _random.Next(1, 100);
_timer.Elapsed += Timer_Elapsed;
_timer.Start();
}
public override void Dispose()
{
_timer.Stop();
_timer.Elapsed -= Timer_Elapsed;
base.Dispose();
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
Data = new double[] { _random.Next(1, 100) };
PushUpdates();
}
}
只有一个名为 Data
的属性来为图表提供数据——一个将绑定到视图的双精度浮点数数组,并且有一个基本的 Windows 计时器来模拟每 1 秒更新一次。注意视图模型继承自 BaseVM
。这是 dotNetify
库中的基类,它隐藏了我们之前讨论的所有细节。
BaseVM
类提供自动属性方法 Get
和 Set
,它们实现了 INotifyPropertyChanged
以支持绑定机制。它提供 PushUpdates
方法,顾名思义:将更新推送到客户端。如果您认为为什么这不能在 Data
属性值更新时自动完成,我的理由是优化:当有多个活动视图模型时,此方法会将它们全部池化,以便批量处理更新,而无需多次昂贵的调用。
接下来是视图;它也极其简单
视图
<html>
<head>
<script src="/Scripts/require.js" data-main="/Scripts/app"></script>
<script src="/Scripts/Example/chart.min.js" type="text/javascript"></script>
<script src="/Scripts/Example/LiveChart.js" type="text/javascript"></script>
</head>
<body>
<h3>Live Chart</h3>
<div data-vm="LiveChartVM">
<canvas data-bind="vmOn: { Data: updateChart }" />
</div>
</body>
</html>
第一个 script
标签使用 RequireJS 模块加载器库加载 dotNetify
JavaScript 库及其所有依赖项。第二个 script
标签加载 ChartJS 库,它提供 HTML5 画布元素上的图表渲染,最后一个是我们稍后将讨论的“代码隐藏”。
注意“data-vm
”属性。这是 dotNetify
定义范围的方式:基本上,它内部的 HTML 元素将属于指定视图模型的管辖范围;在本例中,它是 LiveChartVM
,我们之前定义的视图模型。
“data-bind
”属性来自 Knockout,这是它在 HTML 元素上声明绑定的方式。它最棒的地方在于 Knockout 也支持自定义绑定,我充分利用了这一点。ChartJS 自然不提供可绑定属性来填充其数据,所以我编写了一个名为“vmOn
”的自定义绑定,它接受一个对象字面量格式的 2 个参数:要绑定的视图模型属性,以及当该属性值更改时要调用的“代码隐藏
”方法。
这就引出了代码隐藏。在一个完美的世界里,一切都可以声明式绑定,我就不必编写它。但我没有生活在那个世界里,所以它来了
代码隐藏
var LiveChartVM = (function () {
return {
// On data update, update the chart.
updateChart: function (iItem, iElement) {
var vm = this;
var data = vm.Data();
if (vm._chart == null) {
vm._chart = this.createChart(data, iElement);
vm._counter = data.length;
}
else {
for (var i = 0; i < data.length; i++) {
vm._chart.addData([data[i]], vm._counter++);
// Remove the oldest data.
vm._chart.removeData();
}
}
// Reset the data.
vm.Data(null);
},
// Create the chart with ChartJS.
createChart: function (iData, iElement) {
var labels = [];
for (var i = 0 ; i < iData.length; i++)
labels.push(i);
var chartData = {
labels: labels,
datasets: [{
label: "My live dataset",
data: iData,
fillColor: "rgba(217,237,245,0.2)",
strokeColor: "#9acfea",
pointColor: "#9acfea",
pointStrokeColor: "#fff"
}]
};
return new Chart(iElement.getContext('2d')).Line(chartData, {
responsive: true, animation: false });
}
}
})();
这是以模块模式风格编写的,具有执行匿名函数——有一篇很好的博客文章讨论了这个问题。但不要认为这是你普通的、司空见惯的 JavaScript 代码;dotNetify
库通过与视图模型名称匹配的命名约定自动找到它,并对其进行预处理,以便“this
”引用作用域到客户端视图模型,以及其他事情。
此代码提供了您记得绑定到视图的“updateChart
”方法。在初始页面加载时,当 Data
属性设置为其初始数据集时,会调用此方法,然后它又调用“createChart
”来实例化并填充一个新的 ChartJS
对象。随后,当 Data
属性每 1 秒设置为新值时,该方法会更新 ChartJS
对象。
实时演示
实时演示可在我的网站 http://dotnetify.net/index/livechart 上找到。代码略有不同,因为在这里我也发送了图表的 X 轴标签,但逻辑相同。
此网站托管在满足 SignalR 使用 Web Socket 要求的服务器上,所以我很高兴看到它的实际应用——非常高效的传输,开销很低。顺便说一句,这个演示在 iPad 上看起来也非常好!
后记
第一部分到此结束。编写这个 dotNetify 库对我来说很有趣,因为我知道我真的可以将它应用于实际的 Web 应用程序,并提高生产力——而且更快乐(非常重要!)。
这个库还有很多功能——基本的 CRUD、懒加载、嵌套视图、视图之间的通信等等。请访问网站 http://dotnetify.net 及其 GitHub 存储库。