Tuesday, December 8, 2015

Generate client views with jQuery templates in ASP.NET MVC. RsRender Engine

Здравствуйте, уважаемые читатели. Сегодня мы с вами продолжим погружение в мир web-разработки программного обеспечения. А рассмотрим мы интересную и актуальную тему, которая редко затрагивается в книгах по ASP.NET MVC. Сегодня мы разберем пример того, как можно использовать генерацию вашего кода отображения со стороны клиента. Итак, в чем это дает преимущество – как минимум в объемах, пересылаемых данных, так как большинство движков работает с json-форматом данных. Мы также проанализируем работу с jQuery Templates. Команда jQuery написала плагин, который так и называется: jQuery Templates. Но он так и не вышел с беты, и над ним прекратили работу, да и сами разработчики предлагаю посмотреть в сторону JsRender, что и предлагаю сделать. Если перейти на сайт JsRender, то можно увидеть, что JsRender предлагает интересную связку с JsViews, которая позволит работать с вашими данными, используя паттерн MVVM и MVP. На момент написания статьи существовало много движков, о части которых я даже никогда не слышал, но некоторые из них мы можем перечислить. Это knockoutjs, JsViews, Kendo UI Templates, Angular, Pure и другие. Например, можете здесь посмотреть список из 10 таких движков: 10 JavaScript and jQuery Templates Engines.  
Сегодняшняя задача следующая: сделать выбор списка машин и по запросу с сервера загружать детальную информацию по них. Не так давно я писал подобный пример в статье "Использование Autocomplete для своих приложений ASP.NET MVC5", так что сегодня будет что-то подобное. Как минимум, мы переделаем тот проект к сносному виду и будем использовать JsRender для генерации UI. Классический пример работы генерации представления в ASP.NET MVC показан на рисунке ниже.
Когда на ринг выходит JsRender, в идеале диаграмма должна иметь следующий вид:
Но так как мы живем не в идеальном мире, да и мне нравится то, как Razor генерирует нам представления, поэтому подход будет следующим:
Эта диаграмма нуждается в пояснении. Дело в том, что проще сгенерировать пустую страницу вначале, которая будет возвращена через ViewResult, а затем все данные получать через ajax запросы в формате json. Такой подход используют разработчики, которые на клиентской части используют knockout.js или angular js.  Мы же с вами ничем не хуже, да и сами, наверное, используете такой подход, поэтому предлагаю не отходить от традиций. Тем более, если традиции можно улучшить или скомбинировать с другими. 
Достаточно теории; приступаем к практике. Первым делом создадим новый проект ASP.NET Web Application, который назовем “JsrenderSample”.
Затем выберем готовый шаблон MVC, как показано на рисунке ниже.
Суть нашего проекта будет заключаться в следующем. На нашу html страницу мы будем выводить информацию о самых дорогих автомобилях и их характеристики. Для того чтобы хранить где-то информацию об автомобилях, нам понадобится соответственная модель данных. Для этого зайдем в папку Models и добавим туда новый класс Car.
public class Car
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
    public int Speed { get; set; }
    public double Acceleration { get; set; }
    public int Power { get; set; }
    public double Displacement { get; set; }
    public int Weight { get; set; }
}
Теперь нам нужно откуда-то брать эту информацию. У меня она хранится в файле cars.txt, и вы сможете скачать этот файл себе по ссылке в конце статьи. Но для того чтобы ваш пример работал, вы можете создать свой файл с информации, которая приведена ниже (ее просто нужно скопировать и поместить в текстовый файл), и положить этот файл в папку App_Data, так как в эту папку у вас точно будет доступ.
car_id, car_name, cost, top_speed,0_100_kph,power_bhp,displacement, weight,
1,"Ferrari 250 GTO", 52000000,280,6.1,302,3,1100,
2,"Ferrari   250 Testa Rossa", 16400000,259,6,300,3,800,
3,"Jaguar XJ13", 15000000,274,3.4,509,5,998,
4,"Mercedes-Benz SLR McLaren 999 Red Gold Dream Ueli Anliker", 10000000,340,3,999,5.4,1800,
5,"Ferrari 330 P4", 9000000,338,5,450,4,875,
6,"Maybach Exelero", 8000000,351,4.4,700,5.9,2600,
7,"Rolls-Royce Hyperion Pininfarina", 6000000,250,5.6,460,6.7,2650,
Просто скопируйте в отдельный файл информацию выше, и пример у вас будет работать. Затем перейдем в наш контроллер HomeController и добавим приватный метод GetAllCars(), который достанет нам все необходимые данные. В этот метод мы добавим action filter OutputCache, для того чтобы кешировать получение списка GetAllCars. Так как это список изменяться особо не будет, мы добавили кеширование на 5 минут.
[OutputCache(Duration = 300)]
private List<Car> GetAllCars()
{
    var path = HttpContext.Server.MapPath("~/App_Data/cars.txt");
    var cars = System.IO.File.ReadAllLines(path)
        .Skip(1)
        .Select(x => x.Split(new[] { ',' }, StringSplitOptions.None))
        .Select(item =>
        {
            return new Car()
            {
                Id = int.Parse(item[0]),
                Name = item[1].Trim(new[] { '"' }),
                Price = double.Parse(item[2]),
                Speed = int.Parse(item[3]),
                Acceleration = double.Parse(item[4]),
                Power = int.Parse(item[5]),
                Displacement = double.Parse(item[6]),
                Weight = int.Parse(item[7])
            };
        })
        .ToList();

    return cars;
}
Затем добавим метод, который назовем Autocomplete, чтобы он подгружал не все данные сразу, а только часть.
public JsonResult Autocomplete(string term)
{
    var cars = GetAllCars();
    var result = cars.Where(s => s.Name.ToLower().Contains
                    (term.ToLower())).Select(w => w.Name).ToList();
    return Json(result, JsonRequestBehavior.AllowGet);
}
Приступим сейчас к реализации нашего примера. Для этого откроем наше представление Index.cshtml и подправим его следующим образом:
@{
    ViewBag.Title = "Home Page";
}

<link rel="stylesheet" href="http://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css">
<script src="http://code.jquery.com/jquery-1.10.2.js"></script>
<script src="http://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>

<div class="form-control-static">
    <div class="form-group">
        <label for="autocompleteCar">Enter car name: </label>
        <input id="autocompleteCar" class="form-control">
    </div>
    <div class="form-group">
        <input type="submit" id="submit" class="btn btn-info"/>
    </div>
</div>

<script type="text/javascript">
    $("#autocompleteCar").autocomplete({
        source: '@Url.Action("Autocomplete", "Home")',
        minLength: 1,
        width: 200
    });
</script>
Для заполнения текста в контроле мы использовали плагин jQuery Autocomplete, но это не важно в нашем случае. Просто скопируйте код и запустите пример, чтобы убедиться, что он сейчас работает. Ниже на рисунке показано, как это выглядит у меня.
Самое время реализовать выборку деталей по каждой машине и показать их на клиенте по нажатию на кнопку submit. Для начала нам нужно загрузить jsrender.js скрипты. Сделать это можно отсюда: JsRender, JsViews and JsObservable Downloads вручную, либо использовать HotGlue.Template.JsRender, который работает с движком JsRender, потому что на момент подготовки статьи написание NuGet пакета только планировалось Create a NuGet package #211. Загружаем себе jsrender c сайта и добавляем его в нашу папку Scripts.
Следующим делом поправим немного BundleConfig.cs, чтобы сделать загрузку при старте нашего приложения. В самый конец функции RegisterBundles добавляем следующую строку:
bundles.Add(new ScriptBundle("~/bundles/custom").Include(
                                "~/Scripts/jsRender.js"));
Переходим в папку Shared и открываем на редактирование файл _Layout.cshtml. Ищем в самом конце секцию
@RenderSection("scripts", required: false)
И добавляем перед ней рендеринг нашего бандла.
@Scripts.Render("~/bundles/custom")
У меня это выглядит следующим образом:
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@Scripts.Render("~/bundles/custom")
@RenderSection("scripts", required: false)
Затем нужно реализовать в контроллере HomeController функцию, которая будет доставать детальную информацию по конкретной машине. Назовем эту функцию GetCarDetails.
public JsonResult GetCarDetails(string carName)
{
    var cars = GetAllCars();
    var car = cars.FirstOrDefault(x => x.Name == carName);
    return Json(car, JsonRequestBehavior.AllowGet);
}
Для полной проверки приведу весь листинг класса HomeController.
public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public JsonResult Autocomplete(string term)
    {
        var cars = GetAllCars();
        var result = cars.Where(s => s.Name.ToLower().Contains
                        (term.ToLower())).Select(w => w.Name).ToList();
        return Json(result, JsonRequestBehavior.AllowGet);
    }

    [OutputCache(Duration = 300)]
    private List<Car> GetAllCars()
    {
        var path = HttpContext.Server.MapPath("~/App_Data/cars.txt");
        var cars = System.IO.File.ReadAllLines(path)
            .Skip(1)
            .Select(x => x.Split(new[] { ',' }, StringSplitOptions.None))
            .Select(item =>
            {
                return new Car()
                {
                    Id = int.Parse(item[0]),
                    Name = item[1].Trim(new[] { '"' }),
                    Price = double.Parse(item[2]),
                    Speed = int.Parse(item[3]),
                    Acceleration = double.Parse(item[4]),
                    Power = int.Parse(item[5]),
                    Displacement = double.Parse(item[6]),
                    Weight = int.Parse(item[7])
                };
            })
            .ToList();

        return cars;
    }

    public JsonResult GetCarDetails(string carName)
    {
        var cars = GetAllCars();
        var car = cars.FirstOrDefault(x => x.Name == carName);
        return Json(car, JsonRequestBehavior.AllowGet);
    }

    public ActionResult About()
    {
        ViewBag.Message = "Your application description page.";

        return View();
    }

    public ActionResult Contact()
    {
        ViewBag.Message = "Your contact page.";

        return View();
    }
}
Продолжим правку файла Index.cshtml с папки Views\Home. Добавим новый div, который будет отображать наш результат.
<div class="form-group" id="result-box">
</div>
Затем нужно сгенерировать темплейт, который будет отображать всю необходимую информацию. Для этого сразу после нашего предыдущего блока div добавим следующий шаблон:
<script id="template" type="text/x-jsrender">
    <label for="Name">Name: </label>
    <label id="Name" class="form-control">{{:Name}}</label>
    <label for="Price">Price: </label>
    <label id="Price" class="form-control">{{:Price}}</label>
    <label for="Speed">Price: </label>
    <label id="Speed" class="form-control">{{:Speed}}</label>
    <label for="Acceleration">Acceleration: </label>
    <label id="Acceleration" class="form-control">{{:Acceleration}}</label>
    <label for="Power">Power: </label>
    <label id="Power" class="form-control">{{:Power}}</label>
    <label for="Displacement">Displacement: </label>
    <label id="Displacement" class="form-control">{{:Displacement}}</label>
    <label for="Weight">Weight: </label>
    <label id="Weight" class="form-control">{{:Weight}}</label>
</script>
Генерацию шаблонов мы можем разнести по разным файлам, что очень удобно для больших приложений. Теперь время реализовать обработчик кнопки submit, который будет выполнять ajax запрос, который мы ставим для нашего движка. Вот так все просто. Ниже приведена реализация данного обработчика.
$("#submit").click(function (evt) {
    evt.preventDefault();

    $.ajax({
        url: '@Url.Action("GetCarDetails", "Home")',
        type: "POST",
        dataType: "json",
        data: { carName: $("#autocompleteCar").val() },
        success: function (data) {
            $("#result-box").html(
                $("#template").render(data)
            );
        },
        error: function (xhr) {
            alert(xhr.responseText);
        }
    });
});
Теперь для проверки рассмотрим, как выглядит код всего файла Index.cshtml, чтобы убедиться, что мы ничего не пропустили.
@{
    ViewBag.Title = "Home Page";
}

<link rel="stylesheet" href="http://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css">
<script src="http://code.jquery.com/jquery-1.10.2.js"></script>
<script src="http://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>

<div class="form-control-static">
    <div class="form-group">
        <label for="autocompleteCar">Enter car name: </label>
        <input id="autocompleteCar" class="form-control">
    </div>
    <div class="form-group">
        <input type="submit" id="submit" class="btn btn-info"/>
    </div>
</div>

<div class="form-group" id="result-box">
</div>

<script id="template" type="text/x-jsrender">
    <label for="Name">Name: </label>
    <label id="Name" class="form-control">{{:Name}}</label>
    <label for="Price">Price: </label>
    <label id="Price" class="form-control">{{:Price}}</label>
    <label for="Speed">Price: </label>
    <label id="Speed" class="form-control">{{:Speed}}</label>
    <label for="Acceleration">Acceleration: </label>
    <label id="Acceleration" class="form-control">{{:Acceleration}}</label>
    <label for="Power">Power: </label>
    <label id="Power" class="form-control">{{:Power}}</label>
    <label for="Displacement">Displacement: </label>
    <label id="Displacement" class="form-control">{{:Displacement}}</label>
    <label for="Weight">Weight: </label>
    <label id="Weight" class="form-control">{{:Weight}}</label>
</script>

<script type="text/javascript">
    $("#autocompleteCar").autocomplete({
        source: '@Url.Action("Autocomplete", "Home")',
        minLength: 1,
        width: 200
    });

    $("#submit").click(function (evt) {
        evt.preventDefault();

        $.ajax({
            url: '@Url.Action("GetCarDetails", "Home")',
            type: "POST",
            dataType: "json",
            data: { carName: $("#autocompleteCar").val() },
            success: function (data) {
                $("#result-box").html(
                  $("#template").render(data)
                );
            },
            error: function (xhr) {
                alert(xhr.responseText);
            }
        });
    });
</script>
Запустим наш проект и проверим, что данные возвращаются успешно.
Как видите, все неплохо получилось. Теперь немного допилим наш вариант и все-таки выведем весь список машин из стоимости в какой-либо div. Для начала добавим новый GetCars в наш контроллер.
public JsonResult GetCars()
{
    var cars = GetAllCars();
    return Json(cars, JsonRequestBehavior.AllowGet);
}
Теперь перейдем к реализации представления. Как и в предыдущем примере, добавим сначала новый блок div и шаблон для отображения.
<div id="carList">
</div>

<script id="listCarTemplate" type="text/x-jsrender">
    <div>
        {{:#index+1}}: <b>{{>Name}}</b> ({{>Price}} $)
    </div>
</script>
Теперь осталось добавить ajax метод, который загрузит нам список машин.
$.ajax({
    url: '@Url.Action("GetCars", "Home")',
    type: "GET",
    dataType: "json",
    success: function (data) {
        $("#carList").html(
            $("#listCarTemplate").render(data)
        );
    },
    error: function (xhr) {
        alert(xhr.responseText);
    }
});
Чуть позже мы разберем весь синтаксис более детально. Перепроверяем наш код, чтобы убедиться в том, что мы ничего не пропустили.
@{
    ViewBag.Title = "Home Page";
}

<link rel="stylesheet" href="http://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css">
<script src="http://code.jquery.com/jquery-1.10.2.js"></script>
<script src="http://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>

<div class="form-control-static">
    <div class="form-group">
        <label for="autocompleteCar">Enter car name: </label>
        <input id="autocompleteCar" class="form-control">
    </div>
    <div class="form-group">
        <input type="submit" id="submit" class="btn btn-info"/>
    </div>
</div>

<div class="form-group" id="result-box">
</div>

<script id="template" type="text/x-jsrender">
    <label for="Name">Name: </label>
    <label id="Name" class="form-control">{{:Name}}</label>
    <label for="Price">Price: </label>
    <label id="Price" class="form-control">{{:Price}}</label>
    <label for="Speed">Price: </label>
    <label id="Speed" class="form-control">{{:Speed}}</label>
    <label for="Acceleration">Acceleration: </label>
    <label id="Acceleration" class="form-control">{{:Acceleration}}</label>
    <label for="Power">Power: </label>
    <label id="Power" class="form-control">{{:Power}}</label>
    <label for="Displacement">Displacement: </label>
    <label id="Displacement" class="form-control">{{:Displacement}}</label>
    <label for="Weight">Weight: </label>
    <label id="Weight" class="form-control">{{:Weight}}</label>
</script>

<div id="carList">
</div>

<script id="listCarTemplate" type="text/x-jsrender">
    <div>
        {{:#index+1}}: <b>{{>Name}}</b> ({{>Price}} $)
    </div>
</script>

<script type="text/javascript">
    $("#autocompleteCar").autocomplete({
        source: '@Url.Action("Autocomplete", "Home")',
        minLength: 1,
        width: 200
    });

    $.ajax({
        url: '@Url.Action("GetCars", "Home")',
        type: "GET",
        dataType: "json",
        success: function (data) {
            $("#carList").html(
              $("#listCarTemplate").render(data)
            );
        },
        error: function (xhr) {
            alert(xhr.responseText);
        }
    });

    $("#submit").click(function (evt) {
        evt.preventDefault();

        $.ajax({
            url: '@Url.Action("GetCarDetails", "Home")',
            type: "POST",
            dataType: "json",
            data: { carName: $("#autocompleteCar").val() },
            success: function (data) {
                $("#result-box").html(
                  $("#template").render(data)
                );
            },
            error: function (xhr) {
                alert(xhr.responseText);
            }
        });
    });
</script>
Теперь запустим наш пример, чтобы проверить, что все работает.
Получили в итоге симпатичный список машин и цену каждой машины. Теперь детальнее рассмотрим на то, что за шаблон у нас генерируется.
  1. Шаблон id=listCarTemplate будет просто интерпретирован как текст, и браузер его обрабатывать не будет.
  2. {{:#index+1}} позволяет обратиться к свойствам объекта. Так как мы указали решетку, то мы получим индекс списка, который начинается с 0, поэтому мы добавили + 1. Если не использовать #, то мы бы просто искали переменную с именем index, как в предыдущем нашем примере.
  3. {{>Name}} и {{>Price}} позволяет обратиться к свойствам Name и Price нашего класса Car, а треугольная скобка вначале говорит о том, что строка будет кодированная, то есть, будут видны теги.
Что такое div и что делает функция render, думаю, не стоит объяснять. Просто если вы не знаете, зачем нужен div, то на процентов 90 вы ничего со статьи не поняли. И тут уже нужно возвращаться к основам html, что не является идеей данной статьи.
Если вы работали с angular js, то такой синтаксис не должен у вас вызывать никакого дискомфорта, тем более, что он подобен angular js.
<div class="row">
    <div class="col-lg-12">
        <h1 class="page-header">
            {{name}}({{surname}})
        </h1>
    </div>
</div>
Синтаксис позволяет использовать цикл for,
{{for cars}}
    <div>{{:Name}}</div>
{{/for}}
операторы if/else, операторы сравнения и многое другое. Не вижу особого смысла приводить полную документацию, так как вы сами с ней можете ознакомиться JsRender Quickstart. Это не займет много времени: там одна страничка текста. Чтобы начать использовать все, что там описано, достаточно полчаса времени.
Итоги
Основной идеей этой статьи было показать, как работать с генерацией темплейтов на стороне клиента, передавая туда json результат, и я думаю, мы с вами справились с поставленной задачей. Если вы работаете или работали с angular или knockout, то подобный синтаксис для вас не в новинку. Он часто реализуется по-другому и имеет свою структуру, но суть его остается той же. Спасибо за внимание. Буду рад ответить на ваши вопросы в комментариях к статье.
Статьи, которые пригодятся и будут интересны вам для изучения данной области:


Исходники к статье: JsRender sample

No comments:

Post a Comment