ASP.NET MVC: DataSource на JavaScript или обертка на Web API сервис (часть 1)
Сайтостроение | создано: 15.07.2013 | опубликовано: 15.07.2013 | обновлено: 13.01.2024 | просмотров: 8302
В этой статье будем строить форму 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 для главной странице.
В данной реализации совсем не работает пейджинг, нет возможности какой-либо фильтрации. В следующей части будем добавлять этот функционал на страницу.