基于ASP.NET MVC、SignalR和Knockout的实时UI同步——适用于协同工作UI和持续客户端






4.89/5 (21投票s)
演示如何使用ASP.NET MVC、SignalR、EF和Knockout Js构建实时同步UI
引言
新时代的Web应用程序可能需要提供新时代的用户体验——并且应该正确处理协同工作和持续客户端场景。这涉及到确保用户界面在设备和用户之间正确同步,以确保应用程序和用户界面的状态“保持不变”。
用户视图之间的实时数据同步*曾经*很难,尤其是在Web应用程序中。大多数情况下,第二个用户需要刷新屏幕才能看到第一个用户所做的更改,或者我们需要实现一些长轮询来获取数据并手动进行更新。
现在,借助SignalR和Knockout,ASP.NET开发人员可以利用用户之间的视图模型同步,这将以最少的代码大大简化这些场景。本文讨论如何实现一个实时待办事项板,它将同步访问应用程序的用户之间的数据。这意味着,用户可以更改他们的任务(添加/删除/更新等),其他用户将立即看到这些更改。重点在于技术,我并不是想在这里构建一个出色的用户体验。
我知道我们对待办事项示例感到厌倦,但现在让我们构建一个待办事项应用程序,它可以通过完整的CRUD支持和持久性,实时同步你和你的妻子(或你的团队成员)之间的任务。是的,我们将使用适当的视图模型(哦,这在JavaScript中可能吗?)使代码保持最小化和可维护。
所以,观看此视频,在这里你可以看到你在一个屏幕上对任务进行的更改(添加、删除、更新等)。你可以看到数据正在多个用户之间同步。我们将使用KnockoutJs来维护一个视图模型,并使用SignalR在用户之间同步视图模型。如果你不熟悉Knockout和SignalR,我们将在途中快速了解它们。
首先,让我们创建一个新的ASP.NET MVC 3.0应用程序。创建一个空项目,我已经安装了ASP.NET MVC 3工具更新。创建ASP.NET MVC项目后,调出Nuget控制台(视图->其他窗口->包管理器控制台),并安装Knockout和SignalR的Nuget包。
install-package knockoutjs
以及SignalR
install-package signalr
此外,如果你还没有最新版本的Entity Framework,也请安装它,以便我们可以使用Code First功能。
Install-Package EntityFramework
如果你已经熟悉Knockout和SignalR,可以跳过接下来的两个标题,直接跳到“构建KsigDo”部分。
Knockout
Knockout Js是一个很棒的JavaScript库,它允许你遵循MVVM约定,将用户控件绑定到JavaScript视图模型。这非常酷,因为它允许你以最少的代码非常容易地构建丰富的UI。这是一个快速示例,展示了如何将HTML元素绑定到JavaScript视图模型。
这是一个非常简单的视图模型
// This is a simple *viewmodel* var viewModel = { firstName: ko.observable("Bert"), lastName: ko.observable("Bertington") }; // Activates knockout.js ko.applyBindings(viewModel);
属性的类型是ko.observable(..)
,如果你想将ViewModel转换为一个对象(你可以通过网络发送),你可以使用ko.toJS(viewModel)
轻松做到这一点。现在,让我们将上面的视图模型绑定到一个视图。
绑定发生在data-bind
属性中,你可能会看到我们将文本框的值绑定到firstname和lastname变量。当你调用ko.applyBindings
时,Knockout将进行必要的连接,以便视图模型属性与目标控件的属性值同步。
<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
KnockoutJs非常容易学习,最好的入门方法是阅读Knockout开发者在这里提供的交互式教程:http://learn.knockoutjs.com/。
更新:发现Shawn写了一篇关于Knockout的综合文章,也请阅读。
SignalR
SignalR是最近微软开发人员遇到的“自切片面包以来最伟大的事情”。(想知道原因,可以阅读我的文章HTML5正在大肆盛行,可能部分杀死HTTP。)无论如何,SignalR是一个用于ASP.NET的异步信号库,旨在帮助构建实时、多用户交互式Web应用程序。如果你最近听说过Node、Backbone、Nowjs等,你就知道我在说什么。如果不知道,你很快就会知道的。
理解SignalR最简单的起点是查看Hub快速入门示例。请查看该示例,然后回来。
你可以在服务器端从SignalR.Hubs.Hub继承你的Hub——SignalR将在客户端生成必要的轻量级JavaScript代理,这样你就可以通过网络调用你的Hub,甚至支持类型化参数。不仅如此。SignalR还在你的Hub中提供了动态的“Clients”和“Caller”对象,这样你就可以通过服务器端的代码直接调用用JavaScript编写的客户端方法。非常智能。SignalR将其整个实现隐藏在其漂亮的小API之下。
构建KsigDo应用程序
现在,让我们继续构建我们的KsigDo应用程序。让我们一步一步地将这些部分组合起来。
使用Entity Framework Code First进行持久化的任务模型
在你的ASP.NET MVC应用程序中,转到Models文件夹,并添加一个新的Code First模型文件。我们的模型非常简单,如你所见,我们有一个任务ID和任务标题,以及一些定义的验证规则,例如标题长度。此外,Completed
属性决定任务是否已完成。
如果你不熟悉Entity Framework Code First,这里有Scott博客中的好文章,这里还有一些其他资源。
public class KsigDoContext : DbContext
{
public DbSet<Task> Tasks { get; set; }
}
public class Task
{
[Key]
public int taskId { get; set; }
[Required] [MaxLength(140)] [MinLength(10)]
public string title { get; set; }
public bool completed { get; set; }
public DateTime lastUpdated { get; set; }
}
上面使用的DbContext
和DbSet
类是EF4 Code-First库的一部分。此外,我们正在使用Key
、Required
等属性进行数据注释,以提供基本的验证支持。
用于基本操作的TaskHub
在你的ASP.NET MVC项目中创建一个名为Hubs的新文件夹,并添加一个新的TaskHub.cs文件(不,我们现在不使用控制器)。是的,你可以将你的Hub放在任何地方。这是我们的TaskHub
,它继承自SignalR.Hubs.Hub
类。你可能会看到我们正在使用这个Hub来执行我们任务模型中的大多数CRUD操作。
public class Tasks : Hub
{
/// <summary>
/// Create a new task
/// </summary>
public bool Add(Task newTask)
{
try
{
using (var context = new KsigDoContext())
{
var task = context.Tasks.Create();
task.title = newTask.title;
task.completed = newTask.completed;
task.lastUpdated = DateTime.Now;
context.Tasks.Add(task);
context.SaveChanges();
Clients.taskAdded(task);
return true;
}
}
catch (Exception ex)
{
Caller.reportError("Unable to create task. Make sure title length is between 10 and 140");
return false;
}
}
/// <summary>
/// Update a task using
/// </summary>
public bool Update(Task updatedTask)
{
using (var context = new KsigDoContext())
{
var oldTask = context.Tasks.FirstOrDefault(t => t.taskId == updatedTask.taskId);
try
{
if (oldTask == null)
return false;
else
{
oldTask.title = updatedTask.title;
oldTask.completed = updatedTask.completed;
oldTask.lastUpdated = DateTime.Now;
context.SaveChanges();
Clients.taskUpdated(oldTask);
return true;
}
}
catch (Exception ex)
{
Caller.reportError("Unable to update task. Make sure title length is between 10 and 140");
return false;
}
}
}
/// <summary>
/// Delete the task
/// </summary>
public bool Remove(int taskId)
{
try
{
using (var context = new KsigDoContext())
{
var task = context.Tasks.FirstOrDefault(t => t.taskId == taskId);
context.Tasks.Remove(task);
context.SaveChanges();
Clients.taskRemoved(task.taskId);
return true;
}
}
catch (Exception ex)
{
Caller.reportError("Error : " + ex.Message);
return false;
}
}
/// <summary>
/// To get all the tasks up on init
/// </summary>
public void GetAll()
{
using (var context = new KsigDoContext())
{
var res = context.Tasks.ToArray();
Caller.taskAll(res);
}
}
}
Clients
和Caller
属性由SignalR作为Hub类定义的一部分提供。令人惊讶的是,这些是你可以概念性地用来调用用JavaScript编写的客户端方法的动态对象。SignalR使用长轮询或WebSockets或任何其他方式进行管道连接,我们不必关心。此外,正如我前面提到的,SignalR将生成一个客户端代理Hub,以调用我们上面编写的TaskHub
中的方法,我们很快就会看到如何使用它。例如,当客户端在初始化期间调用上述Hub中的GetAll
方法时,调用GetAll
方法的客户端(Caller)将收到对其taskAll
JavaScript方法的回调,其中包含所有现有任务。
同样地,假设我们的客户端Hub有诸如`taskUpdated`、`taskAdded`、`taskRemoved`等JavaScript方法——我们正在使用`Clients`动态对象调用这些方法,这样每当发生更新、添加或删除时,此信息就会广播到所有当前连接的客户端。
主视图
现在,让我们继续创建客户端。添加一个“Home”控制器和一个“Index”动作。创建一个新的“Index”视图。此外,请确保你已连接必要的JavaScript脚本以导入Knockout和SignalR库(请参阅代码)。
我们的Index页面有几个视图模型和一些HTML(视图)。对于视图模型,我们有一个taskViewModel
和一个taskListViewModel
,如下所示。你可能会注意到我们的taskViewModel
具有与我们的实际任务模型几乎相同的属性,这样SignalR可以在我们调用TaskHub
中的方法时非常轻松地管理序列化/映射。
你可以看到,在`taskListViewModel`中,我们正在访问`$connection.tasks`代理,它提供了一个代理对象来访问`TaskHub`中的方法。此外,我们正在通过`this.hub`指针将诸如`tasksAll`、`taskUpdated`等方法附加到`$connection.tasks`,并且这些方法从`TaskHub`类中“调用”,正如我们前面所看到的,以虚拟地将数据“推送”到客户端。
$(function () {
//---- View Models
//Task View Model
function taskViewModel(id, title, completed, ownerViewModel) {
this.taskId = id;
this.title = ko.observable(title);
this.completed = ko.observable(completed);
this.remove = function () { ownerViewModel.removeTask(this.taskId) }
this.notification = function (b) { notify = b }
var self = this;
this.title.subscribe(function (newValue) {
ownerViewModel.updateTask(ko.toJS(self));
});
this.completed.subscribe(function (newValue) {
ownerViewModel.updateTask(ko.toJS(self));
});
}
//Task List View Model
function taskListViewModel() {
//Handlers for our Hub callbacks
this.hub = $.connection.tasks;
this.tasks = ko.observableArray([]);
this.newTaskText = ko.observable();
var tasks = this.tasks;
var self = this;
var notify = true;
//Initializes the view model
this.init = function () {
this.hub.getAll();
}
//Handlers for our Hub callbacks
//Invoked from our TaskHub.cs
this.hub.taskAll = function (allTasks) {
var mappedTasks = $.map(allTasks, function (item) {
return new taskViewModel(item.taskId, item.title,
item.completed, self)
});
tasks(mappedTasks);
}
this.hub.taskUpdated = function (t) {
var task = ko.utils.arrayFilter(tasks(),
function (value)
{ return value.taskId == t.taskId; })[0];
notify = false;
task.title(t.title);
task.completed(t.completed);
notify = true;
};
this.hub.reportError = function (error) {
$("#error").text(error);
$("#error").fadeIn(1000, function () {
$("#error").fadeOut(3000);
});
}
this.hub.taskAdded = function (t) {
tasks.push(new taskViewModel(t.taskId, t.title, t.completed, self));
};
this.hub.taskRemoved = function (id) {
var task = ko.utils.arrayFilter(tasks(), function (value) { return value.taskId == id; })[0];
tasks.remove(task);
};
//View Model 'Commands'
//To create a task
this.addTask = function () {
var t = { "title": this.newTaskText(), "completed": false };
this.hub.add(t).done(function () {
console.log('Success!')
}).fail(function (e) {
console.warn(e);
});
this.newTaskText("");
}
//To remove a task
this.removeTask = function (id) {
this.hub.remove(id);
}
//To update this task
this.updateTask = function (task) {
if (notify)
this.hub.update(task);
}
//Gets the incomplete tasks
this.incompleteTasks = ko.dependentObservable(function () {
return ko.utils.arrayFilter(this.tasks(), function (task) { return !task.completed() });
}, this);
}
var vm = new taskListViewModel();
ko.applyBindings(vm);
// Start the connection
$.connection.hub.start(function () { vm.init(); });
});
每当创建一个`taskViewModel`时,`taskListViewModel`的实例将作为其`ownerViewModel`传递,这样我们就可以在当前任务的属性更改时调用`taskListViewModel`的`updateTask`方法。在`taskListViewModel`中,我们还有诸如`addTask`、`removeTask`等方法,这些方法直接绑定到我们的“视图”。
我们正在创建一个`taskListViewModel`的新实例,然后调用Knockout来完成将绑定应用于视图的工作。请查看“视图”部分。
<div id="error" class="validation-summary-errors">
</div>
<h2> Add Task</h2>
<form data-bind="submit: addTask">
<input data-bind="value: newTaskText"
class="ui-corner-all" placeholder="What needs to be done?" />
<input class="ui-button" type="submit" value="Add Task" />
</form>
<h2>Our Tasks</h2>
You have <b data-bind="text: incompleteTasks().length"> </b> incomplete task(s)
<ul data-bind="template: { name: 'taskTemplate', foreach: tasks }, visible: tasks().length > 0">
</ul>
<script type="text/html" id="taskTemplate">
<!--Data Template-->
<li style="list-style-image: url('/images/task.png')">
<input type="checkbox" data-bind="checked: completed" />
<input class="ui-corner-all" data-bind="value: title, enable: !completed()" />
<input class="ui-button" type="button" href="#"
data-bind="click: remove" value="x"></input>
</li>
</script>
<span data-bind="visible: incompleteTasks().length == 0">All tasks are complete</span>
如果你查看“添加任务”标题下方,你会看到我们将文本框的值绑定到`taskListViewModel`的`newTaskText`属性,并将表单提交绑定到`taskListViewModel`中的`addTask`方法。`
- `绑定到视图模型的`Tasks`属性。如果你仔细查看,`taskListViewModel`的`Tasks`属性是一个`koObservableArray`,它几乎就像一个`ObservableCollection`,当数组中插入/删除项目时,它会通知绑定的控件。
- `的渲染。
删除也以同样的方式工作,你可以看到“x”按钮绑定到每个单独的`taskViewModel`的remove方法,该方法在内部调用`taskListViewModel`的`removeTask`方法,以使用hub代理调用`TaskHub`中的`Remove`方法,然后`taskRemoved`将被调用到所有客户端,在那里我们实际上从项目集合中删除该项目。
更新项目
一旦项目绑定到模板,请注意我们正在订阅`taskViewModel`中任务的更改事件。每当属性更改时,我们都会调用`ownerViewModel`中的`updateTask`方法,该方法再次调用hub的update方法,将任务发送到我们`TaskHub`的update方法——这得益于SignalR的连接。在那里,我们尝试保存项目,如果一切顺利,更新后的项目将从`TaskHub`广播到所有客户端,通过调用我们附加到hub的`taskUpdated` JavaScript方法,在那里我们实际上更新所有客户端中项目的属性。
结论
惊喜,我们完成了。代码量极少,工作量极小,成果却很棒。感谢ASP.NET、SignalR、Entity Framework和Knockout。这就是我喜欢.NET的原因
。编码愉快,但请在Twitter上关注我 @amazedsaint 并订阅此博客。
您可能还会喜欢这些类似的文章:Kibloc – 使用Kinect进行实时、基于距离的物体跟踪和计数 和 5个很棒的程序员学习资源(帮助您和您的孩子增长极客神经元)。
添加和删除项目
查看`taskListViewModel`中的`addTaskMethod`,你会看到我们正在创建一个新任务,然后调用“hub”的“add”方法,该方法在内部调用服务器端`TaskHub`的`Add`方法。在`TaskHub`的`Add`方法中,你会看到我们正在通过调用客户端的`taskAdded`方法将添加的任务广播给所有客户端——在那里我们正在更新`items` observable数组,这样Knockout将根据数据模板`tasktemplate`(参见我们有tasktemplate的上面视图代码)在内部管理`
- `下新的`