понедельник, 23 января 2012 г.

Цепляем jQuery Datepicker c помощью кастомного биндинга на KnockoutJS

Как я говорил в предыдущей статье сегодня мы создадим свою "команду" ну или правильному custom binding (кастомный биндинг).
Что это такое ?
В knockout для того, чтобы с вашим элементом происходили какие-то махинации: скрыть, сделать неактивным, добавить стиль, присвоить значение и тп существуют команды, некоторые из которых вы уже видели в примерах - value, text, visible, ckecked, enable, options. Эти команды предопределены в фреймворке и предоставляют базовый и достаточный функционал для реализации ваших нужд.
Иногда бывают случаи когда код выглядит когда их вроде бы не хватает. Например, обавим в наши фильтры поддержку типа Date и когда пользователь выбрал поле, например Issue Date, установил курсор чтоб ввести значение, я хочу чтоб он не вводил его руками, а выбрал дату через jQuery datepicker. Естественно я хочу чтоб выбор даты срабатывал только для Date типа фильров, а для других Number и String оставался просто полем ввода.
Без своей кастомной команды, нам бы пришлось подписаться через subscribe на ColumnObj и смотреть при выборе поля из выпадающего списка его тип. Если это дата - применить datepicker, если нет - удалить datepicker. Если мы в будущем захотим добавить еще какую-то логику, то пойдут кастыли и будет все в куче. 
Тут нам как раз и поможет кастомный биндинг (команда). В итоге наш шаблон будет выглядеть так:
 
    <input data-bind="value: Value, datePicker: Type" type="text" />

Вся логика для выбора даты будет вынесена в команду datePicker. Собственно код:

ko.bindingHandlers.datePicker = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        var type = ko.utils.unwrapObservable(valueAccessor());

        if (type == 'Date') {
            $(element).datepicker();
        }

    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        var type = ko.utils.unwrapObservable(valueAccessor());

        if (type == 'Date') {
            $(element).datepicker();
        }
        else {
            $(element).datepicker("destroy");
        }
    }
}


Я не буду описывать все опции в деталях, о них выможете прочитать здесь.
Рассмотром, что и как.  Как мы видим для того чтобы создать команду, мы создаем объект с соответствующим называнием команды в ko.bindingHandlers. У нашего объекта должно быть две функции.

init - вызывается, когда knockout доходит до места в html, где установленна наша команда.
update - вызывается, когда изменится значение поля за которым мы следим с помощью нащей команды.
В нашем случае, data-bind="dataPicker: Type, value: Value" значит - следить за полем Type и если оно изменится вызвать команду datePicker. Она вызовится при инициализации и после последющих обновлений.
Что происходит в обработчиках?

valueAccessor() - возвращает поле, к которому привязана команд. Это всегда будет функция. т.к. все поля объявляются через ko.obsorvable или ko.computed и они возврашают функции.
ko.utils.unwrapObservable - это функция, которая вычисляет значение поля на такущий момент, когда вызывается эта строка.
Дальше, я думаю, все понятно. Параметры, которые передаютися в init и update очевидны, кроме allBindingsAccessor, но и пользоваться им вы врядли будете.

Ниже рабочий пример. Я добавил тип Date в Conditions, саму команду datePicker и поле Issue Date.

пятница, 13 января 2012 г.

Динамические фильтры на Javascript c помощью KnockoutJS

Сегодня я покажу практический пример, как можно применить KnockoutJS в реальном проекте и сделаем мы динамические фильтры для поиска задач, багов, тасков и тп по разработчику, названию таска и описанию. Возможно вы такие уже видели в TFS для генерации отчета или других системах, где можно добавлять любые фильтры и условия для поиска чего-либо. В итоге выглядит это так:



В конце мы добавим валидацию, чтоб нельзя было добавлять фильтр, если текущий пустой, а в следующей статье я напишу о том как можно писать плагины и свои команды в knockout и добавит фильтр по дате.

Приступим!

Сначала  определимся, что будет представлять из себя сам фильтр:


Column - колонка по кторой фильтруем или ищем
Condition - оператор для фильтра =, != и тп
Value - значение фильтра
Type - тип. Пока что у нас будет только String, Number потом расширим
NextFilterBinaryCondition - это условие с которым будет выполняться следующий фильтр "И", "ИЛИ". Например Developer = "Vitaly" and Issue = "write post" or Issue Description = "knockout".

Фильтры у нас будуте создаваться по названию поля по которому мы фильтруем. Колонки у нас хранятся как объекты. При создании фильтра мы сохраним название поля и найдем в списке объект колонку с помощью вспомогательной функции findFilterColumn и будем хранить обьъект колонка в поле ColumnObj. 
Если поле ColumnObj изменится, мы очищаем значение фильтра. Чтоб следить за изменениями поля ColumnObj мы подписуемся функцией subscribe.
Функцию-оброботчик, которыю мы передаем в subscribe, будет вызываться всегда, когда значение поля, на которое мы подписываемся, изменится.
Все поля, кроме ColumnObj, Condition, Value, NextFilterBinaryCondition  зависымые и значения мы не присваиваем им явно. Значения для этих полей пересчитываются при любом изменения одного из обычных полей. Это одна из прелестей knockout! Если смотреть сверху вниз то экосистема knockout будет вызывать обработчики в следующем порядке:
1. Присвоить значение в ColumnObj
2. Определеить поле Column как зависимое и попробовать просчитать.
3. Присвоить значение полю Condition.
4. Пересчитать Column
5. Присвоить значение полю NextFilterBinaryCondition.
6. Пересчитать Column.
7. Определеить поле Type как зависимое и просчитать.
8. Пересчитать Column.
9. Определеить поле AvailableConditions как зависимое и просчитать.
10. Пересчитать Type.
11. Пересчитать Column.
12. Установить значние поля Value и пересчитать Column, Type, AvailableConditions.

Все поля устанавливаются и пересчитываются по порядку в котором они были объявлены.

Теперь рассмотрит класс, который управляет всей бизнес логикой, хранит фильтры и он является и называется ViewModel

Объект Conditions содержит список типов фильтров и соответсвенно операций, условий, которые мы поодерживаем для этих типов данных. Также есть вспомогательная функция для поиска доступных условий по названию типа.

Например найти все доступные условия для фильтрации по типо 'Number'. Этой функцией мы будем пользоваться когда после выбора поля в первом списке, нам нужно будет отобразаить доступные условия во втором выпадающем списке.

Небольшое ТЗ. У нас должны выполнятся следующие требования:
- хранить фильтры
- добавлять фильтры
- удалять фильтры
- посылать все фильтры на сервер

При создании ViewModel мы передаем в него список колонок, которы объявлены выше. В конструкторе мы создаем массив фильтров и сразу добавляем один пустой фильтр. Если этого не сделать, то у нас на экране будет одна кнопка. Как только мы добавляем пустой фильтр, экосистема knockout трекает это, уведомляет всех подписчиков на изменения, если есть такие, и отображает пустой добавленные фильтр по шаблону и определенной для него бизнес логике.

Функция addFilter принимает объект фильтр, напротив которого мы клацнули "+", находит этот фильтр в списке уже существующих фильтров и добавляет после него новый пустой.
В данном случае у нас нет проверки, заполнены ли все фильтры или нет. Сейчас мы можем нажимать "+" бесконечно и у нас будет куча фильтров. Мы это исправим потом.

Функция removeFilter принимает объект фильтры который надо удалить, удаляет его из списка фильтров и если это был последний фильтр мы добавляем пустой. Зачем? Если мы этого не сделаем, то мы не сможем добавить фильтры, т.к. "+" отображается на строке с фильтром и без него у нас останется только кнопка submit.

Функция postFilters показывает сообщение с сериализованными фильтрами в JSON, которые надо послать на сервер для обработки. В knockout есть много утилитных функций о которых мы можете почитать подробней здесь.

Функция toJS, создает новый js объект, который не связан больше с knockout, это просто js объект. Все свойства в нем будут вычеслены с теми значениями, которые были на момент вызова функции toJS.

Когда мы сериализовали фильтры в js objects, то там будут поля, которые нам нужны и которые были нужны только для knockout, например ColumnObj, AvailableConditions. Эти поля не несут нам никакой информации на сервере, поэтому мы их смело удаляем и на сервер пойдет меньше данных. Функция toJSON сериализует js класс в JSON строку, которую мы и показываем.

Переходим к рассмотрению шаблона html:

Фильтры отображаются по шаблону в данном случае шаблон, это li и все что внутри него.
В предыдущих версия knockout обязательно надо было подключать jQuery Teample. В последней версии этого делать больше не нужно, теперь в фреймворке есть свой встроеный движок для шаблонов со своими плюшками. О всех новшествах новой версии 2.0 можно прочитать на блоге самого разработчика здесь.
Из нового  я использую переменную $root, которая ссылается на ViewModel класс. Если этого не сделать, то, например, поле columns будет искаться в классе Filter. Команда foreach пробегает по всем объектам коллекции filters и применяет шаблон для каждого объекта Filter. Шаблон выполняется с контекстом объекта, который передается в шаблон (Filter).
Переменная $data хранит ссылку на объект контекста (Filter в данном случае). Этой переменной я воспользовался, когда добавлял или удалял фильтр.
По умолчанию функции вызываются в контексте объекта в шаблоне (Filter), для этого в новой версии не нужно писать function () { ... }, есть более красиывый синтаксис например click: removeFilter($data). Но нам надо выполнять функции в контексте ViewModel поэтому мы пишем по старому, через function и выполняем функцию в контексте, котором нам надо.

Вроде бы все.

Теперь, как я говорил, добавим проверку. Если у нас хотя бы один фильтр без значения, то мы не будем добавлять новый.
Обновленный класс ViewModel выглядит теперь так:

а фильтр так:


и собственно результат:


В следующей статье я напишу как писать расширения для knockout и мы сделаем фильтр по дате. Если у нас поле будет с типом Date, то к нему будет автоматически применяться jQuery date picker.

Вопросы, коментарии преветсвуются!

четверг, 22 декабря 2011 г.

Отображение картинки из базы без запроса на сервер в MVС


Я искал альтернативный способ как можно отобразить картинку на странице не делая запроса на сервер, который отдает контент изображения. В основном, у всех вывод картинки на страницу выглядит так:


если она у вас хранится на диске то первый варинат, если в базе то второй вариант. В обоих случая браузер посылает GET запрос на картинку. Если мы отображаем 10 записей, мы должны сделать 10 запросов чтоб получить картинки. А если мы делаем фотоальбом, отображаем штук 30 привьюх и храним картинки в базе, то грузиться будет дольше.

Решение которое я покажу называется inline image. Суть в том, что вы не делаете лишний запрос за картинкой, вы сразу встраиваете картинку в браузер. Выглядит это так


В src просто пишется контет, закодированый через base64. Так выглядит мой хелпер, который может генерит такую картинку.

 public static class ImageHelper
    {
        public static MvcHtmlString Image(this HtmlHelper html, byte[] imageBytes, string mime, object attributes)
         {
             TagBuilder img = new TagBuilder("img");

             StringBuilder sb = new StringBuilder();
             sb.AppendFormat("data:{0};", mime);
             sb.AppendFormat("base64,{0}", Convert.ToBase64String(imageBytes));

             img.MergeAttribute("src", sb.ToString());

             return MvcHtmlString.Create(img.ToString(TagRenderMode.SelfClosing));
         }
    }

    // пример использование
    @Html.Image(Model.ToArray(), "image/jpg", new { width = 530, height = 390 }) 


Конечно для картинок которые весяк около 30kb, src будет намного больше. Например чтоб отобразить такой значок в 35kb
у меня получилось 

токо шире в 4 раза :)


Итог

Преимущества данного решения в том, что мы избавляемся от лишнего запроса на сервер и если размер картинки не велик, например, до 10 кб, то данное решение будет оптимально для отображения кучи картинок, если они у вас хранятся в байтах.
Конечно у него есть и недостатки: что мне очень не нравится, то что картинки не кэшируются :/ (только если всунуть data в css) и эта возможность не поддерживается некоторыми версиями браузеров.

В общем это было алтернативное решение, которое имеет место жить в частных случаях. Часто такое можно увидеть в css если посмотреть сайты yahoo, youtube, amazon и тп.


вторник, 20 декабря 2011 г.

Настройка проекта для прекомпиляции cshtml файлов в ASP MVC 3

Если вы писали на ASP MVC 3, то наверно сталкивались с проблемой, что ошибки во вью файлах не отображаются при компиляции проекта, их нет в Error List. Об ошибках вы можете узнать только если переклацаете весь проект :) Это происходит потому, что Razor вьюхи не компилируются когда вы билдите проект в студии.
Можно сделать, чтоб вьюхи компилировались и ошибки во вьюхах отображались в Error List.

Механика:
 Открываем файл проекта в блокноте, находим в первой Project Group секции тег MvcBuildViews и меняем значение c false на true и воаля!.




воскресенье, 11 декабря 2011 г.

Что такое Knockout JS или MVVM на javascript. Пример

Давно хотел написать что-то про Knockout JS фреймворк. Если кто-то пишет сайты с интерфейсом где куча javascript, то вы найдете его для себя очень полезным, а потом не сможете без него ничего делать, т.к. к хорошему быстро привыкаешь.

Что это такое и зачем надо ?

Рассмотрим пример, сразу говорю выдумал на ходу, но он отображает суть.
Вы можете купить продукт из списка, завернуть в подарочную упаковку,  указать надпись на коробке выбрать доставку и сразу видеть цену, которая меняется от выбранных опций и Summary, которое отображает выбранные опции как результат.
При нажатии на 'Sumbit' отображает JSON, которы может поститься на сервер или тп.



Сколько бы вы делали этот пример просто на JS + jQuery ? Конечно, вы можете сказать "та нехер делать", но код будет выглядеть месивом из подписывания на события, if'ы для треканья зависимостей между элементами, что показать, какой скрыть, потом будете еще дэбажить, а потом начнете переосмысливать жизненные ценности и позавете Darth Vadera, чтоб он все разрулил :).

На входе

- Набор элементов формы (View)
- Данные, которые отражаются и отображаются в этих элементах (Model) и  бизнес логика, которая скрывает, отображает элементы и делает расчеты (Controller или ViewModel).

На выходе
- Нужно как-то это скомпоновать, чтоб зависимости и пересчеты резолвись сами, если какое-то значение изменится и никаких явных привязок к событиям через jQuery.

Итого, как мы видим, получается MVC, но более точно это MVVM, шаблон группы MVx, который был придуман под технологию WPF и очень хорошо укладывается с возможностями этой технологии.
В WPF это реализуется с помощью binding ("биндинга") и класса (ViewModel) с dependency property и routed events.

Наш ViewModel и Dependancy Property

 Как мы видим у нас есть свойства, которые создаются с помощью observable. Так экоститема knockout знает, что если значение этого свойство набиндить на элемент, например, Address то это значение отобразится в текстовом поле. Если пользователь изменит значение текстового поля, новое значение запишется обратно в свойство (Address), с которым связан элемент через observable. Получается двухсторонняя связь. Можно сделать и в одну сторону, это потом.

Наш Binding 



У нас есть один атрибут datа-bind, в которм мы указываем с каким свойством (значением) связывать этот элемент и дополнительные опции. Для разных элементов свойства привязываются по разному, но в общем значение datа-bind начинается так - "value: Property"

Рассмотрим по порядку:

select data-bind="options: Products, value: Product" - это значит, что мы биндим список продуктов на выпадающий список и если выбранный продукт изменится, это значение запишется в свойство Product. Если бы мы указали viewModel.Product = ko.observable("Jeans") то сразу выбраными были бы "Jeans".
input type="radio" data-bind="checked: GiftWrap" - это значит, что мы биндим value радиокнопки на свойство GiftWrap. Какая радиокнопка выбрана - такое значение и будет "Yes" или "No". Ничего не выбранно - значит ничего не будет, null.
div data-bind="visible: GiftWrap() == 'Yes'" - тут начинается интересное. С помощью visible мы решаем будет ли элемент отображатся по условию после двоеточия. Если мы поставим радиокнопку в Yes, будет div style="display: none", в другом случае div style="display: block". Как это работает ? Как только одно из observable свойств меняется, экосистема knockout проверяет весь биндинг и пересчитыват условия в атрибутах.
С простыми свойствами я думаю несложно. Давайте рассмотрим этот кусок
и код

Если бы у нас было observable, мы бы не смогли посчитать Total, т. к. оно зависит от других значений свойств. Для этого мы используем новый тип свойства dependentObservable, который обозначает, что это свойство считается динамически. Сколько свойств во ViewModel изменится, столько раз knockout пересчитает свойство Total. Нам остается только отобразить это свойство в span data-bind="text: Total". Это все работает автоматически !!! Достаточно только поставить нужное значение и опции в атрибут data-bind. Точно также работает Summary.
Я не буду дальше описывать в деталях синтаксис и тп, на сайте knockout очень хорошо все расписано с отличными примерами. Моя цель была заинтересовать и показать, как можно избавиться от кучи ненужной привязки к событиям через jQuery, месива, рассредоточеной бизнес логики и других "прелестей" при работе с сложным UI и JS.
Я считают, что этот фреймворк должен быть на счету у каждого серъезного разработчика.

Что может этот фреймворк:
- простой биндинг, поле-свойство
- сложный биндинг, динамическое значение поля
- можете писать свои настройки типо как value, checked и тп.
- можете использовать jTemplate
- можете использовать ajax
- можете биндить события, типо click, chenched и тп
- можете биндить не только на значение элемента, но и например явно на атрибуты или стили, например если вы хотите чтоб в style свойство color менялось если сумма больше $20 и тп.
- можете делать все что угодно, если мой пример отображает суть вашей проблемы.

Мне очень нравится этот фреймворк. Читаешь доку, смотришь код и все как-то интуитивно понятно, запоминай только опции в атрибутах, а так работать одно удовольствие. Уже не раз сокращал старый код в раза полтора-два точно. Но это не главное, а главное что бизнес логика в одном месте, а вьюха в другом.
Есть еще подобный фреймворк backbones, но мне он показался ужасным и более низкоуровневым, там надо много писать руками, очен много, но у него есть свои преимущества.

Если вам интересна данная тема и фреймворк - напиши в коментах ваши пожелания или мысли, стоит ли мне развивать эту тему и писать другие примеры, реальные, которые я применял в проектах.
Отвечу на любый вопросы.