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

ASP.NET MVC 单页应用 (SPA) 结合 Upida/Jeneva (前端/AngularJS)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (12投票s)

2013年11月29日

CPOL

8分钟阅读

viewsIcon

98703

downloadIcon

2104

本文演示了如何使用 AngularJS 创建一个单页 Web 应用程序。这对于希望学习 Angular 和 MVVM 实践的人来说会很有帮助。

引言

上一篇文章中,我演示了如何使用 WebAPIJeneva.Net 创建一个 JSON 驱动的 Web 后端。在本文中,我将展示一些使用 Angular.js 创建单页前端的基本技巧。如果您不熟悉 AngularJS 的基本知识,请参考这个 YouTube 频道

背景

我们假设已经有一个工作的后端,它包含两个控制器——一个提供 HTML 视图的 MVC 控制器,以及一个提供方便的 REST JSON API 的 WebAPI 控制器。

public class ClientController : System.Web.Mvc.Controller
{
    public ActionResult List()
    {
        return this.View();
    }
 
    public ActionResult Create()
    {
        return this.View();
    }
 
    public ActionResult Edit()
    {
        return this.View();
    }
}
public class ClientController : System.Web.Http.ApiController
{
    public IClientService ClientService { get; set; }

    public Client GetById(int id)
    {
        return this.ClientService.GetById(id);
    }

    public IList<client> GetAll()
    {
        return this.ClientService.GetAll();
    }

    public void Save(Client item)
    {
        this.ClientService.Save(item);
    }

    public void Update(Client item)
    {
        this.ClientService.Update(item);
    }

    [HttpPost]
    public void Delete(int id)
    {
        this.ClientService.Delete(id);
    }
}

这两个控制器看起来都非常简单且可用。请参考我上一篇文章,了解我如何创建后端。我的目标是为这两个控制器创建前端。前端必须是一个单页浏览器应用程序 (SPA)。许多人认为 SPA 并没有什么用,因为它不会生成指向不同页面的链接,而且 SPA 需要付出巨大的努力,不值得这样做。我将向您展示,使用 AngularJS,您可以像创建多页应用程序一样轻松地创建 SPA,并且多页应用程序的所有优点都将保留在 SPA 中。

基本结构

每个单页应用程序 (SPA) 中最重要的部分是它的结构。我们将有 3 个视图:“客户列表”、“创建客户”和“编辑客户”。这实际上意味着我们将为这些视图编写三个 AngularJS 控制器——clientListController.jsclientCreateController.jsclientEditController.js。“vendor”文件夹包含所有第三方库。我们也有自定义过滤器。通常,这种结构还会有“directives”和“services”文件夹,但在这种简单的 SPA 中,我没有使用任何自定义指令和服务。

前端的基础部分是位于 js 文件夹根目录下的 app.js 文件。让我们分析一下它的内容。

var myclients = myclients || {};
myclients.app = angular.module("myclients", ["ngRoute", "jenevamodule"]);
 
myclients.app.config(function ($routeProvider) {
    $routeProvider
        .when("/", {
            templateUrl: "client/list",
            controller: "clientListController"
        })
        .when("/client/list", {
            templateUrl: "client/list",
            controller: "clientListController"
        })
        .when("/client/create", {
            templateUrl: "client/create",
            controller: "clientCreateController"
        })
        .when("/client/edit/:id", {
            templateUrl: "client/edit",
            controller: "clientEditController"
        })
        .otherwise({
            templateUrl: "home/notfound"
        });
});
 
$jeneva.settings.baseUrl = "/api/";

在第一行,我定义了应用程序的主命名空间。然后,使用 AngularJS,我创建了我主模块的一个实例。这是每个 Angular 应用程序的常见任务。该模块依赖于两个不同的模块:ngRoutejenevamodule,它们都是独立的、可重用的 Angular 模块。最有趣的部分在中间。正如您所见,我在这里定义了几个路由。这实际上是任何前端的核心。简单来说——在这里,我告诉 AngularJS——根据浏览器中的 URL,应该启用哪个 js 控制器和哪个 视图。在任何时刻,我的应用程序将只有一个活动的 视图控制器,具体取决于浏览器窗口中输入的 URL,仅此而已。例如,如果我输入 URL:client/list,那么 client/list 视图和 clientListController 将被加载为活动状态。

app.js 的最后一部分专门用于 Jeneva.Net。我使用它是因为它使我的应用程序更易于开发和维护。您可以参考我上一篇文章,了解有关 Jeneva.Net 的更多信息。我告诉 Jeneva,我的 WebAPI 控制器位于 /api/ 子文件夹中。

JS 控制器

现在,让我们看看控制器。第一个是 clientListController.js

myclients.app.controller(
    "clientListController", ["$scope", "jeneva",
    function ($scope, jeneva) {
 
    $scope.clientRows = new Array();
 
    $scope.ClientRow = function (id) {
        this.id = id;
        this.name = null;
        this.lastname = null;
        this.age = null;
        this.logins = new Array();
    };
 
    $scope.loadClients = function() {
        jeneva.get("client/getall")
        .then(function (items) {
            angular.forEach(items, function (p, i) {
                var row = new $scope.ClientRow(p.id);
                row.name = p.name;
                row.lastname = p.lastname;
                row.age = p.age;
                angular.forEach(p.logins, function (q, j) {
                    row.logins.push(q.name);
                });
                $scope.clientRows.push(row);
            });
        });
    };

   $scope.onDelete = function (clientId) {
      jeneva.post("client/delete/" + clientId)
      .then(function () {
         $scope.loadClients();
      });
   };
 
    $scope.$on("$routeChangeSuccess", function () {
        jeneva.setScope($scope);
        $scope.loadClients();
    });
}]);

正如您所见,我引用了 app.js 文件前两行中创建的 myclients.app 模块。我的控制器名为 clientListController,并注入了 2 个变量:$scopejeneva。在控制器的主体中,我定义了 $scope.clientRows——这是我的视图的模型——来自数据库的客户列表。与后端交互是通过 $scope.loadClients 方法完成的,该方法使用 jeneva 服务调用 WebAPI 控制器。我使用 jeneva 服务而不是直接的 Angular ($http) 调用是因为——Jeneva 会自动管理后端失败的情况,并在视图中的正确位置显示错误消息(基于 jvPath 验证器和 jvErrorkey 指令)。当我的 WebAPI 返回客户列表时,我将它们填充到 $scope.clientRows 字段中。

我的控制器最后一部分是最重要的。我向 $routeChangeSuccess 事件添加了一个处理程序。每次此控制器变为活动状态时,都会触发此事件,即,每次用户导航到“客户列表”视图时,都会触发此事件。在我的例子中,当此事件触发时,将调用 $scope.loadClients 方法,并从后端提取数据。

其他控制器的工作方式大致相同。它们被注入了 $location 变量,该变量用于在 AngularJS SPA 中的视图之间导航。例如,clientCreateController.js

myclients.app.controller(
    "clientCreateController",
    ["$scope", "$location", "jeneva", function ($scope, $location, jeneva) {
 
    $scope.name = null;
    $scope.lastname = null;
    $scope.age = null;
    $scope.loginRows = new Array();
 
    $scope.LoginRow = function () {
        this.name = null;
        this.password = null;
        this.enabled = false;
    };
 
    $scope.onRemoveLoginClick = function (item) {
        var index = $scope.loginRows.indexOf(item);
        $scope.loginRows.splice(index, 1);
    };
 
    $scope.onAddLoginClick = function () {
        var row = new $scope.LoginRow();
        $scope.loginRows.push(row);
    };
 
    $scope.onSave = function () {
        var data = {};
        data.name = $scope.name;
        data.lastname = $scope.lastname;
        data.age = $scope.age;
        data.logins = new Array();
        angular.forEach($scope.loginRows, function (p, i) {
            var item = {};
            item.name = p.name;
            item.password = p.password;
            item.enabled = p.enabled;
            data.logins.push(item);
        });
        jeneva.post("client/save", data)
        .then(function () {
            $location.path("client/list");
        });
    };
 
    $scope.$on("$routeChangeSuccess", function () {
        $scope.onAddLoginClick();
    });
}]);

看看 $scope.onSave 方法。当用户单击客户创建视图上的“保存”按钮时,它会触发。它将所有用户输入的数据收集到一个大的 data 变量中,并使用 jeneva 服务将其作为 JSON 发送给 WebAPI 控制器。如果数据保存成功,则调用 $location.path() 方法,用户将被再次导航到客户列表视图。如果后端失败或后端验证失败,jeneva 服务将处理此问题,所有验证问题都将根据 Jeneva 指令在视图中的正确位置显示(本文稍后您将看到 Jeneva 如何处理这些失败)。

clientEditController.js 的工作方式相同,您可以在源代码中找到它。

HTML 视图

正如您已经知道的,只有三个视图——客户列表创建客户编辑客户。如果您不熟悉 AngularJS,请参考这个 YouTube 频道。让我们看看列表视图。

<a href="#/client/create">NEW CLIENT</a>
<h2>Clients</h2>
<span class="error" jv-error-key></span>
<hr />
<table border="1" style="width: 50%;">
<thead>
    <tr class="head">
        <th>ID</th>
        <th>NAME</th>
        <th>LASTNAME</th>
        <th>AGE</th>
        <th>LOGINS</th>
        <th></th>
    </tr>
</thead>
<tbody>
    <tr ng-repeat="row in clientRows">
        <td ng-bind="row.id"></td>
        <td ng-bind="row.name"></td>
        <td ng-bind="row.lastname"></td>
        <td ng-bind="row.age"></td>
        <td>
            <div ng-repeat="login in row.logins">
                <span ng-bind="login"></span><br />
            </div>
        </td>
        <td>
            <a ng-href="#/client/edit/{{row.id}}">Edit</a>
        </td>
        <td>
            <input type="button" ng-click="onDelete(row.id)" value="Delete" />
        </td>
    </tr>
</tbody>
</table>

对于那些熟悉 AngularJS 的人来说,理解这段代码会很容易。此视图与 clientListController.js 绑定在一起。基本上,这段代码引用了控制器代码中的 $scope.clientRows 字段,并将客户列表显示为 HTML 表格。

这里最有趣的地方是链接。看看这些链接

<a href="#/client/create">NEW CLIENT</a>

正如您所见,URL 以“#”符号开头,这实际上意味着如果您单击链接,浏览器不会重新加载页面。相反,AngularJS 将捕获此事件,并根据 app.js 文件中注册的路由切换活动视图和活动控制器。例如,如果您单击新客户链接,AngularJS 将用创建客户视图的内容替换当前视图的内容,并且活动控制器将变为 clientCreateController.js

另一个有趣的时刻在视图的顶部

<span class="error" jv-error-key></span> 

这个 spanJenevajv-error-key 指令控制。实际上,这意味着当后端服务器向浏览器发送失败响应时,具有空键的错误消息将显示在 span 的主体中。有时,您需要使用无键(或无路径)错误消息,例如,“意外失败”消息或更具体的“您无法删除此客户”(或者您可以为某些错误消息设置静态键)。无键(或静态键)失败消息最棒的一点是它们不需要将任何 JSON 数据发布到服务器。除了无键消息外,您始终可以定义自己的自定义键,例如,您可以定义“server_error”键(路径),然后您可以将具有此键的失败注册到验证上下文中,然后可以使用 jv-error-key 指令在表单的任何位置显示该失败消息。在此示例中,我使用无键消息来处理未捕获的异常,并将它们简单地显示在每个表单的顶部。我还使用无键消息来告知用户因某种原因无法删除客户(请参阅 ClientService 中的删除客户)。

现在看看编辑客户链接

<a ng-href="#/client/edit/{{row.id}}">Edit</a> 

正如您所见,此链接导航到编辑客户视图,它还包含客户的ideditClientController 必须知道如何从链接中提取此id。查看 app.js 文件,了解编辑客户视图的路由是如何定义的。

.when("/client/edit/:id", {
    templateUrl: "client/edit",
    controller: "clientEditController"
}) 

AngularJS 的路由机制能够处理 URL 参数。并且 clientEditController 可以使用 $routeParams 服务访问它们。$routeParams 服务必须像其他服务一样注入到控制器中。有关更多详细信息,请参见源代码。

现在,让我们看看创建客户视图。

<a href="#/client/list">ALL CLIENTS</a>|
<h2>New Client</h2>
<span class="error" jv-error-key></span>
<span class="error" jv-error-key="logins"></span>
<hr />
<table ng-form name="form">
<tr>
    <td>Name</td>
    <td>
        <input name="name" type="text" ng-model="name" jv-path="name" />
        <span class="error" ng-if="form.name.$error.jvpath" 
         ng-repeat="msg in form.name.$jvlist">{{msg}}</span>
    </td>
</tr>
<tr>
    <td>Last name</td>
    <td>
        <input name="lastname" type="text" ng-model="lastname" jv-path="lastname" />
        <span class="error" ng-if="form.lastname.$error.jvpath" 
         ng-repeat="msg in form.lastname.$jvlist">{{msg}}</span>
    </td>
</tr>
<tr>
    <td>Age</td>
    <td>
        <input name="age" type="text" ng-model="age" jv-path="age" />
        <span class="error" ng-if="form.age.$error.jvpath" 
         ng-repeat="msg in form.age.$jvlist">{{msg}}</span>
    </td>
</tr>
<tr>
    <td>Logins</td>
    <td>
        <table>
          <thead>
            <tr>
            <th>Login</th>
            <th>Password</th>
            <th>Enabled</th>
            <th></th>
            </tr>
          </thead>
          <tbody>
            <tr ng-repeat="row in loginRows" ng-form name="loginForm">
            <td>
                <input name="loginName" type="text" ng-model="row.name" 
                 jv-path="{{'logins['+ $index + '].name'}}" />
                <span class="error" ng-if="loginForm.loginName.$error.jvpath" 
                 ng-repeat="msg in loginForm.loginName.$jvlist">{{msg}}</span>
            </td>
            <td>
                <input name="password" type="text" ng-model="row.password" 
                 jv-path="{{'logins['+ $index + '].password'}}" />
                <span class="error" ng-if="loginForm.password.$error.jvpath" 
                 ng-repeat="msg in loginForm.password.$jvlist">{{msg}}</span>
            </td>
            <td>
                <input name="enabled" type="checkbox" ng-model="row.enabled" 
                 jv-path="{{'logins['+ $index + '].enabled'}}" />
                <span class="error" ng-if="loginForm.enabled.$error.jvpath" 
                 ng-repeat="msg in loginForm.enabled.$jvlist">{{msg}}</span>
            </td>
            <td>
                 <input type="button" ng-click="onRemoveLoginClick(row)" value="Delete" />
            </td>
            </tr>
          </tbody>
        </table>
        <div style="padding-bottom: 0.5em;">
             <input type="button" ng-click="onAddLoginClick()" value="Add" />
        </div>
    </td>
</tr>
</table>
<hr />
<input type="button" ng-click="onSave()" value="Save" />

每个 input 元素都使用 ng-model Angular 指令绑定到控制器相应的 $scope 字段。每个 input 元素后面都有一个错误消息 span

<input name="name" type="text" ng-model="name" jv-path="name" />
<span class="error" ng-if="form.name.$error.jvpath" 
ng-repeat="msg in form.name.$jvlist">{{msg}}</span>

或者像这样:

<tr ng-repeat="row in loginRows" ng-form name="loginForm">
<td>
    <input name="loginName" type="text" ng-model="row.name" 
     jv-path="{{'logins['+ $index + '].name'}}" />
    <span class="error" ng-if="loginForm.loginName.$error.jvpath" 
     ng-repeat="msg in loginForm.loginName.$jvlist">{{msg}}</span>

正如您所见,span 带有 ng-ifng-repeat 指令。这基本上意味着,如果 loginFormname 字段验证失败,span 将显示错误消息。第二种情况看起来很奇怪,但也很简单。jv-path="{{'logins['+ $index + '].name'}}" 意味着如果 logins[0].name 字段失败——那么错误消息将被放置在正确的位置。

视图容器

好了,就这些了。我们有视图,有控制器,有配置所有内容的 app.js。最后一步是创建一个页面,也就是单页。托管所有视图和控制器的页面。当用户导航到 Web 应用程序的根目录时,将在此页面中显示此页面。我的容器页面位于此处:views/home/Index.cshtml。它看起来非常简单

<!DOCTYPE html>
<html ng-app="myclients">
<head>
   <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
   <title>MyClients Single Page Angular Application</title>
 
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/vendor/angular.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/vendor/angular-route.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/vendor/jeneva.angular.js")"></script>
 
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/app.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/filters/idtext.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/filters/datetime.js")"></script>
 
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/controllers/clientCreateController.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/controllers/clientListController.js")"></script>
   <script type="text/javascript" 
     src="@Url.Content("/Resources/js/controllers/clientEditController.js")"></script>
   <link href="@Url.Content("~/Resources/css/style.css")" rel="stylesheet"/>
</head>
<body>
    <ng-view>
    </ng-view>
</body>
</html>

首先,我必须使用 ng-app 指令定义应用程序模块。我的模块名为 myclients,这是 app.js 文件中引用的模块。然后我必须包含 angular.jsjeneva.angular.js 以及所有控制器、服务、指令和过滤器。

body 元素只包含一个标签——<ng-view>,正如您可能已经猜到的,AngularJS 将用当前活动的视图替换此标签的内容。就这样。

结论

使用 AngularJS,您可以毫不费力地创建单页应用程序。Jeneva.Net 通过管理验证例程使其更加简单。

参考文献

历史

  • 2013 年 12 月 3 日:初始版本
© . All rights reserved.