Saturday, February 6, 2016

Using AngularJS with Web API

Сегодня мы рассмотрим принципы работы с Web API в ASP.NET MVC, а также способы работы с ApiController через AngularJS. В предыдущей статье я рассмотрел возможность использования Wep API с отображением на стороне клиента через jQuery с генерацией темплейтов с помощью mustache.js. Сегодня все будет намного проще, благодаря AngularJS, в чем вы сами сможете убедиться. Чтобы не быть мне столь голословным, приступим к реализации. Для этого нужно создать новый ASP.NET Web Application проект, который назовем AngularStudentsSample.
Затем нам нужно выбрать шаблон Web API, как показано на рисунке ниже.
Теперь настало самое время приступить к установке пакетов. Нам для работы понадобится AngularJS.Core и AngularJS.Route и Angular.UI.Bootstrap. В общем, вам нужно поставить все пакеты, которые приведены на рисунке ниже.
После того, как наши пакеты успешно установились, нужно подключить их в файле BundleConfig (папка App_Start).
Первое, что мы сделаем, – это добавим новый бандл.
bundles.Add(new ScriptBundle("~/bundles/angular").Include(
    "~/Scripts/angular.js",
    "~/Scripts/angular-route.js",
    "~/Scripts/angular-ui/ui-bootstrap.js",
    "~/Scripts/angular-ui/ui-bootstrap-tpls.js"
    ));
А затем обновим предыдущий по стилям 
bundles.Add(new StyleBundle("~/Content/css").Include(
            "~/Content/bootstrap.css",
            "~/Content/site.css"));
на следующий:
bundles.Add(new StyleBundle("~/Content/css").Include(
    "~/Content/bootstrap.css",
    "~/Content/site.css",
    "~/Content/ui-bootstrap-csp.css"));
Полный исходный код приведен ниже.
public class BundleConfig
{
    // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                    "~/Scripts/jquery-{version}.js"));

        // Use the development version of Modernizr to develop with and learn from. Then, when you're
        // ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
        bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                    "~/Scripts/modernizr-*"));

        bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
                    "~/Scripts/bootstrap.js",
                    "~/Scripts/respond.js"));

        bundles.Add(new ScriptBundle("~/bundles/angular").Include(
            "~/Scripts/angular.js",
            "~/Scripts/angular-route.js",
            "~/Scripts/angular-ui/ui-bootstrap.js",
            "~/Scripts/angular-ui/ui-bootstrap-tpls.js"
            ));

        bundles.Add(new StyleBundle("~/Content/css").Include(
            "~/Content/bootstrap.css",
            "~/Content/site.css",
            "~/Content/ui-bootstrap-csp.css"));
    }
}
Теперь нам нужно открыть файл _Layout.cshtml, который лежит в папке Views\Shared, и найти там следующие строчки, которые находятся в самом конце.
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
И сразу после них добавить следующую строку:
@Scripts.Render("~/bundles/angular")
Следующим делом нам нужно создать класс Student, который будет основной лошадкой нашего приложения. Этот класс нам нужно добавить в папку Models. Ниже представлена реализация данного класса.
public class Student
{
    [Key]
    public int Id { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
    [StringLength(50, MinimumLength = 1)]
    [Display(Name = "Last Name")]
    public string LastName { get; set; }

    [Display(Name = "First Name")]
    [StringLength(50, MinimumLength = 2, ErrorMessage = "First name must be between 2 and 50 characters.")]
    public string FirstName { get; set; }

    public string FullName
    {
        get
        {
            return FirstName + " " + LastName;
        }
    }
}
Теперь создадим DbContext для работы с этим классом. Для этого добавим новую паку, которую назовем DAL (Data Access Layer), и в нее добавим новый класс StudentContext.
public class StudentContext : DbContext
{
    public StudentContext() : base("StudentsConnection")
    {
        Database.SetInitializer(new CreateDatabaseIfNotExists<StudentContext>());
    }
    public DbSet<Student> Students { get; set; }
}
По умолчанию Entity Framework установлен с тем темплейтом, который мы выбрали, поэтому все, что я написал выше, у вас должно работать без необходимости что-либо подгружать. Чтобы это все заработало, осталось добавить новое подключение в Web.config. Для этого открываем Web.config, ищем в нем connectionStrings и добавляем туда следующую строку подключения:
<add name="StudentsConnection" connectionString="Data Source=.;Initial Catalog=Students;Integrated Security=true" providerName="System.Data.SqlClient" />
Полностью connectionStrings у меня выглядит, как показано ниже.
<connectionStrings>
  <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-StudentWebApiSample-20151222060005.mdf;Initial Catalog=aspnet-StudentWebApiSample-20151222060005;Integrated Security=True" providerName="System.Data.SqlClient" />
  <add name="StudentsConnection" connectionString="Data Source=.;Initial Catalog=Students;Integrated Security=true" providerName="System.Data.SqlClient" />
</connectionStrings>
Затем переходим к реализации окон, которые будут отображать нужную нам информацию. Весь биндинг будет построен через AngularJS, поэтому если вам будут непонятны какие-то монеты с биндингом, стоит почитать немного об основах использования данного фреймворка.  В данной статье мы не будем глубоко копать в понимании его принципов. Так что если вы видите его впервые, то можете не понять, как работает некоторая часть кода. Но для того чтобы оценить простоту использования, этого материала будет больше чем достаточно. Для начала в папке Views создадим новую папку, которую назовем appview. Затем в нее добавим новый html файл, который назовем studentsview.html. Его реализация приведена ниже.
<div id="mainWindow">
    <div class="row">
        <button id="addNewStudent" class="btn btn-primary" ng-click="addNewStudent()">Create New</button>
    </div>
    <div class="row">
        <table id="studentTable" class="table table-bordered">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>
                        First Name
                    </th>
                    <th>
                        Last Name
                    </th>
                    <th style="width: 40px">
                    </th>
                    <th style="width: 40px">
                    </th>
                </tr>
            </thead>
            <tbody ng:repeat="student in students">
                <tr>
                    <td>
                        {{ student.Id}}
                    </td>
                    <td>
                        {{ student.FirstName }}
                    </td>
                    <td>
                        {{ student.LastName }}
                    </td>
                    <td>
                        <button class="btn btn-danger btn-xs btn-block" style="width: 36px" ng-click="editRow(student)"><span class="glyphicon glyphicon-edit"></span></button>
                    </td>
                    <td>
                        <button class="btn btn-danger btn-xs btn-block" style="width: 36px" ng-click="removeRow($index, student)"><span class="glyphicon glyphicon-trash"></span></button>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</div>
Если вы раньше работали с генерацией темплейтов на клиенте с помощью jQuery templates, mustache.js, JsRender или других библиотек, то синтаксис для вас точно не будет страшным.
Нам также нужно создать представление, которое будет отвечать за редактирование и добавление новых студентов в нашу базу данных. Для этого добавим еще один html файл в папку appview, который назовем createstudent.html. Ниже показана его реализация.
<div id="createNewStudent">
    <div class="row">
        <form class="form-horizontal" role="form" name="studentForm">
            <input type="hidden" id="studentId" ng-model="student.Id">
            <div class="form-group">
                <label class="control-label col-md-2" for="firstName">First Name:</label>
                <div class="col-md-10">
                    <input id="firstName" class="form-control" ng-model="student.FirstName" />
                </div>
            </div>

            <div class="form-group">
                <label class="control-label col-md-2" for="lastName">Last Name:</label>
                <div class="col-md-10">
                    <input id="lastName" class="form-control" ng-model="student.LastName" />
                </div>
            </div>

            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <button class="btn btn-primary" id="saveStudent" ng-click="save()">Save</button>
                    <button class="btn btn-primary" id="cancelStudent" ng-click="cancel()">Cancel</button>
                </div>
            </div>
        </form>
    </div>
</div>
Теперь нам нужно настроить наше приложение так, чтобы наши только что созданные страницы были доступны через браузер. Для этого скопируем с папки Views файл web.config в нашу папку Views/appview. Именно благодаря этому файлу мы и сможем разрешить обращение к нашим созданным страницам. В этом файле присутствуют следующие строки:
  <appSettings>
    <add key="webpages:Enabled" value="false" />
  </appSettings>

  <system.webServer>
    <handlers>
      <remove name="BlockViewHandler"/>
      <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
    </handlers>
  </system.webServer>
Первая – webpages:Enabled – выставлена в false. Ставим сразу в true. И вторая  убираем обработчик BlockViewHandler, который будет нас редиректить на 404 страницу. После изменений эти строчки приобретут следующий вид:
<appSettings>
  <add key="webpages:Enabled" value="true" />
</appSettings>

<system.webServer>
  <handlers>
    <remove name="BlockViewHandler"/>
  </handlers>
</system.webServer>
Нужно проверить, что после внесенных изменений все заработало. Запускаем для этого наш проект и в строке браузера вводим путь к любой нашей форме.
Как минимум  формы у нас уже загружаются. Теперь переходим к нашей части, связанной непосредственно с AngularJS. Для этого в папке Scripts создадим новую папку, которую назовем app. В этой папке сразу создадим еще одну папку controllers, в которую мы будем складывать наши контроллеры для взаимодействия. Теперь нужно, чтобы эта папочка и все js файлы, которые мы будем в нее складывать, загружалась при запуске нашей страницы. Для этого нам нужно открыть файл BundleConfig и так же, как мы проделывали раньше, добавить в конец функции RegisterBundles следующий код:
bundles.Add(new ScriptBundle("~/bundles/app").IncludeDirectory("~/Scripts/app", "*.js", true));
Теперь в файл _Layout.cshtml по аналогии добавляем следующий код:
@Scripts.Render("~/bundles/app")
Приступим к созданию контроллера, который реализует для нас всю необходимую логику по выборке студентов, добавлению новых; а также редактирование и удаление существующих. Для этого кликнем правой клавишей мыши на папке Controllers и выберем пункт меню Add->Controllers… Затем с доступного списка выбираем Web API 2 Controller with actions, using Entity Framework.
Ищем созданную нами модель и контекст, который мы создали для работы со студентами.
У меня получился следующий код StudentsController, при том что я не написал ни одной строчки кода:
public class StudentsController : ApiController
{
    private StudentContext db = new StudentContext();

    // GET: api/Students
    public IQueryable<Student> GetStudents()
    {
        return db.Students;
    }

    // GET: api/Students/5
    [ResponseType(typeof(Student))]
    public IHttpActionResult GetStudent(int id)
    {
        Student student = db.Students.Find(id);
        if (student == null)
        {
            return NotFound();
        }

        return Ok(student);
    }

    // PUT: api/Students/5
    [ResponseType(typeof(void))]
    public IHttpActionResult PutStudent(int id, Student student)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        if (id != student.Id)
        {
            return BadRequest();
        }

        db.Entry(student).State = EntityState.Modified;

        try
        {
            db.SaveChanges();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!StudentExists(id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return StatusCode(HttpStatusCode.NoContent);
    }

    // POST: api/Students
    [ResponseType(typeof(Student))]
    public IHttpActionResult PostStudent(Student student)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        db.Students.Add(student);
        db.SaveChanges();

        return CreatedAtRoute("DefaultApi", new { id = student.Id }, student);
    }

    // DELETE: api/Students/5
    [ResponseType(typeof(Student))]
    public IHttpActionResult DeleteStudent(int id)
    {
        Student student = db.Students.Find(id);
        if (student == null)
        {
            return NotFound();
        }

        db.Students.Remove(student);
        db.SaveChanges();

        return Ok(student);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            db.Dispose();
        }
        base.Dispose(disposing);
    }

    private bool StudentExists(int id)
    {
        return db.Students.Count(e => e.Id == id) > 0;
    }
}
Теперь нам нужно создать angular компоненты, которые и будут обеспечивать взаимодействие. Я покажу вам простой способ, как это делать, через студию, который требует от вас минимальных затрат энергии. Для начала выберем созданную нами ранее папку app для наших скриптов и кликнем правой клавишей мыши. В контекстном меню выберем добавить новый объект. Затем ищем среди темплейтов с левой стороны Web и пролистываем в самый низ, пока не найдем AngularJs Module, как показано внизу на картинке.
Если у вас по какой-то причине нет шаблонов AngularJs, то создаете обычный JavaScript файл с именем app.js и копируете код, который приведен ниже.
(function () {
    'use strict';

    angular.module('app', [
        // Angular modules
        'ngRoute',
        'ui.bootstrap',
        'ui.bootstrap.tpls'
        // Custom modules

        // 3rd Party Modules
       
    ])
    .config(function ($routeProvider, $locationProvider) {
        console.log("app");
    });
})();
Я сразу добавил в него те методы и свойства, которые нам необходимы. Теперь нам нужно добавить загрузку нашего модуля при старте программы. Для этого открываем наш файл _Layout.cshtml и в html подбавляем вызов нашего AngularJS модуля.
ng-app="app"
И сразу в <head> блок добавим следующую строку:
<base href="@Url.Content("~/")" />
Она нам нужна будет дальше для навигации с помощью AngularJS.
После изменений наша шапка будет иметь следующий вид:
<html ng-app="app">
<head>
    <meta charset="utf-8"/>
    <base href="@Url.Content("~/")" />
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
</head>
Теперь запустим наш проект, чтобы проверить, что наш модуль успешно загружен. У нас в консоли должно быть выведено слово app”.
Как видите, наш Angular модуль успешно был подгружен. Теперь дело за малым. Написать контроллеры, которые будет имплементировать всю необходимую работу по загрузке данных, редактированию удалению и сохранению. Для этого перейдем в папку Scripts\app\controllers и так, как раньше, с контекстного меню добавим новый Angular контроллер, который назовем studentsCtr.
У нас будет создан следующий код:
(function () {
    'use strict';

    angular
        .module('app')
        .controller('studentsCtr', studentsCtr);

    studentsCtr.$inject = ['$location'];

    function studentsCtr($location) {
        /* jshint validthis:true */
        var vm = this;
        vm.title = 'studentsCtr';

        activate();

        function activate() { }
    }
})();
Нам нужно изменить его следующим образом:
(function () {
    'use strict';

    angular
        .module('app')
        .controller('studentsCtr', studentsCtr);

    studentsCtr.$inject = ['$scope', '$http', '$location'];

    function studentsCtr($scope, $http, $location) {
        console.log("studentsCtr");

        var url = 'api/Students';
        $http.get(url)
            .success(function (data) {
                $scope.students = data;
            })
            .error(function (error) {
                console.log(error.message);
            });

        $scope.addNewStudent = function () {
            $location.path('/create');
        };

        $scope.editRow = function (entity) {
            $location.path('/edit/' + entity.Id);
        };

        $scope.removeRow = function (index, entity) {
            $http.delete('api/Students/' + entity.Id)
            .success(function () {
                $scope.students.splice(index, 1);
            })
            .error(function (error) {
                console.log(error.Message);
            });
        };
    }
})();
Немного поговорим о том, что здесь происходит. Мы сделали инъекцию трех параметров.
studentsCtr.$inject = ['$scope', '$http', '$location'];
Пройдемся по каждому из них.
$scope – это наша рабочая лошадка, которая позволяет получать данные с наших страниц. Обратите внимание, например, на метод $scope.removeRow. Давайте перейдем в нашу страницу studentsview.html и найдем этот метод.
<button class="btn btn-danger btn-xs btn-block" style="width: 36px" ng-click="removeRow($index, student)"><span class="glyphicon glyphicon-trash"></span></button>
В этом заключается роль данного $scope.
$http реализует обращения к Web API методам, таким как get, post, put и delete
$location позволяет установить или получить путь, к которому или от которого мы хотим перейти. Выполняет, по сути, роль навигации по нашим страницам.
Так как мы вкратце рассмотрели кратко все параметры, которые использовали, код должен стать более понятным.
Я предпочитаю немного другой подход в написании контроллеров, как показано ниже,
(function () {
    "use strict";

    angular.module("studentsDemo").controller("studentsCtr", [
        '$scope', '$http', '$routeParams', '$location', function ($scope, $http, $routeParams, $location) {

            console.log("studentsCtr");

            var url = 'api/Students';
            $http.get(url)
                .success(function (data) {
                    $scope.students = data;
                })
                .error(function (error) {
                    console.log(error.message);
                });

            $scope.addNewStudent = function () {
                $location.path('/create');
            };

            $scope.editRow = function(entity) {
                $location.path('/edit/' + entity.Id);
            };

            $scope.removeRow = function (index, entity) {
                $http.delete('api/Students/' + entity.Id)
                .success(function () {
                    $scope.students.splice(index, 1);
                })
                .error(function (error) {
                    console.log(error.Message);
                });
            };
        }
    ]);;

})();
не вызывая явно метод $inject, но это дело вкуса. 
Перейдем к созданию остальных контроллеров. По такому же принципу добавим новый контроллер, назвав его createStudentCtr.
(function () {
    'use strict';

    angular
        .module('app')
        .controller('createStudentCtr', createStudentCtr);

    createStudentCtr.$inject = ['$scope', '$http', '$location'];

    function createStudentCtr($scope, $http, $location) {
        console.log("createStudentCtr");

        $scope.student = new Object();

        $scope.save = function () {
            $http.post('api/Students', $scope.student).success(function () {
                $location.path("");
            }).error(function (error) {
                console.log(error.Message);
            });
        };

        $scope.cancel = function () {
            $location.path("");
        };
    }
})();
Осталось еще добавить контроллер для редактирования студентов editStudentCtr.
(function () {
    'use strict';

    angular
        .module('app')
        .controller('editStudentCtr', editStudentCtr);

    editStudentCtr.$inject = ['$scope', '$http', '$routeParams', '$location'];

    function editStudentCtr($scope, $http, $routeParams, $location) {
        console.log("editStudentCtr");
        var id = $routeParams.studentId;

        var url = 'api/Students/' + id;
        $http.get(url)
            .success(function (data) {
                $scope.student = data;
            })
            .error(function (error) {
                console.log(error.message);
            });

        $scope.save = function () {
            $http.put('api/Students/' + id, $scope.student).success(function () {
                $location.path("");
            }).error(function (error) {
                console.log(error.Message);
            });
        };

        $scope.cancel = function () {
            $location.path("");
        };
    }
})();
Здесь добавился параметр $routeParams, который служит для того, чтобы можно было достать параметры, которые были переданы в контроллер. Теперь перейдем в наш модуль app.js и добавим навигацию, чтобы мы могли по конкретному URL связывать нашу страницу с контроллером. Эти изменения приведены ниже.
(function () {
    'use strict';

    angular.module('app', [
        // Angular modules
        'ngRoute',
        'ui.bootstrap',
        'ui.bootstrap.tpls'
        // Custom modules

        // 3rd Party Modules
       
    ])
    .config(function ($routeProvider, $locationProvider) {
        console.log("app");

        $routeProvider
                .when('/', {
                    templateUrl: 'Views/appview/studentsview.html',
                    controller: 'studentsCtr'
                })
                .when('/create', {
                    templateUrl: 'Views/appview/createstudent.html',
                    controller: 'createStudentCtr'
                })
                .when('/edit/:studentId', {
                    templateUrl: 'Views/appview/createstudent.html',
                    controller: 'editStudentCtr'
                })
                .otherwise({
                    redirectTo: '/'
                });

        $locationProvider.html5Mode(true);
    });
})();
Ну и финальный штрих  это зайти в папку Views\Home; из файла Index.cshtml удалить все внутри и заменить на следующий код:
<div class="row">
    <div class="container slide" ng-view="">
    </div>
</div>
Так как мы сделали всю необходимую логику, пришло время запустить наш проект и проверить его работоспособность. У меня главная страница с теми данными, которые я добавил раньше, загрузилась, что можно увидеть на рисунке ниже.
Теперь проверю, можно ли добавить нового студента. Нажимаем на кнопку Create New, вводим в поле First Name имя John и в Last Name вводим Carter, как показано на картинке ниже.
А затем нажимаем на кнопку Save. Проверяем, что в нашу таблицу добавилась новая строка.
Следующим действием проверим редактирование для только что созданной записи. Изменим Last Name с Carter на Skeet и нажмем на кнопку Save.
Проверяем в таблице, что запись изменилась.

Итоги
В этой статье мы разобрали работу с Web API с использованием для front-end разработки AngularJS фреймворк. Этот фреймворк несложный в использовании. Правда, в нем есть свои недостатки. Например, вы можете часами просидеть в отладке, пытаясь понять, почему не работает тот или иной функционал, а вы просто забыли передать нужную директиву в angular модуль. Так я недавно потратил больше часа на то, чтобы понять, почему не работает редактирование ng-grid в angular, а оказалось, что я просто забыл передать в модуль нужный параметр. Надеюсь, что с выходом полноценного Angular 2 (на момент написания статьи была уже бета-версия, которую использовать нормально было нельзя, так как у браузеров не было поддержки ES6), который работает с TypeScript, это уйдет в прошлое. Статья получилась объемной, но по моим ощущениям, не очень сложной.
Исходники к статье: AngularJS Students sample

No comments:

Post a Comment