ASP.NET MVC: DataSource на JavaScript или обертка на Web API сервис (часть 1)
Сайтостроение | создано: 15.07.2013 | опубликовано: 15.07.2013 | обновлено: 13.01.2024 | просмотров: 8632
В этой статье будем строить форму Master/Detail на JavaScript с использованием KnockoutJs. Цель статьи: практическое применения контрола DataSource из nuget-пакета JsSite с ASP.NET Web API.
Подготовим проект
1. Создаем новый проект по шаблону ASP.NET MVC 4 (я выбрал “basic”). На момент создания статьи ASP.NET MVC 5 находится в статусе beta. Изучив указанные нововведения, могу предложить, что пример будет работать и версией MVC 5.
2. Запускаем процедуру обновления всех nuget-пакетов. Лог обработки команды показывать не буду.
3. Устанавливаем недостающие пакеты. Во-первых, пакет JsSite для начала:
PM> Install-Package jssite Attempting to resolve dependency 'toastr (≥ 1.1.4.2)'. Attempting to resolve dependency 'jQuery (≥ 1.6.3)'. Attempting to resolve dependency 'AmplifyJS (≥ 1.1.0)'. Attempting to resolve dependency 'knockoutjs (≥ 2.2.1)'. Attempting to resolve dependency 'Knockout.Mapping (≥ 2.4.0)'. Attempting to resolve dependency 'Knockout.Validation (≥ 1.0.1)'. Attempting to resolve dependency 'underscore.js (≥ 1.4.3)'. Attempting to resolve dependency 'Moment.js (≥ 1.7.2)'. Attempting to resolve dependency 'infuser (≥ 0.2.1)'. Attempting to resolve dependency 'TrafficCop (≥ 0.3.0)'. Attempting to resolve dependency 'Knockout.js_External_Template_Engine (≥ 2.0.0)'. Installing 'Knockout.js_External_Template_Engine 2.0.5'. Successfully installed 'Knockout.js_External_Template_Engine 2.0.5'. Installing 'JsSite 0.6.1'. Successfully installed 'JsSite 0.6.1'. Adding 'Knockout.js_External_Template_Engine 2.0.5' to JsSiteDataSourceDemo. Successfully added 'Knockout.js_External_Template_Engine 2.0.5' to JsSiteDataSourceDemo. Adding 'JsSite 0.6.1' to JsSiteDataSourceDemo. Successfully added 'JsSite 0.6.1' to JsSiteDataSourceDemo. PM>
Далее устанавливаем SampleData, чтобы были данные, с которыми можно ставить эксперименты:
PM> Install-Package SampleData Installing 'SampleData 1.2.2'. Successfully installed 'SampleData 1.2.2'. Adding 'SampleData 1.2.2' to JsSiteDataSourceDemo. Successfully added 'SampleData 1.2.2' to JsSiteDataSourceDemo. PM>
А еще для разбивки данных на страницы нам потребуется PagedListExt:
PM> Install-Package PagedListExt Installing 'PagedListExt 0.6.6'. Successfully installed 'PagedListExt 0.6.6'. Adding 'PagedListExt 0.6.6' to JsSiteDataSourceDemo. Successfully added 'PagedListExt 0.6.6' to JsSiteDataSourceDemo. PM>
4. Теперь правим BundleConfig.cs, чтобы подключить установленные скрипты. У меня после правки стал выглядеть следующим образом:
public static void RegisterBundles(BundleCollection bundles) {
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/third").Include(
"~/Scripts/globalize.js",
"~/Scripts/cultures/globalize.culture.ru.js",
"~/Scripts/cultures/globalize.culture.ru-RU.js",
"~/Scripts/cultures/moment.min.js",
"~/Scripts/cultures/toastr.min.js",
"~/Scripts/cultures/underscore.min.js",
"~/Scripts/underscore.js",
"~/Scripts/moment.js",
"~/Scripts/infuser.js",
"~/Scripts/TrafficCop.js",
"~/Scripts/amplify.js"));
bundles.Add(new ScriptBundle("~/bundles/knockout").Include(
"~/Scripts/knockout-2.2.1.debug.js",
"~/Scripts/knockout.mapping-latest.debug.js",
"~/Scripts/koExternalTemplateEngine.js",
"~/Scripts/knockout.command.js",
"~/Scripts/knockout.dirtyFlag.js",
"~/Scripts/knockout.validation.debug.js"));
bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
"~/bootstrap/js/bootstrap.js"));
bundles.Add(new ScriptBundle("~/bundles/site").Include(
"~/Scripts/app/site.core.js",
"~/Scripts/app/site.controls.js",
"~/Scripts/app/site.bindingHandlers.js",
"~/Scripts/app/site.services.person.js",
"~/Scripts/app/site.m.all.js",
"~/Scripts/app/site.utils.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*"));
// 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 StyleBundle("~/bootstrap/css")
.Include("~/bootstrap/css/bootstrap.css")
.Include("~/bootstrap/css/bootstrap-responsive.css"));
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"));
}
Следует иметь в виду, что все скрипты (или почти все), которые будут созданы в процессе работы над статьёй будут добавляться в bundle/site (строка 29).
5. Теперь надо создать HomeController и представление Index. Дело в том, что из всех любезно предложенных студией шаблонов, я выбрал для своего проекта Basic шаблон, который не включает в себя куча разных ненужных “ненужностей”, но при этом не имеет контролера по умолчанию. Хотя в настройках маршрутов RouteConfig.cs, название контролера по умолчанию указано “Home”.
Проект готов к работе. Теперь можно приступить непосредственно к реализации Master/Details паттерну. На первом этапе создадим сервисы Web API.
Сервисы WEB API
Первый из них будет работать с классом Person из сборки SampleData. Пусть для начала он выглядит таким образом:
public class PersonApiController : ApiController {
private List _listOfPerson = new List();
public HttpResponseMessage GetPersons(JsonQueryParams query) {
return Request.CreateResponse(HttpStatusCode.OK,
new {
success = "все данные",
total = _listOfPerson.Count,
items = _listOfPerson
});
}
public HttpResponseMessage GetPerson(int id) {
return Request.CreateResponse(HttpStatusCode.OK,
new {
success = "один по идентификатору",
item = _listOfPerson.Where(x => x.Id.Equals(id))
});
}
public HttpResponseMessage PostPerson(Person person) {
throw new NotImplementedException();
}
public HttpResponseMessage PutPerson(Person person) {
throw new NotImplementedException();
}
public HttpResponseMessage DeletePerson(Person person) {
throw new NotImplementedException();
}
}
Class site.m.Person.js
Перед тем как создать сам файл сервиса, надо бы создать класс (ViewModel) как ViewModel для Person на Javascript:
(function (site, ko) {
site.m.Person = function (dto) {
var me = this, data = dto || {};
me.age = ko.observable(data.Age);
me.country = ko.observable(data.Country);
me.departmentId = ko.observable(data.DepartmentId);
me.description = ko.observable(data.Description);
me.id = ko.observable(data.Id);
me.gender = ko.observable(data.Gender);
me.isMember = ko.observable(data.IsMember);
me.name = ko.observable(data.Name);
me.weight = ko.observable(data.Weight);
me.selected = ko.observable(false);
return me;
};
})(site, ko)
Строки 6-14 определяют существующие поля и свойства класса Person, который мы взяли из пакета SampleData.
Строка 16 выполняет требование DataSource контрола, который, по нашей договоренности, может установить это свойство в значение “true”, если пользователь сделает выбор этой сущности (дальше будет понятнее).
Не забудьте добавить ссылку на этот файл в представлении (view) или в BundleConfig.cs.
@section scripts
{
<script src="~/Scripts/app/site.m.person.js"></script>
<script src="~/Scripts/app/site.vm.homeIndex.js"></script>
}
ViewModel для представления (View)
Наверное, вы уже обратили внимание на то что, в строке 4 предыдущего листинга присутствует ссылка на файл site.vm.homeIndex.js? Это как раз тот самый viewModel, который мы должны создать в этом разделе. Этот файл (ViewModel) является отправной точкой, именно он запускает весь механизм. На текущий момент его содержимое такое:
/// <reference path="site.controls.js" />
/// <reference path="site.core.js" />
/// <reference path="../knockout-2.2.1.debug.js" />
$(function() {
"use strict";
site.vm.homeIndex = function () {
var clock = new site.controls.Clock(),
dsPerson = new site.controls.DataSource({
autoLoad: true,
service : site.services.person
});
return {
dsPerson: dsPerson,
clock: clock
};
}();
ko.applyBindings(site.vm.homeIndex);
});
В строке 11 создаем объект “часы”. Как я уже упоминал статьях опубликованных ранее, это делается для того, чтобы проверить, что необходимые скрипты загружены на странице, и не просто загружены, а “правильной” последовательности. Это своего рода тест конфигурации скриптов на странице. Я в разметку Index.cshtml добавил span-тег, чтобы часы заработали и запустил проект.
@{
ViewBag.Title = "DataSource: Master/Details";
}
<span data-bind="text: clock.time">span>
Часы заработали. Далее добавил строки 12-15.
В строке 12-15 создаем экземпляр контрола DataSource. Параметрами DataSource для него служат autoLoad (необязательный) со значение true и service (обязательный) со значением site.services.person. Этот сервис мы будем создаем в следующем разделе.
Сервис для DataSource
Скажу честно, я для создания сервисов для DataSource использую шаблон (snippet или Template от Resharper) в силу того, что для различных сущностей сервисы практически не отличаются. Вот полный текст работающего сервиса:
/// <reference path="site.core.js" />
(function(site) {
site.services.person = function() {
var init = function() {
site.amplify.request.define("getperson", "ajax", {
url: "/api/personapi",
dataType: "json",
type: "GET",
cache: false
});
site.amplify.request.define("postperson", "ajax", {
url: "/api/personapi",
dataType: "json",
contentType: "application/json; charset=utf-8",
type: "POST",
cache: false
});
site.amplify.request.define("putperson", "ajax", {
url: "/api/personapi",
dataType: "json",
contentType: "application/json; charset=utf-8",
type: "PUT",
cache: false
});
site.amplify.request.define("delperson", "ajax", {
url: "/api/personapi",
dataType: "json",
contentType: "application/json; charset=utf-8",
type: "DELETE",
cache: false
});
},
mapItem = function(data) {
return new site.m.Person(data);
},
mapItems = function(data) {
var mapped = [];
site._.each(data, function(item) {
mapped.push(mapItem(item));
});
return mapped;
},
getData = function(params, back) {
if (typeof back !== "function") throw new Error("callback not a function");
if (!params) throw new Error("queryParams notis null");
return site.amplify.request({
resourceId: "getperson",
data: { qp: ko.toJSON(params) },
success: function(json) {
if (json) {
if (json.success) {
params.total(json.total);
var result = mapItems(json.items);
back(result);
return;
}
if (json.warning) {
site.logger.warning(json.warning);
}
if (json.error) {
site.logger.error(json.error);
}
}
back();
},
error: function() {
site.logger.error("Ошибка загрузки сущности \"Пользователь\" (method \"get\") Person");
back();
return;
}
});
},
getDataById = function(params, back) {
if (typeof back !== "function") throw new Error("callback not a function");
if (!params) throw new Error("queryParams notis null");
return site.amplify.request({
resourceId: "getperson",
data: { id: params },
success: function(json) {
if (json) {
if (json.success) {
var result = mapItem(json.item);
back(result);
return;
}
if (json.warning) {
site.logger.warning(json.warning);
}
if (json.error) {
site.logger.error(json.error);
}
}
back();
},
error: function() {
site.logger.error("Ошибка загрузки сущности \"Должность\" (method \"get\") Person");
back();
return;
}
});
},
postData = function(params, back) {
if (typeof back !== "function") throw new Error("callback not a function");
return site.amplify.request({
resourceId: "postperson",
data: ko.toJSON(params),
success: function(json) {
if (json) {
if (json.success) {
site.logger.success(json.success);
back(new mapItem(json.item));
return;
}
if (json.warning) {
site.logger.warning(json.warning);
}
if (json.info) {
site.logger.info(json.info);
}
if (json.error) {
site.logger.error(json.error);
}
}
back();
},
error: function() {
site.logger.error("Ошибка сохранения сущности \"Пользователь\" (method \"post\") Person");
back();
return;
}
});
},
putData = function(params, back) {
if (typeof back !== "function") throw new Error("callback not a function");
return site.amplify.request({
resourceId: "putperson",
data: ko.toJSON(params),
success: function(json) {
if (json) {
if (json.success) {
site.logger.success(json.success);
back(new mapItem(json.item));
return;
}
if (json.warning) {
site.logger.warning(json.warning);
}
if (json.error) {
site.logger.error(json.error);
}
}
back();
},
error: function() {
site.logger.error("Ошибка обновления сущности \"Пользователь\" (method \"put\") Person");
back();
return
}
});
},
delData = function(params, back) {
if (typeof back !== "function") throw new Error("callback not a function");
return site.amplify.request({
resourceId: "delperson",
data: ko.toJSON(params),
success: function(json) {
if (json) {
if (json.success) {
site.logger.success(json.success);
back(new mapItem(json.item));
return;
}
if (json.warning) {
site.logger.warning(json.warning);
}
if (json.error) {
site.logger.error(json.error);
}
}
back();
},
error: function() {
site.logger.error("Ошибка удаления сущности \"Пользователь\" (method \"del\") Person");
back();
return;
}
});
};
init();
return {
getDataById: getDataById,
postData: postData,
getData: getData,
putData: putData,
delData: delData
};
}();
})(site);
Теперь всё готово для первого запуска.

Заключение
В качестве заключения скажу следующее, в данной части мы подготовили проект, а именно: Web API сервис, ViewModel на JavaScript для класса Person, JavaScript обертку для Web API, главную страницу, ViewModel для главной странице.
В данной реализации совсем не работает пейджинг, нет возможности какой-либо фильтрации. В следующей части будем добавлять этот функционал на страницу.