Tuesday, January 12, 2016

Using Web API with jQuery templates

Здравствуйте, уважаемые читатели. Сегодня мы с вами рассмотрим, как можно писать приложения. используя Web API. Я решил разделить свое обозрение на две отдельные статьи: в первой я постараюсь рассказать о том, как использовать Web API и всю клиентскую логику генерировать на клиенте, например, с помощью библиотеки mustache.js; во второй статье будет показано, как можно использовать Web API более элегантно с Angular JS
Приступим к нашей реализации, внося небольшие комментарии по ходу работы. Будем все мы писать на ASP.NET MVC 5, так как шестой – хоть и релиз-кандидат, но использовать его пока рано. Начнем с создания нового проекта, который назовем “StudentWebApiSample”, задача которого  простая регистрация студентов. Никакой сложной структуры мы реализовывать не будем. Вокруг которой таблицы и будем строить всю нашу логику. Напишем все сразу, чтобы можно было расширять это все для более крупных проектов.
Затем выберем шаблон Web API, как показано на рисунке ниже.
Постараемся все рассмотреть быстро, чтобы по минимуму уделять времени на ненужные детали. После того, как ваш проект будет создан, вы можете посмотреть, как выглядит контроллер ApiController, если вам еще не приходилось до этого работать с Web API. По умолчанию создается контроллер ValuesController, который представляет собой тестовую реализацию. Вы можете с ней ознакомиться, по желанию, и сразу удалить. Как минимум, для нашего приложения она не понадобится.
Следующим делом нам нужно создать класс 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>
После того как мы проделали все действия выше, можно добавить новый Web API контроллер, который сгенерирует нам всю необходимую логику. Для этого выберем папку Controllers, кликнем правой кнопкой мыши и выберем Add-> Controller…, как показано ниже.
Затем в списке выбираем Web API 2 Controller with actions, using Entity Framework
Затем выберем нашу модель, для которой мы хотим сгенерировать контроллер, а также контекст, в котором эта модель присутствует.
Если хотите, можете выбрать для себя галочку использования асинхронного контроллера, тогда ваш котроллер будет построен на тасках (TPL). Наш код будет работать как с асинхронным контролером, так и на синхронном. Поэтому для теста я эту галочку не ставил. (По причине того, что в реальных примерах, с которыми я сталкивался в основном уклон был на синхронные контроллеры).
Ниже представлен контроллер, который был сгенерирован студией.
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;
    }
}
Как видите, не написав ни строчки кода для контролера, мы уже имеем вполне рабочий проект. Зачастую у вас возможны разные прослойки, например, кастомный UnitOfWork поверх DbContext. Даже в таком случае Scaffolding здорово упростит вам жизнь. потому что создаст вам скелет, который вы сможете адаптировать под свои нужды.
Теперь пришла пора реализовать логику, необходимую для отображения студентов и добавления новых. Чтобы не писать жуткую логику добавления студентов в таблицу, подобно той, которая приведена ниже,
$("#studentTable").append("<tr><td>" + value.Id + "</td><td>" + value.FirstName + "</td><td>" +
    value.LastName + "</td><td>" + "<a>Edit</a>" + "</td><td>" + "<a>Delete</a>" + "</td></tr>");
нам понадобится библиотека для работы с темплейтами, которая позволит нам создавать наш HTML код, и заполнять его сразу реальными данными. Для этого я воспользовался библиотекой mustache.js, которую можно установить через NuGet Package Manager.
Если вам нужна более свежая версия, то вы можете поставить ее через npm. как описано об этом на github.com mustache.js.
Нам же достаточно того, что есть в поставке. Для более сложных вещей, конечно же, вам будет mustache.js маловато, но есть очень большое количество альтернатив. Одну из таких альтернатив я описал в статье "Generate client views with jQuery templates in ASP.NET MVC. RsRender Engine". В этой же статье есть перечень популярных альтернатив, которые вы можете использовать.
После того как мы поставили через NuGet Package Manager себе mustache, нужно добавить загрузку его в наше приложение. Для этого перейдем в класс BundleConfig и в методе RegisterBundles добавим в самый конец следующий код:
bundles.Add(new ScriptBundle("~/bundles/custom").Include(
                    "~/Scripts/mustache.js"));
Затем нам нужно добавить загрузки только что созданного бандла в нашу страницу. Для этого ищем в папке Shared файл _Layout.cshtml, в котором в самом конце добавлены следующие строки:
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
Сразу после них добавляем загрузку нашего бандла.
@Scripts.Render("~/bundles/custom")
Теперь пришло время правки нашей главной страницы. Для этого откроем нашу страницу Index.cshtml и все удалим с нее. Затем добавим простую таблицу, как показано ниже.
<div id="mainWindow">
    <div class="row">
        <button id="addNewStudent" class="btn btn-primary">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></tbody>
        </table>
    </div>
</div>
И следующий JavaScript код, который будет представлять собой шаблон для нашей таблицы.
<script id="studentRowTemplate" type="text/html">
    <tr>
        <td>
            {{ Id}}
        </td>
        <td>
            {{ FirstName }}
        </td>
        <td>
            {{ LastName }}
        </td>
        <td>
            <button class="btn btn-danger btn-xs btn-block" style="width: 36px" id="editButton" onclick="editRow({{Id}}) "><span class="glyphicon glyphicon-edit"></span></button>
        </td>
        <td>
            <button class="btn btn-danger btn-xs btn-block" style="width: 36px" id="removeButton" onclick="removeRow({{Id}}) "><span class="glyphicon glyphicon-trash"></span></button>
        </td>
    </tr>
</script>
Студия, правда, может ругаться на такую реализацию методов, но у вас реально будет сложность с подпиской на события через jQuery, потому что на момент создания страницы у вас не будет создано контролов. То есть, следующий код работать не будет.
<script src="~/Scripts/jquery-1.10.2.js"></script>
<script type="text/javascript">

    $(function () {

        $("#removeButton").on('click', function (e) {
            e.preventDefault();
            console.log("removeButton click");      
        });
    });
</script>
Основная причина – это то, что мы используем темплейты, которые создают наши элементы динамически. Поэтому здесь будет работать event-delegation. Описание самой проблемы вы можете найти здесь jQuery .click() won't fire from an <a> within a mustache.js template.
К реализации этих обработчиков мы перейдем немного позже. Нам нужен код, который загрузит наших студентов в таблицу. Для этого перепишем наш код выше следующим образом:
<script src="~/Scripts/jquery-1.10.2.js"></script>
<script type="text/javascript">

    $(function () {

        $.getJSON(
             "api/Students",
             function (data) {
                 var template = $('#studentRowTemplate').html();

                 $.each(data,
                     function (index, value) {
                         var info = Mustache.render(template, value);
                         $('#studentTable >tbody').append(info);
                     }
                 );
             }
         );
    });
</script>
Запустим наше приложение и убедимся в том, что метод api/GetStudents успешно вызывается.
Наверное, было бы лучше для тестового примера сделать начальные данные для запуска, чтобы убедиться, что начальная часть у нас работает. Для этого перейдем в папку App_Start и добавим новый класс SampleData, как показано ниже.
using System.Linq;
using StudentWebApiSample.DAL;
using StudentWebApiSample.Models;

namespace StudentWebApiSample
{
    public static class SampleData
    {
        public static void Initialize()
        {
            using (var context = new StudentContext())
            {
                if (!context.Students.Any())
                {
                    context.Students.Add(
                        new Student {LastName = "Austen", FirstName = "Jane"});
                    context.Students.Add(
                        new Student { LastName = "Dickens", FirstName = "Charles"});
                    context.Students.Add(
                        new Student { LastName = "Cervantes", FirstName = "Miguel"});

                    context.SaveChanges();
                }
            }
        }
    }
}
После того, как мы добавили наш класс, открываем файл Global.asax и вконце добавим следующую строку кода:
SampleData.Initialize();
Полный код выглядит следующим образом:
public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        GlobalConfiguration.Configure(WebApiConfig.Register);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        SampleData.Initialize();
    }
}
После того, как мы внесли все необходимые данные, запустим наш сайт и проверим, что у нас все работает.
После того, как мы добавили отображение наших студентов (которые являются авторами популярных книг), следующим делом добавим удаление студентов по нажатию на кнопкуУ нас уже добавлен обработчик на эту кнопку. Осталось только ее реализовать. Выглядеть эта функция будет следующим образом:
function removeRow(id) {
    $.ajax({
        url: "api/Students/" + id,
        type: 'DELETE',
        success: function () {
            window.location.href = "";
        },
        fail: function (data) {
            console.log(data);
        }
    });
}
Полный код использования приведен ниже.
<script type="text/javascript">

    function removeRow(id) {
        $.ajax({
            url: "api/Students/" + id,
            type: 'DELETE',
            success: function () {
                window.location.href = "";
            },
            fail: function (data) {
                console.log(data);
            }
        });
    }

    $(function () {

        $.getJSON(
             "api/Students",
             function (data) {
                 var template = $('#studentRowTemplate').html();

                 $.each(data,
                     function (index, value) {
                         var info = Mustache.render(template, value);
                         $('#studentTable >tbody').append(info);
                     }
                 );
             }
         );
    });
</script>
Теперь приступим к созданию окна, которое будет у нас служить для добавления новых студентов и редактирования старых. Для этого в наш html код добавим следующие строки:
<div id="createNewStudent">
    <div class="row">
        <form class="form-horizontal" role="form" name="studentForm">
            <div id="studentInformation">

            </div>

            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <button class="btn btn-primary" id="saveStudent">Save</button>
                    <button class="btn btn-primary" id="cancelStudent">Cancel</button>
                </div>
            </div>
        </form>
    </div>
</div>

<script id="editStudentTemplate" type="text/html">
    <input type="hidden" id="studentId" value="{{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" value="{{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" value="{{LastName}}" />
        </div>
    </div>
</script>
Здесь мы создали новый div для отображения окна по созданию и редактированию новых студентов. А ниже добавлен темлейт, который отображает позиционирование контролов и binding их с данными. Если мы сейчас запустим наш пример, то увидим два окна. С таблицей для отображения и для добавления/редактирования. Это не то, чего мы хотим добиться.
Поэтом в наш JavaScript код, в котором у нас реализована вся логика, добавим следующие строки:
$("#mainWindow").show();
$("#createNewStudent").hide();
Теперь необходимо добавить реализацию на кнопку Create New для регистрации новых студентов.
$(function() {
    $("#addNewStudent").on('click', function () {
        console.log("addNewStudent click");
        var studentTemplate = $('#editStudentTemplate').html();
        var info = Mustache.render(studentTemplate, null);

        $("#mainWindow").hide();
        $("#studentInformation").html(info);
        $("#createNewStudent").show();
    });
});
Здесь мы добавили подписку на кнопку addNewStudent через Id, а также сгенерировали новый темлпейт для добавления информации по студентам. Теперь у нас по нажатию на кнопку будет появляться другая форма, которая будет закрываться по нажатию на кнопку Save или Cancel.
Теперь давайте реализуем обработчик для кнопки Save. Ниже представлена реализация данного обработчика, которую я сделал как для редактирования, так и для добавления новых данных в базу данных.
$("#saveStudent").on('click', function (e) {
    e.preventDefault();
    var student = new Object();
    student.firstName = $('#firstName').val();
    student.lastName = $('#lastName').val();
    var id = parseInt($('#studentId').val());

    //insert or update
    if (isNaN(id)) {
        $.post('api/Students', student, function (data) {
            console.log(data);
            window.location.href = "";
        });
    }
    else {
        student.id = id;
        $.ajax({
            url: 'api/Students/' + student.id,
            type: 'PUT',
            data: student,
            success: function (response) {
                window.location.href = "";
            }
        });
    }
});
Здесь все просто. Создаем объект на JavaScript, который будет мапиться на наш класс Student. Запустим и посмотрим, что у нас добавляются новые значения в таблицу. Попробуем добавить нового студента по имени John Carter.
Проверяем, что новый студент успешно добавлен.
Теперь осталось добавить редактирование для текущих студентов, иначе наше приложение будет неполным. Для этого или после метода removeRow, или перед ним нужно добавить следующий метод:
function editRow(id) {
    $.getJSON("api/Students/" + id, function (data) {
        var studentTemplate = $('#editStudentTemplate').html();
        var info = Mustache.render(studentTemplate, data);
        $("#mainWindow").hide();
        $("#studentInformation").html(info);
        $("#createNewStudent").show();
    });
}
Здесь мы выбираем студента с нашего сервера по id, а затем отправляем его данный в сгенерированный через mustache темплейт. Перезапустим наш проект и поменяем нашего John Carter на John Lennon. Теперь кнопка редактирования подгрузит нам всю информацию о John Carter.
Нам останется ее лишь немного изменить.
Проверим, что у нас все обновилось.
Ниже для проверки приведу весь исходный код.
<div id="mainWindow">
    <div class="row">
        <button id="addNewStudent" class="btn btn-primary">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>

            </tbody>
        </table>
    </div>
</div>

<div id="createNewStudent">
    <div class="row">
        <form class="form-horizontal" role="form" name="studentForm">
            <div id="studentInformation">

            </div>

            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <button class="btn btn-primary" id="saveStudent">Save</button>
                    <button class="btn btn-primary" id="cancelStudent">Cancel</button>
                </div>
            </div>
        </form>
    </div>
</div>

<script id="studentRowTemplate" type="text/html">
    <tr>
        <td>
            {{ Id}}
        </td>
        <td>
            {{ FirstName }}
        </td>
        <td>
            {{ LastName }}
        </td>
        <td>
            <button class="btn btn-danger btn-xs btn-block" style="width: 36px" onclick="editRow({{Id}})"><span class="glyphicon glyphicon-edit" ></span></button>
        </td>
        <td>
            <button class="btn btn-danger btn-xs btn-block" style="width: 36px" onclick="removeRow({{Id}})"><span class="glyphicon glyphicon-trash"></span></button>
        </td>
    </tr>
</script>

<script id="editStudentTemplate" type="text/html">
    <input type="hidden" id="studentId" value="{{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" value="{{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" value="{{LastName}}" />
        </div>
    </div>
</script>

<script src="~/Scripts/jquery-1.10.2.js"></script>
<script type="text/javascript">
    $("#mainWindow").show();
    $("#createNewStudent").hide();

    function editRow(id) {      

        $.getJSON("api/Students/" + id, function (data) {
            var studentTemplate = $('#editStudentTemplate').html();
            var info = Mustache.render(studentTemplate, data);
            $("#mainWindow").hide();
            $("#studentInformation").html(info);
            $("#createNewStudent").show();
        });
    }

    function removeRow(id) {
        $.ajax({
            url: "api/Students/" + id,
            type: 'DELETE',
            success: function (result) {
                window.location.href = "";
            },
            fail: function (data) {
                console.log(data);
            }
        });
    }

    $(function () {
        debugger;
        $.getJSON(
            "api/Students",
            function (data) {
                var template = $('#studentRowTemplate').html();

                $.each(data,
                    function (index, value) {
                        var info = Mustache.render(template, value);
                        $('#studentTable >tbody').append(info);
                        //$("#studentTable").append("<tr><td>" + value.Id + "</td><td>" + value.FirstName + "</td><td>" +
                        //    value.LastName + "</td><td>" + "<a>Edit</a>" + "</td><td>" + "<a>Delete</a>" + "</td></tr>");
                    }
                );
            }
        );

        $("#saveStudent").on('click', function (e) {
            e.preventDefault();
            var student = new Object();
            student.firstName = $('#firstName').val();
            student.lastName = $('#lastName').val();
            var id = parseInt($('#studentId').val());

            //insert or update
            if (isNaN(id)) {
                $.post('api/Students', student, function (data) {
                    console.log(data);
                    window.location.href = "";
                });
            }
            else {
                student.id = id;
                $.ajax({
                    url: 'api/Students/' + student.id,
                    type: 'PUT',
                    data: student,
                    success: function (response) {
                        window.location.href = "";
                    }
                });
            }
        });

        $("#cancelStudent").on('click', function () {
            window.location.href = "";
        });

        $("#addNewStudent").on('click', function () {
            console.log("addNewStudent click");
            var studentTemplate = $('#editStudentTemplate').html();
            var info = Mustache.render(studentTemplate, null);

            $("#mainWindow").hide();
            $("#studentInformation").html(info);
            $("#createNewStudent").show();
        });
    });
</script>

Итоги
Давайте подведем итоги того, что же у нас получилось. Мы с вами рассмотрели, как использовать Web API на клиенте. Как минимум, вы поняли, что использовать Web API на стороне клиенте с генерацией темплйтов – задача не из простых. Разные темплейт генераторы, такие как mustache или JsRender, не особо спасают ситуацию. На помощь и выручку нужно брать все-таки более высокоуровневые UI фреймворки, как AngularJS или KnockoutJS, потому что как вы выйдите с MVC работать проще, чем с Web API. В следующей статье мы с вами рассмотрим, как использовать этот же подход с AngularJS, чтобы убедиться, что все может быть не настолько страшно.

Исходники: Student Sample

No comments:

Post a Comment