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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (21投票s)

2012 年 1 月 31 日

CPOL

10分钟阅读

viewsIcon

209517

演示如何使用ASP.NET MVC、SignalR、EF和Knockout Js构建实时同步UI

引言

新时代的Web应用程序可能需要提供新时代的用户体验——并且应该正确处理协同工作和持续客户端场景。这涉及到确保用户界面在设备和用户之间正确同步,以确保应用程序和用户界面的状态“保持不变”。

image

用户视图之间的实时数据同步*曾经*很难,尤其是在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; }

}

上面使用的DbContextDbSet类是EF4 Code-First库的一部分。此外,我们正在使用KeyRequired等属性进行数据注释,以提供基本的验证支持。

用于基本操作的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);
        }

    }
}

ClientsCaller属性由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`,当数组中插入/删除项目时,它会通知绑定的控件。

    添加和删除项目

    查看`taskListViewModel`中的`addTaskMethod`,你会看到我们正在创建一个新任务,然后调用“hub”的“add”方法,该方法在内部调用服务器端`TaskHub`的`Add`方法。在`TaskHub`的`Add`方法中,你会看到我们正在通过调用客户端的`taskAdded`方法将添加的任务广播给所有客户端——在那里我们正在更新`items` observable数组,这样Knockout将根据数据模板`tasktemplate`(参见我们有tasktemplate的上面视图代码)在内部管理`

      `下新的`
    • `的渲染。

      删除也以同样的方式工作,你可以看到“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个很棒的程序员学习资源(帮助您和您的孩子增长极客神经元)

© . All rights reserved.