ASP.NET MVC: MVVM на HTML или использование knockout при создании сайта (часть 1 из 2).
Сайтостроение | создано: 14.09.2012 | опубликовано: 14.09.2012 | обновлено: 13.01.2024 | просмотров: 14602 | всего комментариев: 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. Щас буду двигаться дальше)