HTML 5: Пример использования knockout, amplify и underscore или JsSite как стартовая архитектура для сайта (обновление)
Цель
В одной из прошлых статьей было рассказано о nuget-пакете под названием JsSite. В последнее время достаточно часто и продуктивно пришлось работать с этим пакетом, и как следствие, сам пакет претерпел большое количество изменений. Цель данной статьи, описать возможности (в том числе и новые), которые предоставляет данный набор скриптов.
Еще раз хочу предупредить, что JsSite всего лишь простой пример построения архитектуры сайта с использованием knockoutjs, amplifyjs, moment и других полезных библиотек на JavaScript. Но этот пакет влючены некоторые полезные, по моему мнению, контролы, в частности DataSource? о котором и пойдет речь в этой статье.
Пример использования или How to use.
В примере будем строить MVC приложение, которое будет отображать список сотрудников (возьмем из nuget-пакета SampleData) с использованием AJAX, Web API и knockoutjs. Ключевой момент в том, чтобы не просто отображать данные, а разбить их на страницы, подключить простейший фильтр, и возможность задавать количество записей на странице.
Более того, очень хочется один раз написать сервис для работы с сущностью, а потом по возможности использовать его на разных страницах и/или в разных запросах (в том числе типа “Master/Details”).
Подготовка к работе
Создаем новое ASP.NET MVC приложение. Я выбрал новый (появился после ASP.NET and Web Tools 2012.2) шаблон MVC4 Basic, в нем папки Controller и Model пусты. Проект я назову JsSitePackageDemo2, а вы как посчитаете нужным. Запустим обновление всех предустановленных пакетов, выполнив команду update-package в Package Manager Console. И после этого поставим несколько дополнительных пакетов:
1) 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 'underscore.js (≥ 1.4.3)'.
Attempting to resolve dependency 'Moment.js (≥ 1.7.2)'.
Successfully installed 'toastr 1.1.5'.
Successfully installed 'AmplifyJS 1.1.0'.
Successfully installed 'Knockout.Mapping 2.4.0'.
You are downloading underscore.js from Jeremy Ashkenas, the license agreement to which is available at https://github.com/documentcloud/underscore/blob/master/LICENSE. Check the package for additional dependencies, which may come with their own license agreement(s). Your use of the package and dependencies constitutes your acceptance of their license agreements. If you do not accept the license agreement(s), then delete the relevant components from your device.
Successfully installed 'underscore.js 1.4.4'.
Successfully installed 'Moment.js 1.7.2'.
Successfully installed 'JsSite 0.4.2'.
Successfully added 'toastr 1.1.5' to JsSitePackageDemo2.
Successfully added 'AmplifyJS 1.1.0' to JsSitePackageDemo2.
Successfully added 'Knockout.Mapping 2.4.0' to JsSitePackageDemo2.
Successfully added 'underscore.js 1.4.4' to JsSitePackageDemo2.
Successfully added 'Moment.js 1.7.2' to JsSitePackageDemo2.
Successfully added 'JsSite 0.4.2' to JsSitePackageDemo2.
PM>
2) SampleData:
PM> Install-Package SampleData
Successfully installed 'SampleData 1.2.2'.
Successfully added 'SampleData 1.2.2' to JsSitePackageDemo2.
PM>
Подключаю новые CSS, которые появились с установленным пакетом JsSite (см. строка 2):
1: bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"
2: , "~/Content/toastr.css", "~/Content/site.pages.css"));
Можно теперь приступить непосредственно к кодированию. Если учесть, что контролеров в этом шаблоне нет, а в файле RouteConfig.cs прописан маршрут на контролер Home:
1: routes.MapRoute(
2: name: "Default",
3: url: "{controller}/{action}/{id}",
4: defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
5: );
Назревает вопрос, зачем разработчики оставили его или зачем удалил Home контролер? Ну, да ладно, суть не в этом. Меняю наименование контролера на “Site” и создаю новый контролер с этим названием.
Добавлю к методу Index представление (View). На этом представлении и будем упражняться в написании кода. Первым делом добавлю ссылки на скрипты библиотек в секцию scripts. Если учесть что jQuery уже установлена (bundle/jquery в шаблоне), то мне остается добавить ссылки на сторонние библиотеки и на скрипты из пакета JsSite:
1: @section scripts
2: {
3: <!-- third-party library -->
4: <script src="~/Scripts/toastr-1.1.5.min.js"></script>
5: <script src="~/Scripts/amplify.min.js"></script>
6: <script src="~/Scripts/moment.min.js"></script>
7: <script src="~/Scripts/underscore.min.js"></script>
8: <script src="~/Scripts/knockout-2.2.1.js"></script>
9: <script src="~/Scripts/knockout.mapping-latest.js"></script>
10:
11: <!-- jssite and project library -->
12: <script src="~/Scripts/app/site.core.js"></script>
13: <script src="~/Scripts/app/site.bindingHandlers.js"></script>
14: <script src="~/Scripts/app/site.controls.js"></script>
15: }
Я пока не заморачиваюсь на оптимизацию скриптов: “склеивание” и “сжатие”, но в реальном проекте без этого не обойтись. Один из вариантов решения был описан ранее.
В папке App есть еще один файл, который я не добавил на страницу. Дело в том, что этот файл всего лишь пример написания сервиса для DataSource. Мы займемся написанием сервиса чуть позже. Сначала серверная часть.
Web API + OData
Создаем новый API-контролер назовем его PersonController:
Не обойдите вниманием, шаблон – “API controller…”. Вот так выглядит код этого контролера после некоторых доработок причем несложных, но не окончательных:
1: publicclass PersonController : ApiController {
2: privatereadonly List<Person> _list;
3:
4: public PersonController() {
5: _list = People.GetPeople();
6: }
7:
8: // GET api/person
9: public IEnumerable<Person> Get() {
10: return _list;
11: }
12:
13: // GET api/person/5
14: publicstring Get(int id) {
15: return"value";
16: }
17:
18: // POST api/person
19: publicvoid Post([FromBody]stringvalue) {
20: }
21:
22: // PUT api/person/5
23: publicvoid Put(int id, [FromBody]stringvalue) {
24: }
25:
26: // DELETE api/person/5
27: publicvoid Delete(int id) {
28: }
29: }
Строка 2: создаем переменную для хранения списка пользователей. Не забудьте добавить namespace SampleData.
Строки 4-6: в конструкторе наполняем список.
Если воспользоваться прекрасной утилитой Fiddler, то можно протестировать сервис, отправив запрос:
и получить ответ в:
OData – теперь это просто
Небольшое лирическое отступление. Протокол OData теперь есть в MVC4 (не полная реализация, но и это уже не мало). Для того, чтобы превратить наш PersonController в контролер, который будет понимать OData запросы, надо установить еще один nuget-пакет Microsoft.AspNet.WebApi.OData, который добавит магический атрибут Queryable.
PM> Install-Package Microsoft.AspNet.WebApi.OData
Attempting to resolve dependency 'Microsoft.Net.Http (≥ 2.0.20710.0 && < 2.1)'.
Attempting to resolve dependency 'Microsoft.AspNet.WebApi.Client (≥ 4.0.20710.0 && < 4.1)'.
Attempting to resolve dependency 'Newtonsoft.Json (≥ 4.5.6)'.
Attempting to resolve dependency 'Microsoft.AspNet.WebApi.Core (≥ 4.0.20710.0 && < 4.1)'.
Attempting to resolve dependency 'Microsoft.Data.OData (≥ 5.2.0 && < 5.3.0)'.
Attempting to resolve dependency 'System.Spatial (= 5.2.0)'.
Attempting to resolve dependency 'Microsoft.Data.Edm (= 5.2.0)'.
You are downloading System.Spatial from Microsoft Corporation, the license agreement to which is available at http://go.microsoft.com/?linkid=9809688. Check the package for additional dependencies, which may come with their own license agreement(s). Your use of the package and dependencies constitutes your acceptance of their license agreements. If you do not accept the license agreement(s), then delete the relevant components from your device.
Successfully installed 'System.Spatial 5.2.0'.
You are downloading Microsoft.Data.Edm from Microsoft Corporation, the license agreement to which is available at http://go.microsoft.com/?linkid=9809688. Check the package for additional dependencies, which may come with their own license agreement(s). Your use of the package and dependencies constitutes your acceptance of their license agreements. If you do not accept the license agreement(s), then delete the relevant components from your device.
Successfully installed 'Microsoft.Data.Edm 5.2.0'.
You are downloading Microsoft.Data.OData from Microsoft Corporation, the license agreement to which is available at http://go.microsoft.com/?linkid=9809688. Check the package for additional dependencies, which may come with their own license agreement(s). Your use of the package and dependencies constitutes your acceptance of their license agreements. If you do not accept the license agreement(s), then delete the relevant components from your device.
Successfully installed 'Microsoft.Data.OData 5.2.0'.
You are downloading Microsoft.AspNet.WebApi.OData from Microsoft, the license agreement to which is available at http://www.microsoft.com/web/webpi/eula/aspnet_and_web_tools_2012_2_RTW_EULA_ENU.htm. Check the package for additional dependencies, which may come with their own license agreement(s). Your use of the package and dependencies constitutes your acceptance of their license agreements. If you do not accept the license agreement(s), then delete the relevant components from your device.
Successfully installed 'Microsoft.AspNet.WebApi.OData 4.0.0'.
Successfully added 'System.Spatial 5.2.0' to JsSitePackageDemo2.
Successfully added 'Microsoft.Data.Edm 5.2.0' to JsSitePackageDemo2.
Successfully added 'Microsoft.Data.OData 5.2.0' to JsSitePackageDemo2.
Successfully added 'Microsoft.AspNet.WebApi.OData 4.0.0' to JsSitePackageDemo2.
PM>
Если вы не знакомы с протоколом OData, то советую ознакомиться на упомянутом выше сайте. Там же вы можете найти документацию по использованию (спецификацию). После установки пакета можно поставить атрибут [Queryable] над методом Index в контролере PersonController. 1: [Queryable] 2: public IEnumerable<Person> Get() { 3: return _list; 4: }Что это дает? Это на самом деле, не побоюсь этого слова, это “революционная” технология построения запросов непосредственно в строке браузера, то есть формировать запросы к БД можно из строки браузера (!) напрямую. Спецификация протокола очень большая, чтобы рассказать о ней в одном маленьком абзаце, но чтобы было понятен принцип, ознакомьтесь со схемой:
Начало развития OData берет в холодном феврале 2009 года, но на сколько я помню, такой принцип обработки запросов был озвучен еще раньше – в октябре 2007 года (проект “Астория”). На данный момент не все команды по спецификации поддерживаются в ASP.NET MVC 4 Web.API, а список поддерживаемых вы можете найти на ASP.NET.
Прелесть данного подхода в том, что “запрос выполняется непосредственно на SQL сервере”. Это лирическое отступление имело место быть, потому что вчера Microsoft.AspNet.WebApi.OData вышел в статусе Release под номер версии 4.0.0.
Web API
Вернемся к наши Web.API. Для того, чтобы сервис смог вернуть данные разбитые на страницы (это же наша цель, правда?), надо в метод Get() “опустить” как минимум один параметр – номер страницы (pageIndex), а если вы хотите управлять количеством записей на странице со стороны клиента, то второй параметр должен быть – размер страницы (pageSize). У вас не получится без перенастройки Web.API маршрутов “протолкнуть” упомянутые параметры в метод вызова. Настроим маршрут. Я добавил один новый маршрут (в файле WebApiConfig) для того, чтобы Web.API стал “понимать” новые параметры “номер страниц” и “размер страниц” (см. строки 3-7), а не только идентификатор (Id см. строки 9-12) :
1: publicstaticclass WebApiConfig {
2: publicstaticvoid Register(HttpConfiguration config) {
3: config.Routes.MapHttpRoute(
4: name: "PersonApi",
5: routeTemplate: "api/{controller}/{index}-{size}",
6: defaults: new { index = 0, size = 10 }
7: );
8:
9: config.Routes.MapHttpRoute(
10: name: "DefaultApi",
11: routeTemplate: "api/{controller}/{id}",
12: defaults: new { id = RouteParameter.Optional }
13: );
14: }
15: }
Строке 5: обратите внимание на “черточку”, она нам пригодится позже (вообще-то она для “красоты”, чтобы было проще ссылаться в статье). После того как маршруты проложены настроены, можно поработать над методом Get контролера PersonController.
1: public HttpResponseMessage Get(int? index, int? size) {
2: var items = _list.AsQueryable();
3: if (index.HasValue && size.HasValue) {
4: items = items.Skip(index.Value * size.Value)
5: .Take(size.Value);
6: }
7: if (items.Any()) {
8: var data = items.ToArray();
9: var result = new ApiResult { Items = data, Total = _list.Count() };
10: return Request.CreateResponse(HttpStatusCode.OK, result);
11: }
12: return Request.CreateResponse(HttpStatusCode.BadRequest); ;
13: }
Строка 1: Возвращаем не просто коллекцию объектов, а обернутую в специальный класс HttpResponseMessage, который дает множество полезных штучек, как например, статус операции запроса. А также добавляем параметры в сигнатуру метода.
Строка 3-6: Если “номер страницы” и “размер страницы” получены, осуществляем выборку.
Строка 9: Создаем возвращаемый объект (см. следующий листинг). Можно использовать и анонимный тип, но нравится типизация:
1: publicclass ApiResult {
2: public IEnumerable<Person> Items { get; set; }
3: publicint Total { get; set; }
4: }
Далее по листингу метода Get().
Строка 10: Возвращает полученный результат с указанием статуса. Обратите внимание на параметр “Items” и “Total”. Для того, чтобы пейджер заработал, ему надо знать сколько всего записей. Эти параметры использует site.controls.DataSource() из пакета JsSite.
Следующим этапом – JavaScript!
Модель сервиса site.services.person.js
Раз уже API-сервис готов, то время пришло для js-сервиса. При установке пакета JsSite в папке App также появляется файл site.services.js. Как уже говорилось, это демонстрационный пример сервиса, который нужен для работы site.controls.DataSource(). Я его переработал, адаптировав под класс Person, и поменял его название. Теперь он называется site.services.person.js и содержит он много строк. Методы addPerson, updatePerson, deletePerson я не стал реализовывать, но я всё равно приведу файл целиком, а после разберем по строкам:
1: (function (site) {
2:
3: "use strict";
4:
5: site.services.init = function () {
6:
7: //#region service Person
8: site.amplify.request.define("loadPerson", "ajax", {
9: url: "api/Person/{0}-{1}",
10: dataType: "json",
11: type: "GET"
12: }),
13: site.amplify.request.define("addPerson", "ajax", {
14: url: "api/Person/",
15: dataType: "json",
16: cache: false,
17: type: "POST"
18: }),
19: site.amplify.request.define("updatePerson", "ajax", {
20: url: "api/Person",
21: dataType: "json",
22: cache: false,
23: type: "PUT"
24: }),
25: site.amplify.request.define("deletePerson", "ajax", {
26: url: "api/Person",
27: dataType: "json",
28: cache: false,
29: type: "DELETE"
30: });
31: //#endregion
32:
33: }();
34:
35: site.services.person = function () {
36: var
37: loadPerson = function (params, back) {
38: return site.amplify.request({
39: resourceId: "loadPerson",
40: data: {index: params.index(), size: params.size()},
41: success: function (json) {
42: // -> here you code json-data processing <-
43: if (json && json.Items) {
44: site.logger.success("Loaded: "
45: + json.Items.length
46: + " Total: " + json.Total);
47: }
48: if (typeof back == "function") {
49: params.total(json.Total);
50: back.call(this, json.Items);
51: }
52: },
53: error: function (message, status) {
54: site.logger.error(message, status);
55: back.call(this);
56: }
57: });
58: },
59: addPerson = function (jsonPerson) {
60: return site.amplify.request({
61: resourceId: "addPerson",
62: data: jsonPerson,
63: success: function (json, status) {
64: // -> here you code json-data processing <-
65: if (typeof back == "function") {
66: back.call(this, json);
67: }
68: },
69: error: function (message, status) {
70: site.logger.error(message, status);
71: back.call(this);
72: }
73: });
74: },
75: updatePerson = function (jsonPerson) {
76: return site.amplify.request({
77: resourceId: "updatePerson",
78: data: jsonPerson,
79: success: function (json, status) {
80: // -> here you code json-data processing <-
81: if (typeof back == "function") {
82: back.call(this, json);
83: }
84: },
85: error: function (message, status) {
86: site.logger.error(message, status);
87: back.call(this);
88: }
89: });
90: },
91: deletePerson = function (jsonPerson) {
92: return site.amplify.request({
93: resourceId: "deletePerson",
94: data: jsonPerson,
95: success: function (json, status) {
96: // -> here you code json-data processing <-
97: if (typeof back == "function") {
98: back.call(this, json);
99: }
100: },
101: error: function (message, status) {
102: site.logger.error(message, status);
103: back.call(this);
104: }
105: });
106: };
107:
108: return {
109: load: loadPerson,
110: put: updatePerson,
111: post: addPerson,
112: del: deletePerson
113: };
114:
115: }();
116:
117: })(site);
Внимание: Не забудьте добавить ссылку на этот скрипт на Index.cshtml.
Итак, что же делает этот код? По порядку.
Строка 5-33: Инициализируем сервис. Настройка amplify под работу с Web.API. Вот тут-то и пригодилась “галочка” (см. строка 9).
Строка 35-106: Сервис для работы с сущностью Person. Все действия в одном месте и, что самое главное, одни раз! Далее DataSource будет брать этот сервис и работать с его методами.
Ах, да! Самое главное! Вы можете приватные методы называть как хотите, а вот наружу должны быть “выставлены” методы именно с таким название как указано в строке 109-112: “load”, “put”, “post” “del”. Это важно!
Получение данных или Load Data Method
Строки 37-58 в предыдущем листинге задает метода получения списка пользователей. В этом методе используется название идентификатор “loadPerson”, который был инициализирован в ранее в строках 8-11. В строке 40 полученные параметры из DataSource (“index” и “size”) передаем в запрос, amplifyjs расставит параметры в соответствии с указание (см. строка 9) через “черточку”.
Строка 41-56: Обработчики полученных данных. В строках 43-47 проверяем полученных объект и выдаем сообщение, а далее в строках 48-51 возвращаем полученные данные (json.Items) в DataSource.
Строка 55: Если возникает ошибка сервиса, то возвращаем в DataSource “ничего” :)
А где же DataSource или покажите ViewModel представления
Самым простым кодом, в примере использования будет js-viewModel для моей страницы Index.cshtml. По большому счету, он содержит всего один контрол – DataSource, именно к нему и осуществляется привязка на форма (в следующем абзаце). Вот ViewModel:
1: $(function () {
2:
3: "use strict";
4:
5: site.vm.viewModel = function () {
6: var clock = new site.controls.Clock(),
7: meta = new site.fw.Metadata(
8: "Demo JsSite",
9: "Демонстрация работы библиотеки",
10: "http://www.calabonga.net"),
11: ds = new site.controls.DataSource(
12: {
13: autoLoad: true,
14: service: site.services.person
15: }
16: );
17:
18: return {
19: ds: ds,
20: meta: meta,
21: clock: clock
22: };
23: }();
24:
25: ko.applyBindings(site.vm.viewModel);
26:
27: });
Строка 6: Создаем объект “часы”. Я его добавляю на форму обычно первым, чтобы перед тем как начать программирование проверить, что скрипты правильно подключены, инициализированы и привязка HTML (applyBindings) настроена корректно.
Строка 7: Создаем для красоты объект “метаданные”.
Строка 11: И, наконец, создаем тот самый объект “DataSource”, в конструктор которого “опускаем” параметры “autoLoad” со значением “true”, кстати, это значение по умолчанию. А вторым параметром “service” значение которого, как раз и является наш person-сервис, описанный выше.
Все параметры и возможности DataSource планируется описать в следующей статье.
Вернемся на форму
Для того, чтобы полученные данные отобразились на представлении, изменим содержание Index.cshtml. Добавим разметку, обратите внимание, что объект привязки DataSource в строке 14 и 23:
1: <h2 data-bind="text: meta.title"></h2>
2: <p data-bind="text: meta.description"></p>
3: <div data-bind="text: clock.time"
style="color:#888; position: fixed; top:20px; left:50%;"></div>
4:
5:
6: <table>
7: <thead>
8: <tr>
9: <th>Name</th>
10: <th>Age</th>
11: <th>Country</th>
12: </tr>
13: </thead>
14: <tbody data-bind="foreach: ds.items">
15: <tr>
16: <td><span data-bind="text: Name"></span></td>
17: <td><span data-bind="text: Age"></span></td>
18: <td><span data-bind="text: Country"></span></td>
19: </tr>
20: </tbody>
21: </table>
22:
23: <div data-bind="pager: ds"></div>
И как результат:
Зависимости или Master/Details
Я создал API-сервиc и js-сервис для сущности “Department” (тоже есть в SampleData), и немного поправил ViewModel, чтобы при выборе подразделения менялся список сотрудников:
1: $(function () {
2:
3: "use strict";
4:
5: site.vm.viewModel = function () {
6: var clock = new site.controls.Clock(),
7: meta = new site.fw.Metadata(
8: "Demo JsSite",
9: "Демонстрация работы библиотеки",
10: "http://www.calabonga.net"),
11: queryParams = { DepartmentId: ko.observable(), size:4 },
12:
13: ds = new site.controls.DataSource(
14: { autoLoad: false, service: site.services.person },queryParams),
15:
16: ds2 = new site.controls.DataSource({
17: service: site.services.department
18: }),
19:
20: selectDepartment = function (item) {
21: ds.queryParams.DepartmentId(item.Id);
22: };
23:
24: ds.queryParams.DepartmentId.subscribe(function() {
25: ds.load();
26: });
27:
28: return {
29: select: selectDepartment,
30: ds2: ds2,
31: ds: ds,
32: meta: meta,
33: clock: clock
34: };
35: }();
36:
37: ko.applyBindings(site.vm.viewModel);
38:
39: });
Конечно же, пришлось поправить маршруты, чтобы новый параметр “departmentId” был доступен для PersonController. Да и сам метод Get у PersonController’а пришлось доработать, чтобы он “понимал” новый параметр и заработала связка “мастер/детали”.
Итак, в строке 11 создал параметр queryParams для DataSource, а в строка 14 его использую. В этой же строке отключена загрузка по умолчанию (autoLoad: false) для списка сотрудников. В queryParams также переопределен размер страниц (size) по умолчанию (равен 10) на новый размер 4.
Строки 16-18: Создал DataSource для сущности Department.
Строки 20-22: Функция обработки сlick по строке в таблице подразделений (см. листинг ниже строка 11).
Строки 29 и 30 выставляют новые классы и переменные наружу.
Новое представление
Добавил таблицу с подразделениями (строк 2-17).
1: <h2>Подразделения</h2>
2: <table>
3: <thead>
4: <tr>
5: <th>Id</th>
6: <th>Name</th>
7: <th>Description</th>
8: </tr>
9: </thead>
10: <tbody data-bind="foreach: ds2.items">
11: <tr data-bind="click: $root.select" style="cursor:pointer;" >
12: <td><span data-bind="text: Id"></span></td>
13: <td><span data-bind="text: Name"></span></td>
14: <td><span data-bind="text: Description"></span></td>
15: </tr>
16: </tbody>
17: </table>
18: <h2>Сотрудники</h2>
19: <div data-bind="pager: ds2"></div>
20:
21: <table>
22: <thead>
23: <tr>
24: <th>Id</th>
25: <th>Department</th>
26: <th>Name</th>
27: <th>Age</th>
28: <th>Country</th>
29: </tr>
30: </thead>
31: <tbody data-bind="foreach: ds.items">
32: <tr>
33: <td><span data-bind="text: Id"></span></td>
34: <td><span data-bind="text: DepartmentId"></span></td>
35: <td><span data-bind="text: Name"></span></td>
36: <td><span data-bind="text: Age"></span></td>
37: <td><span data-bind="text: Country"></span></td>
38: </tr>
39: </tbody>
40: </table>
41:
42: <div data-bind="pager: ds"></div>
Заключение
Что имеем в результате? Вся базовая логика работы с сущностью пишется один раз в одном месте. Достаточно гибкий способ устанавливать зависимости обеспечивает большой функционал при помощи knockout. Если кого-то заинтересовала разработка, то темой следующей статьи можно сделать “параметры, методы, договоренности при использовании DataSource” или вообще выложить DataSource, например, на github.com. В дальнейшем, планируется доработка DataSource для работы по протоколу OData, о котором было упомянуто в статье.
Загрузить
Если у вас возникли вопросы, предлагаю скачать проект с демонстрацией.
Подробнее: http://feedproxy.google.com/~r/blogmusor/~3/Z9_dLbaL-bA/112
Дайджест новых статей по интернет-маркетингу на ваш email
Новые статьи и публикации
- 2024-11-26 » Капитан грузового судна, или Как начать использовать Docker в своих проектах
- 2024-11-26 » Обеспечение безопасности ваших веб-приложений с помощью PHP OOP и PDO
- 2024-11-22 » Ошибки в Яндекс Вебмастере: как найти и исправить
- 2024-11-22 » Ошибки в Яндекс Вебмастере: как найти и исправить
- 2024-11-15 » Перенос сайта на WordPress с одного домена на другой
- 2024-11-08 » OSPanel 6: быстрый старт
- 2024-11-08 » Как установить PhpMyAdmin в Open Server Panel
- 2024-09-30 » Как быстро запустить Laravel на Windows
- 2024-09-25 » Next.js
- 2024-09-05 » OpenAI рассказал, как запретить ChatGPT использовать содержимое сайта для обучения
- 2024-08-28 » Чек-лист: как увеличить конверсию интернет-магазина на примере спортпита
- 2024-08-01 » WebSocket
- 2024-07-26 » Интеграция с Яндекс Еда
- 2024-07-26 » Интеграция с Эквайринг
- 2024-07-26 » Интеграция с СДЕК
- 2024-07-26 » Интеграция с Битрикс-24
- 2024-07-26 » Интеграция с Travelline
- 2024-07-26 » Интеграция с Iiko
- 2024-07-26 » Интеграция с Delivery Club
- 2024-07-26 » Интеграция с CRM
- 2024-07-26 » Интеграция с 1C-Бухгалтерия
- 2024-07-24 » Что такое сторителлинг: техники и примеры
- 2024-07-17 » Ошибка 404: что это такое и как ее использовать для бизнеса
- 2024-07-03 » Размещайте прайс-листы на FarPost.ru и продавайте товары быстро и выгодно
- 2024-07-01 » Профилирование кода в PHP
- 2024-06-28 » Изучаем ABC/XYZ-анализ: что это такое и какие решения с помощью него принимают
- 2024-06-17 » Зачем вам знать потребности клиента
- 2024-06-11 » Что нового в работе Яндекс Метрики: полный обзор обновления
- 2024-06-11 » Поведенческие факторы ранжирования в Яндексе
- 2024-06-11 » Скорость загрузки сайта: почему это важно и как влияет на ранжирование
Постановка целей, приближение к ним день за днем и, наконец, их достижение — это и есть ключ к счастью Трейси Брайан - - современный американский профессиональный лектор-преподаватель |
Мы создаем сайты, которые работают! Профессионально обслуживаем и продвигаем их , а также по всей России и ближнему зарубежью с 2006 года!
Как мы работаем
Заявка
Позвоните или оставьте заявку на сайте.
Консультация
Обсуждаем что именно Вам нужно и помогаем определить как это лучше сделать!
Договор
Заключаем договор на оказание услуг, в котором прописаны условия и обязанности обеих сторон.
Выполнение работ
Непосредственно оказание требующихся услуг и работ по вашему заданию.
Поддержка
Сдача выполненых работ, последующие корректировки и поддержка при необходимости.