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.length46: + " 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
|
ЦельВ одной из прошлых статьей было рассказано о nuget-пакете под названием JsSite. В последнее время достаточно часто и продуктивно пришлось работать с этим пакетом, и как следствие, сам пакет |
РэдЛайн, создание сайта, заказать сайт, разработка сайтов, реклама в Интернете, продвижение, маркетинговые исследования, дизайн студия, веб дизайн, раскрутка сайта, создать сайт компании, сделать сайт, создание сайтов, изготовление сайта, обслуживание сайтов, изготовление сайтов, заказать интернет сайт, создать сайт, изготовить сайт, разработка сайта, web студия, создание веб сайта, поддержка сайта, сайт на заказ, сопровождение сайта, дизайн сайта, сайт под ключ, заказ сайта, реклама сайта, хостинг, регистрация доменов, хабаровск, краснодар, москва, комсомольск |
Дайджест новых статей по интернет-маркетингу на ваш email
Новые статьи и публикации
- 2025-12-02 » Когда ошибка молчит: как бессмысленные сообщения ломают пользовательский опыт
- 2025-12-02 » 9 лучших бесплатных фотостоков
- 2025-12-02 » UTM-метки: ключевой инструмент аналитики для маркетолога
- 2025-12-02 » ПромоСтраницы Яндекса: Что такое и для чего служит
- 2025-12-02 » Метатеги для сайта: исчерпывающее руководство по Title, Description, Canonical, Robots и другим тегам
- 2025-11-26 » Оценка эффективности контента: превращаем информационный балласт в рабочий актив
- 2025-11-26 » 10 причин высокого показателя отказов на сайте
- 2025-11-26 » Когда и зачем обновлять структуру сайта
- 2025-11-26 » Скрытые демотиваторы: как мелочи разрушают эффективность команды
- 2025-11-26 » Зачем запускать MVP и как сделать это грамотно?
- 2025-11-20 » Половина российских компаний сократит расходы на транспорт и маркетинг в 2026 году
- 2025-11-20 » Перенос сайта с большим количеством ссылок
- 2025-11-20 » Перелинковка сайта: Что такое и как ее использовать
- 2025-11-20 » Критерии выбора SEO-специалиста и подрядчика для продвижения сайта
- 2025-11-20 » Применение искусственного интеллекта в рекламных агентствах: комплексное исследование трендов 2025 года
- 2025-11-19 » Геозапросы по-новому: как покорить локальное SEO с помощью ИИ
- 2025-11-14 » Консалтинг: сущность и ключевые направления
- 2025-11-14 » Онлайн-формы: универсальный инструмент для сбора обратной связи
- 2025-11-14 » Факторы конверсии органического трафика
- 2025-11-14 » Планирование рекламного бюджета: самостоятельный подход
- 2025-11-14 » Авторизация на сайте: как выбрать решение для удержания клиентов и сохранения продаж
- 2025-11-13 » Эффективные методы стимулирования клиентов к оставлению положительных отзывов
- 2025-11-13 » Налоговая реформа — 2026: грядущие изменения для предпринимателей
- 2025-11-13 » Альтернативы мессенджерам: что выбрать вместо Telegram и WhatsApp
- 2025-11-13 » Маркировка рекламы для начинающих: полное руководство по требованиям ЕРИР
- 2025-11-13 » ИИ не отберет вашу работу — её займет специалист, владеющий искусственным интеллектом
- 2025-10-29 » Как оценить эффективность работы SEO-специалиста: практическое руководство для маркетологов и владельцев бизнеса
- 2025-10-29 » Киберспорт как маркетинговый инструмент: стратегии привлечения геймеров
- 2025-10-29 » Как говорить с аудиторией о сложном
- 2025-10-29 » Что такое доказательства с нулевым разглашением (ZKP) и их роль в блокчейне
Гораздо больше людей сдавшихся, чем побежденных. |
Мы создаем сайты, которые работают! Профессионально обслуживаем и продвигаем их , а также по всей России и ближнему зарубежью с 2006 года!
Как мы работаем
Заявка
Позвоните или оставьте заявку на сайте.
Консультация
Обсуждаем что именно Вам нужно и помогаем определить как это лучше сделать!
Договор
Заключаем договор на оказание услуг, в котором прописаны условия и обязанности обеих сторон.
Выполнение работ
Непосредственно оказание требующихся услуг и работ по вашему заданию.
Поддержка
Сдача выполненых работ, последующие корректировки и поддержка при необходимости.

Мы создаем практически любые сайты от продающих страниц до сложных, высоконагруженных и нестандартных веб приложений! Наши сайты это надежные маркетинговые инструменты для успеха Вашего бизнеса и увеличения вашей прибыли! Мы делаем красивые и максимально эффектные сайты по доступным ценам уже много лет!
Комплексный подход это не просто продвижение сайта, это целый комплекс мероприятий, который определяется целями и задачами поставленными перед сайтом и организацией, которая за этим стоит. Время однобоких методов в продвижении сайтов уже прошло, конкуренция слишком высока, чтобы была возможность расслабиться и получать \ удерживать клиентов из Интернета, просто сделав сайт и не занимаясь им...
Мы оказываем полный комплекс услуг по сопровождению сайта: информационному и техническому обслуживанию и развитию Интернет сайтов.
Контекстная реклама - это эффективный инструмент в интернет маркетинге, целью которого является увеличение продаж. Главный плюс контекстной рекламы заключается в том, что она работает избирательно.