ASP.NET MVC: MVVM на HTML или использование knockout при создании сайта (часть 1 из 2).
Сайтостроение | создано: 14.09.2012 | опубликовано: 14.09.2012 | обновлено: 13.01.2024 | просмотров: 14286 | всего комментариев: 7
На простом примере постараюсь объяснить как с помощью JavaScript-фреймворка Knockout можно применить паттерн MVVM на HTML.
Что такое Knockout?
Если говорить о Knockout, то я не буду вдаваться в подробности, а просто перечислю четыре основных принципа фреймворка описанные на официальном сайте:
- Декларативное связывание (declarative binding);
- Автоматическое обновление интерфейса пользователя при изменении свойств объекта (ов) (Automatic UI Update). По такому же принципу работает INotifyPropertyChanged в Silverlight и WPF;
- Отслеживание зависимостей (Dependency tracking);
- Шаблоны (Templating).
Каждый из перечисленных принципов так или иначе (а в некоторых случаях, даже несколько раз) были представлены на суд общественности в той или иной форме. Существует огромное множество контролов, фреймворков и надстроек в сети, но когда появился knockout (далее "ko" или “нокаут”), который объединил всё перечисленное в одном, что называется флаконе, разработка на HTML 5 стала доставлять истинное удовольствие.
Задача для примера с использованием Knockout
Когда-то, совсем давно я писал статью о том, как сделать форму обратной связи на AJAX. В этой статье сделаем тоже самое, но только с использованием Knockout. Задача поставлена определим инструменты. Я буду использовать Visual Studio 2012, NET 4.0, MVC 4.0. Итак, приступим.
Готовим к старту
Создаем новый проект.
Далее надо бы обновить все пакеты, для этого запускаем команду Update-Package - обновления в консоли Nuget Manager . В новом шаблоне MVC4 проекта уже существует файл knockout.*.js. А если вы решите использовать MVC3, то вам придется дополнительно установить nuget-пакет knockout В результате работы получился большой список обновлений и это несмотря на то, что студия вышла не более месяца назад (для подписчиков MSDN):
PM> Update-Package ... много всяких букв про обновление пакетов ... PM>
Добавим еще один пакет MvcTools:
PM> install-Package mvctools Attempting to resolve dependency 'XmlExport (≥ 0.2.1)'. Successfully installed 'XmlExport 0.2.1'. Successfully installed 'MvcTools 1.6.3'. Successfully added 'XmlExport 0.2.1' to MVCKnockoutDemo. Successfully added 'MvcTools 1.6.3' to MVCKnockoutDemo. PM>
Я сразу удалил файл _Layout.cshtml, а появившийся после установки этого пакета _LayoutExtended.cshtml поставил как стартовый по умолчанию. Это делается в файле _ViewStart.cshtml:
@{ Layout = "~/Views/Shared/_LayoutExtended.cshtml"; }
Сейчас проект не запустится, надо в новом шаблоне поправить “_LogOnPartial” на (новый для MVC4 файл) “_LoginPartial”. А теперь достаточно заменить код внизу страницы:
@Content.Scripts("jquery-1.7.2.min.js", Url) @Content.Scripts("jquery.unobtrusive-ajax.min.js", Url) @Content.Scripts("modernizr-2.5.3.js", Url) @*@Content.Scripts("jquery-ui-1.8.21.custom.min.js", Url)*@ @RenderSection("scripts", false)
на новый (опять же потому что мы используем MVC4, для MVC3 ничего менять не надо в головном шаблоне):
@Scripts.Render("~/bundles/jquery") @RenderSection("scripts", required: false) @Html.WriteScriptBlocks()
Чуть позже в этот список мы добавим скрипты для knockout. Запустим проект – Да! Запустился.
Примечание:Внешний вид измененного шаблона не ахти какой, но зато какая свобода для творчества! Не оставлять же шаблон по умолчанию. Но на самом деле, я еще успел переделать nuget-пакет для MVC4.
А сейчас поставим еще один пакет Knockout.Mapping:
PM> Install-Package knockout.Mapping Attempting to resolve dependency 'knockoutjs (≥ 2.0.0)'. Successfully installed 'Knockout.Mapping 2.3.2'. Successfully added 'Knockout.Mapping 2.3.2' to MVCKnockoutDemo. PM>
И после этого еще один пакет:
PM> Install-Package knockout.Validation Attempting to resolve dependency 'knockoutjs (≥ 2.0.0)'. Successfully installed 'Knockout.Validation 1.0.1'. Successfully added 'Knockout.Validation 1.0.1' to MVCKnockoutDemo. PM>
В папке Scripts появились новенькие файлы, а мы создам папку Js чтобы складировать туда скрипты, которые будем писать сами. А сейчас пока отложим эту папку в “сторону”.
Снова модели? Ага, потому что MVC
Создадим простой класс FeedbackViewModel, который будет заполнять пользователь для отправки формы обратной связи:
public class FeedbackViewModel { [Required] [StringLength(100)] [Display(Name = "Тема сообщения")] public string Subject { get; set; } [Required] [StringLength(50)] [Display(Name = "Как к Вам обращаться")] public string UserName { get; set; } [Required] [RegularExpression(@"^\w ([- .']\w )*@\w ([-.]\w )*\.\w ([-.]\w )*$", ErrorMessage = "Неверный формат электронной почты")] [StringLength(50)] [Display(Name = "Email для обратной связи")] public string EmailAdrress { get; set; } [Required] [StringLength(500)] [DataType(DataType.MultilineText)] [Display(Name = "Текст сообщения")] public string Message { get; set; } public override string ToString() { return string.Format(formatstring, this.UserName, this.Subject, this.Message, this.EmailAdrress); } private const string formatstring = @"Новое сообщение!\nПосетитель сайта {0} пишет на тему: ""{1}""\nСообщение:{2}\nОбратный адрес {3}"; }
AjaxController
Создаем новый контролер под названием AjaxController, он будет унаследован от базового класса Controller (Про ApiController и WebAPI мы поговорим в следующий раз).
Добавим первый метод, который будут возвращать JsonResult - список тем для сообщений от пользователя. Я это делаю для наглядности, потому что этот список можно было бы хранить в javascript-коде.
Итак, метод, возвращающий список тем сообщения:
/// <summary> /// Загрузка тем для сообщения /// будет тоже происходить через /// ajax-запрос с формы /// </summary> /// <returns></returns> public JsonResult LoadSubjects() { List<string> subjects = new List<string>() { "Заявка на регистрацию блога на calabonga.net", "Связь с администратором", "Связь с блогером", "Вопрос об копирайтах", "Благодарственное письмо", "Желание поблагодарить материально" }; return Json(subjects.ToArray(), JsonRequestBehavior.AllowGet); }
Всё просто.
Представления (View)
А теперь давайте подправим главную страницу сайта. Откроем Index.cshtml и удалив всё лишнее оставим следующую разметку. Именно с главной формы мы будем отправлять сообщения (feedback). Итак, разметка:
@{ ViewBag.Title = "Отправка сообщения"; } <h3></h3> <p> <input type="submit" value="Отправить сообщение" /> </p>
Далее мы ее будем дописывать. Теперь подключим knockout и все что необходимо для его работы в главном шаблоне проекта, чтобы на конкретных представления (view) достаточно было подключить только скрипт с ViewModel’ом этого представления. Получилось не очень понятно, и поэтому прошу прощения за каламбур. По ходу дальше будет понятнее.
Так как мы используем MVC4 можно воспользоваться Bundles (это нечто новое и ужасно полезное из того, что появилось в MVC4). В папке App_Start есть файл BundleConfig.cs.
public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js", "~/Scripts/knockout-2.1.0.js", "~/Scripts/knockout.mapping-latest.js", "~/Scripts/knockout.validation.js")); bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include( "~/Scripts/jquery-ui-{version}.js")); bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( "~/Scripts/jquery.unobtrusive*", "~/Scripts/jquery.validate*")); bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( "~/Scripts/modernizr-*")); bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css")); bundles.Add(new StyleBundle("~/Content/themes/base/css").Include( "~/Content/themes/base/jquery.ui.core.css", "~/Content/themes/base/jquery.ui.resizable.css", "~/Content/themes/base/jquery.ui.selectable.css", "~/Content/themes/base/jquery.ui.accordion.css", "~/Content/themes/base/jquery.ui.autocomplete.css", "~/Content/themes/base/jquery.ui.button.css", "~/Content/themes/base/jquery.ui.dialog.css", "~/Content/themes/base/jquery.ui.slider.css", "~/Content/themes/base/jquery.ui.tabs.css", "~/Content/themes/base/jquery.ui.datepicker.css", "~/Content/themes/base/jquery.ui.progressbar.css", "~/Content/themes/base/jquery.ui.theme.css")); }
Обратите внимание на строку номер 2. В главном шаблоне _LayoutExtended.cshtml у нас есть строка:
1: @Scripts.Render("~/bundles/jquery")
Это значит, что все скрипты зарегистрированные в этом пакете (от английского Bundle - “пакет”) будут загружаться на все страницы сайта. Я просто добавлю в этот пакет еще парочку строк:
bundles.Add(new ScriptBundle("~/bundles/jquery").Include "~/Scripts/jquery-{version}.js", "~/Scripts/knockout-2.1.0.js", "~/Scripts/knockout.mapping-latest.js", "~/Scripts/knockout.validation.js"));
Я по привычке подключил сразу всё, что называется “до кучи”: и сам нокаут, и расширение маппинга, и валидацию ввода для нокаута. Хотя для такого просто проекта этого и не требовалось (зато вы теперь знаете про эти расширения). Теперь я могу проверить, что скрипты нокаута грузятся:
Теперь начнем, собственно говоря, само программирование.
JavaScript – своими руками
По идеи, надо было бы рассказать об архитектуре (структуре) JavaScript программирования отдельной статьёй, но я понадеюсь на то, что вы уже не раз не только слышали, но и применяли в личном опыте паттерны программирования на JavaScript: Object Literals, Module Pattern, Revealing Module Pattern, Prototype Pattern и другие паттерны.
Я предпочитаю разделять функционал по разным файлам, тем более, что в MVC4 появилась такая прекрасная вещь как минимизация и пакетирование (Bundles). Я создал файл site.core.js, в котором подготовил обертки для работы с jQuery.Ajax. Вот часть кода файла:
(function (site) { "use strict"; var baseUrl = "/ajax/", serviceUrl = function (method) { return baseUrl method; }; site.services.ajax = function () { var getAjaxJson = function (method, jsonIn, callback) { $.ajax({ url: serviceUrl(method), data: ko.toJS(jsonIn), type: 'GET', dataType: 'json', contentType: 'application/json; charset=utf-8', success: function (json) { callback(json); }, error: function (jqXHR, textStatus) { if (confirm(jqXHR.status " " textStatus ":" jqXHR.statusText)) { alert(jqXHR.responseText); } } }); }, postAjaxJson = function (method, jsonIn, callback) { $.ajax({ url: serviceUrl(method), data: ko.toJS(jsonIn), type: 'POST', dataType: 'json', contentType: 'application/json; charset=utf-8', success: function (json) { callback(json); }, error: function (jqXHR, textStatus) { if (confirm(jqXHR.status " " textStatus ":" jqXHR.statusText)) { alert(jqXHR.responseText); } } }); } return { get: getAjaxJson, post: postAjaxJson }; }(); })(site);
Также я создал файл site.services.feedback.js. Этот файл содержит непосредственно сам data-сервис, который и будет используя обертку site.services.ajax чтобы обращаться к методам AjaxController:
/////////////////////////////////////////////////////////////// // site.feedback // Работает через dataService с объектами на форме // Feedback // автор: calabonga.net /////////////////////////////////////////////////////////////// (function (site) { "use strict"; site.services.feedbackForm = { loadSubjects: function (callback) { if (typeof callback === undefined) { throw new Error(200, "callback is undefined"); } site.services.ajax.get("LoadSubjects", {}, callback); }, sendFeedback: function (feedback, callback) { if (typeof callback === undefined) { throw new Error(200, "callback is undefined"); } site.services.ajax.post("SendFeedback", feedback, callback); } }; })(site);
Обратите внимание на строки 17 и 23, именно в них и вызываются методы контролера.
И, собственно говоря, еще один файл site.vm.feedback.js, который уже является ViewModel’ом для представления Index.cshtml:
/////////////////////////////////////////////////////////////// // site.feedback // Работает через dataService с объектами на форме // Feedback // автор: calabonga.net /////////////////////////////////////////////////////////////// (function (site) { "use strict"; site.vm.feedbackViewModel = function(){ var view = { title: "Отправка сообщения" }, subjects = ko.observableArray([]), isbusy = ko.observable(false), loadSubjects = function () { isbusy(true); site.services.feedbackForm.loadSubjects(callback); }, callback = function (json) { isbusy(false); ko.mapping.fromJS(json, {}, subjects); //subjects(json); var total = subjects().length; } loadSubjects(); return { view: view, subjects: subjects, isbusy: isbusy } }(); })(site); $(function () { "use strict"; // привязка ViewModel к форме ko.applyBindings(site.vm.feedbackViewModel); });
Этот ViewModel может пока только загрузить список тем для сообщений, мы его доработаем позже. Назовем этот листинг – “модель 1”. Далее по ходу статьи, я буду на него ссылаться.
Для того чтобы подключить эти скрипты, на странице Index.cshtml пропишу такой код в самом низу страницы, чтобы не мешался:
@section scripts { <script src="@Scripts.Url("~/js/site.core.js")"></script> <script src="@Scripts.Url("~/js/site.service.feedback.js")"></script> <script src="@Scripts.Url("~/js/site.vm.feedback.js")"></script> }
Библиотеки все подгружены (на главном шаблоне), скрипты подключены (на странице Index.cshtml) – переходим к самой разметке.
Декларативная привязка (Declarative binding)
Теперь самое интересное. Я постараюсь объяснить, что же такое декларативная привязка (MVVM) на конкретном примере. Для начала добавлю код, который выведет на форму наименование (см. “модель 1” строка 15) и количество загруженных тем сообщений (см. “модель 1” строка 17):
@{ ViewBag.Title = "Отправка сообщения"; } <h3 data-bind="text: view.title"></h3> <div data-bind="ifnot: isbusy"> <span data-bind="text: subjects().length"></span> <p> <input type="submit" value="Отправить сообщение" /> </p> </div> @section scripts { <script src="@Scripts.Url("~/js/site.core.js")"></script> <script src="@Scripts.Url("~/js/site.service.feedback.js")"></script> <script src="@Scripts.Url("~/js/site.vm.feedback.js")"></script> }
Вот таким незатейлевым способом с использованием атрибута HTML5 “data-…” это можно сделать. Заголовок представления привязывается к HTML-разметке в строке 5, количество загруженных тем - в строке 7. А вот так это выглядит:
Теперь отобразим список тем в элементе <ul>. Для это придется использовать шаблон. Благо что начиная с версии нокаута 2.0 появилась поддержка собственных (nativeTemplateEngine) шаблонов. Код для отображения списка тем сообщений с использованием шаблонов knockout:
<h3 data-bind="text: view.title"></h3> <div data-bind="ifnot: isbusy"> <p> Тем для сообщения (<span data-bind="text: subjects().length"></span>шт.): </p> <ul data-bind="template: {'name':'subjectTemplate', foreach: subjects}"></ul> <p> <input type="submit" value="Отправить сообщение" /> </p> </div> <script id="subjectTemplate" type="text/html"> <li><span data-bind="text: $data"></span></li> </script> @section scripts { <script src="@Scripts.Url("~/js/site.core.js")"></script> <script src="@Scripts.Url("~/js/site.service.feedback.js")"></script> <script src="@Scripts.Url("~/js/site.vm.feedback.js")"></script> }
Добавленный элемент списка (см. строку 6) использует шаблон (см. строки с 13-15). Вот теперь как это выглядит:
Отлично! Вот только мне нужен выпадающий список. Легко для этого поменяем элемент и привязку на новый тип:.
<h3 data-bind="text: view.title"></h3> <div data-bind="ifnot: isbusy"> <p> Тем для сообщения (<span data-bind="text: subjects().length"></span>шт.): </p> <p> <label for="Subject"></label> <select data-bind="options: subjects" id="Subject"></select> </p> <p> <input type="submit" value="Отправить сообщение" /> </p> </div>
Я удалил <ul> вместе с шаблоном и поставил <select> (см. строка 8):
Обратите внимания, я ни строчки кода при этом не поменял! Только разметка! Это и есть декларативная привязка, которая реализовывается принципами MVVM паттерна, которые, в свою очередь, предоставляет фреймворк Knockout (нокаут).
Ссылки
Заключение
В следующей части, я закончу формирование формы обратной связи, подключу проверку (валидацию) введенных пользователем данных, реализую отправку через метод Send. Пишите комментарии, мне важно ваше мнение.
Комментарии к статье (7)
Спасибо!
А у меня не запускается,выдает ошибку:
Ошибка сервера в приложении '/'.
The following sections have been defined but have not been rendered for the layout page "~/Views/Shared/_LayoutExtended.cshtml": "featured".
Ахмед, такое ощущение, что вы или не установили MvcTools или не указали, чтобы этот мастер-шаблон использовался по умолчанию. Ищите в статье место про _ViewStart.cshtml
ПроApiController и WebAPI мы поговорим в следующий раз.- ждем с нетерением.
во вьювСтарте я менял имя мастер страницы и все обновления произвел,и мвц тулс установил. Просто моя студия экспресс2012 такое сообщение выдает,когда я указываю на другую мастер страницу, предворительно создав и указав во вьювстарте имя файла мастер страницы. Я даже на другом проекте эксперементировал,но такие же результаты( Ничего. Мене очень нравится ваш блог и ваши статьи, мало где есть такие уроки.
Ахмед, к обоим частям приложены проекты с демонстрациями, вы не пробовали разобраться что не так в вашем проекте?
Нашлась причина: если добавить этот метод использования разделов @RenderSection("featured", required: false) в мастер-страницу - не выводит ошибку. Или если закоментировать либо удалить определение раздела в index.cshtml. Щас буду двигаться дальше)