volgaboatman

пятница, 13 марта 2009 г.

Команды и MVVM (приложение 2)

Во втором случае нам надо явно задавать имя команды в XAML. Выглядит так
  1.     <UserControl.CommandBindings>
  2.         <CommandBinding:NamedCommandBinding Command="ApplicationCommands.Save" CommandName="Save" />
  3.     </UserControl.CommandBindings>
* This source code was highlighted with Source Code Highlighter.


А в презентере надо пометить методы для CanExecute и Execute соответствующими атрибутами. Выглядит вот так

  1.     public class BindToNamedCommandVM : INotifyPropertyChanged
  2.     {
  3.         [NamedCanExecute("Save")]
  4.         public bool SaveCanExecute(object obj)
  5.         {
  6.             return CanExecute;
  7.         }
  8.  
  9.         [NamedExecute("Save")]
  10.         public void SaveExecute(object obj)
  11.         {
  12.             LogItems.Add(Sum.ToString());
  13.             Sum = 0;
  14.         }
  15.         // Ну и что там еще у него есть
  16.     }
* This source code was highlighted with Source Code Highlighter.


С одной стороны явный минус в том, что появляются строки, в которых можно допустить ошибку. Но с другой стороны в презентере получается меньше кода (не надо заводить команду), и чисто теоретически можно объединить CanExecute для нескольких команд в один метод (просто навесив на него несколько атрибутов).

Команды и MVVM (приложение 1)

Решил все-таки поподробнее расписать варианты как все будет выглядеть при использовании.

При использовании связывания через команду (BindToICommand в примере).

В xaml просто прописываем свой биндинг:
  1.     <UserControl.CommandBindings>
  2.         <CommandBinding:DataContextCommandBinding Command="ApplicationCommands.Save" />
  3.     </UserControl.CommandBindings>
* This source code was highlighted with Source Code Highlighter.

Соотв. задается только DataContextCommandBinding должен будет в презентере (что в данном случае является синонимом DataContext) найти команду, обслуживающую ApllicationCommand.Save.

В презентере все выглядит примерно так

  1.   public class BindToICommandVM : INotifyPropertyChanged
  2.   {
  3.     public DataContextCommand Save { get; set; }
  4.  
  5.     public BindToICommandVM()
  6.     {
  7.       // Какие-то действия в конструкторе
  8.       Save = new DataContextCommand
  9.             {
  10.               BoundCommand = ApplicationCommands.Save,
  11.               CanExecuteDelegate = x => CanExecute,
  12.               ExecuteDelegate = x =>
  13.                          {
  14.                            LogItems.Add(Text);
  15.                            Text = "";
  16.                          }
  17.             };
  18.     }
  19.     // Ну и что там еще у него есть
  20.   }
* This source code was highlighted with Source Code Highlighter.

Т.е. на каждую команду надо заводить свойство типа DataContextCommand и в конструкторе (или где-нибудь еще на раннем этапе) прописывать ему делегаты для Execute и CanExecute и командую, по которой DataContextCommandBinding поймет что это реализация именно той команды, которой ему надо. Естественно можно использовать и много команд в презентере и несколько DataContextCommandBinding.


четверг, 12 марта 2009 г.

Команды и MVVM

Давно не писал. Но время оно не резиновое...

За это время пример, описанный ранее (с биндингом FlowDocument) практически без доработок напильником был встроен в рабочий проект и работает.

Но сегодня про другое. В WPF есть классная штука, под названием RoutedUICommand. Если сильно упростить - то с ее помощью можно указывать какую команду будет выполнять кнопка или пункт меню. При этом сама реализации команды будет жить где-то отдельно. Причем для разных элементов управления может быть разная реализация (например команда "Сохранить" может сохранять как документ, так и какой-нибудь элемент справочника). Более того, в нужный момент WPF проверяет а можно ли вообще выполнит команду и если нельзя, то дизаблит кнопку или что у нас там к команде привязано. В общем не буду долго расписывать что это такое, кому интересно может в журнале почитать.

Однако как всегда не все так хорошо как хотелось. В простом примере все шоколадно. Но как только мы начинаем все усложнять то нам сразу хочется тестов. А для такого случая умные дядьки придумали MVVM (опять так есть гугл и журнал). В кратце смысл в том, что на форме у нас вообще никакого кода нет, а вся логика живет в View-модели, на которую настроены биндинги и которая разными шаманскими методами (самый простой - тупо назначить DataContext) подсовывается форме. А так как вся логика получсется живет в обычном классе (который о форме вообще ничего не знает), то его и тестировать можно очень легко.

И вот как раз на перекрестии двух очень интересных идей как обычно и живет засада. Заключается она в том, что команды привязываются через CommandBinding. А он как обычно не является DependencyObject и соответственно никакие его свойства мы не можем использовать в биндинге. Хотя как-бы и логично, потому что привязка в оригинале задумывалась как привязка к данным, а тут медоды... И соотв. нельзя написать что-то типа

  1. <UserControl.CommandBindings>
  2. <CommandBinding Command="ApplicationCommands.Save" Execute="{Binding SaveExecute}"/>
  3. UserControl.CommandBindings>
* This source code was highlighted with Source Code Highlighter.
(Идея не моя, где-то в блогах подсмотрел).

Получается что единственный более-менее стандартный метод - это описывать обработчики команд в коде формы (или UserControl-а, в общем в xaml.cs) и их этих обработчиков звать методы презентера. Что конечно можно, но не красиво.

Первым желанием что-либо сделать было создать универсальный обработчик команд, который через Reflection каждый раз лез бы в DataContext, искал там метод по атрибуту или еще как-нибудь и вызывал его. Однако посмотрев на частоту, с которой вызывается CanExecute возникли подозрения что тормозить будет безбожно... Немного почесав репу пришло в голову примерно следующее решение: так как презентер на самом деле меняется редко (чаще всего вообще назначается один раз при создании формы) то можно каждый раз отслеживать поменялся ли он, и лезть Reflection-ом только когда поменялся. А если ничего не менялось - то вызывать заранее запомненные (или созданные) делегаты. Такая вот эмуляция DataBinding-а.

Осталось определиться с тем, как связывать команды. Так как есть стандартный CommandBinding, то его и будем расширять. Вопрос только, как искать методы, исполняющие команду. Пока в голову пришло 2 варианта
  1. В презентере создавать экземпляр ICommand, и просто делегировать методы ему. При этом так как в презентере может быть много команд, то каким-то образом надо выбрать какая из них соответствует какому CommandBindig-у. Пока в голову пришло все команды в презентере делать определенным классом (DataContextCommand) у которого есть свойство, определяющее какую команду обслуживаем. Как это выглядит при использовании можно посмотреть тут.
  2. Пометить методы для CanExecute и Execute атрибутами. При этом в CommandBinding-е указать имя команды (к сожалению никак, кроме как строками не получается) и в атрибуте тоже указать имя команды. Как это выглядит при использовании можно посмотреть тут.
Долго расписывать реализацию не буду, вроде как не сильно сложно. Полный пример (пока не нашел куда положить, выложил как и раньше на iFile.ru). Состоит из 3-х частей:
  • StdCommand - почти целиком копия из примера для статьи в msdn magazine. Просто чтобы продемонстрировать как оно работает в простом случае.
  • BindToICommand - реализация первого способа.
  • NamedBind - реализация второго способа.
На самом деле в примере есть еще демонстрация некоторых хитростей, но их опишу позже.

вторник, 23 декабря 2008 г.

Копаем дальше

Собственно практически все уже получилось. Правда не идеально, но работает.

Как оказалось DataBinding обновляется не сразу. А только когда пройдет цикл обработки событий (каких не вникал если честно). Выражается это в том, что при экспорте в XPS не вычисляются все свойства на которые есть Binding. Пока не загрузишь этот документ в FlowDocumentReader (ну он его естесвенно на экране отобразит, и когда начнет ждать нажатия кнопок, вот тут событя и пробегают). Причем в отличие от старых добрых средсв разработки, где можно было сказать Apllication.ProcessMessages :) в WPF приложениях пнуть цикл сообщений вручную довольно интересная задача. Решение нашлось не сразу (надо еще знать какие ключевые слова спросить у гугла), но все-таки нашлось. Описано вот тут: http://blog.fohjin.com/2008/07/saving-wpf-xaml-flowdocument-to-xps.html. Насколько я понял вкратце надо выполнить через Invoke (т.е. дернуть и подождать) пустой метод у Dispatcher-а с самым низким приоритетом. А так как приоритет самый низкий, то сначала отработают все "сообщения" (хотя наверно некорректное название) с более высоким приоритетом и таки вычислят все Binding-выражения.

Интересный подход используется и при печати заголовков-подвалов страниц (естественно не сам придумал, честно передрал и рихтанул напильником отсюда). А именно, делается наследник от ReportPaginator, который резервирует место под загловки (устанавливая размер страницы для оригинального Paginator-а на размер страницы минус размер заголовка). И при формировании каждой страницы складывает ее из 3-х частей (заголовок, сама страница, подвал).

Немного прояснена засада с тем, что в DataTemplate не получается положить ContentElement. На самом деле ругается дизайнер студии. Но при этом все компилируется и работает. А так как дизайнер студии все равно не отрабатывает корректно DataBinding, то и фиг с ним. Так что от промежуточного класса избавились.

Файл с исходниками выложил пока на ifolder. Там есть пример, вроде даже работает. Мог конечно ошибиться где-нибудь с размерами, но вроде похоже на правду. Так как времени сейчас особо много нет, то подробно расписывать что там и как не буду, кому интересно пока предлагаю скачать и посмотреть. Там 18 килобайт.

Осталось 2 неприятные вещи.

1. Пока HeaderHeight и FooterHeight задается вручную. Для эксперимента пойдет, но надо будет все-таки сделать возможность вычислять высоту HederTemplate автоматом.

2. Не получилось придумать нормального места для PageNumber и PageCount. Подставлять их через шаблон вроде как идеологически не очень правильно (хотя и самое простое). А привязать к самому отчету с наскоку не получилось. Поэтому пока работает по следующему алгоритму: У DataContext-а отчета (который по идеологии DM-V-VM является просто обычным объектом) через Reflection ищутся свойства PageNumber и PageCount. И если нашлись - то им присваиваются соотв. значения. Ну а в самом отчете на них можно забиндится. В принципе работает, единственное неудобсвто, что надо заводить 2 дополнительных свойсва, которые будут слать NotifyPropertyChanged.

суббота, 20 декабря 2008 г.

Небольшие дополнения и непонятки

Вчера, когда публиковал предыдущую заметку, наткнулся еще на одно описание решения аналогичной проблемы (http://pieceofsummer.livejournal.com/72317.html). Там почти тоже самое, но немного по другому. Кроме очевидных ItemTemplate и ItemsSource используются еще DataTemplateSelector и GroupStyles. Пока не очень понятно практическое применение, но может кому и потребуется. Плюс к тому за базовый класс выбран Section, что может быть когда-нибудь кому-нибудь и понадобится, но в моем случае пока не используется. В общем надо иметь в виду, может и пригодится.

Зато вроде как на первый взгляд используется аналогичный механизм, просто устанавливающий DataContext. Может это и есть правильный способ.

Но самое интересное, что там внутри DataTemplate используется напрямую Paragraph. А у меня так не получается хоть убей. Property 'VisualTree' does not support values of type 'Paragraph' и все...

пятница, 19 декабря 2008 г.

Отчеты и DataBinding

Когда начинал копаться в WPF все выглядело красиво и хорошо. И на экране все красиво (ну если не считать проблем с шрифтами по XP), и отчеты вроде можно делать, и печатать их через XPS. Но вот наконец дело дошло до реализации отчетов. Я конечно предполагал, что есть засады, но такой подлянки не ожиал. Оказывается DataBinding (собсвенно самая интересная особенность в WPF) не работает во Flow-документах. Т.е. на экран вывести красивый список указав шаблон и подсунув свой список объектов - пожалуйста, а вот напечатать с применение такого подхода - фиг вам. Везде на выбор предлагается 2 варианта

  1. Генерить отчет в коде. Ага, сейчас. Последний раз я это делал в 94-м году на клиппере. А сейчас все-таки 21-й век, опять-таки избаловались всякими FastReport-ами и иже с ними.
  2. Генерить статический XAML по шаблону. Например как вот здесь: http://janrep.blog.codeplant.net/WPF-Multipage-Reports--Part-IV--Pagination.aspx. Уже конечно интереснее, но все равно попахивает прошлым веком :)
Мучения гугла дали направление действий. Оказывается реализовать DataBinding все-таки можно. Просто дяденьки в микрософте почему-то забыли сделать свойство Text и элемента Run Dependency-объектом. Это все описано у Filipe Fortes. Еще бы они не забыли, кто же будет убивать корову, несущую золотые яйца в виде кучи сторонних генераторов отчетов. Хотя может быть на это есть и не только религиозные, но и технические соображения. Однако простенький эксперимент показывает, что вроде как оно на самом деле не так сложно сделать, и вроде на первый взгляд работает.

Итак, описание маленького эксперимента. Совсем маленького и простенького, поэтому просьба сильно не пинать.

Так как очевидно, что самое частое применение подхода с ItemsTemplate и ItemSource - это отобразить какие-нибудь табличные данные, то в эту сторону и смотрим. Подходящим кандидатом для надругательств является TableRowGroup. От него и будем плясать.
  • Делаем наследника от TableRowGroup
  • Приделываем к нему 2 Dependency-свойства
public DataTemplate ItemsTemplate
public IEnumerable ItemSource


Что это такое и код целиком пока приводить не буду. Вот разберусь как в блог исходники залить - тогда и качайте :)

Первая засада возникает в том, что у меня с первого раза не получилось в DataTemplate положить TableRow. Ругается, что "Property 'VisualTree' does not support values of type 'TableRow'". Что в общем странно. Пока обошел созданием наследника от FrameworkElement, у кототорого есть только одно свойство:

public TableRow Row


Из-за этого конечно получается довольно большой XAML, но для эксперимента пойдет.

Дальше собсвенно все просто. При установке любого из свойств (либо ItemsTemplate, либо ItemSource) просто перегенерируем заново содержимое TableRowGroup. Примерно следующим образом



void Refresh()
{
if (Rows == null) return;
if (ItemSource == null) return;

Rows.Clear();
foreach (var obj in ItemSource)
{
var content = ItemsTemplate.LoadContent();
if (content is RowTemplate)
{
var rt = (RowTemplate)content;
rt.Row.DataContext = obj;
Rows.Add(rt.Row);
}
}
}


На самом деле код скорее всего не идеолгически правильный, но на скорую руку не нашел как правильно разворачивать ItemsTemplate, а в исходники ItemsControls-а лезть тоже лениво.

Собсвенно этого достаточно. Теперь можно писать примерно вот так



<table>
<table.columns>
<tablecolumn width="160">
<tablecolumn width="80">
<tablecolumn width="80">
</Table.Columns>
<tablerowgroup>
<tablerow> <!-- Тут у нас заголовок таблицы -->
<tablecell borderthickness="1" borderbrush="Black">
<paragraph>Наименование</paragraph>
</tablecell >
<tablecell borderthickness="1" borderbrush="Black">
<paragraph>Цена</paragraph>
</tablecell >
<tablecell borderthickness="1" borderbrush="Black">
<paragraph>Кол-во</paragraph>
</tablecell>
</tablerow>
</tablerowgroup>

<my:templatedtablerowgroup name="MyRow">
<my:templatedtablerowgroup.itemstemplate>
<datatemplate>
<my:rowtemplate>
<my:rowtemplate.row>
<tablerow>
<tablecell>
<paragraph borderthickness="1" borderbrush="Gray">
<my:bindablerun
boundtext="{Binding Name}">
</paragraph>
</tablecell >
<tablecell>
<paragraph borderthickness="1" borderbrush="Gray">
<my:bindablerun
boundtext="{Binding Price}">
</paragraph>
</tablecell >
<tablecell>
<paragraph borderthickness="1" borderbrush="Gray">
<my:bindablerun
boundtext="{Binding Qty}">
</paragraph>
</tablecell>
</tablerow>
</my:RowTemplate.Row>
</my:RowTemplate>
</datatemplate>
</my:TemplatedTableRowGroup.ItemsTemplate>
</my:TemplatedTableRowGroup>
</table>



Осталось выяснить несколько вопросов
  1. Разобраться почему TableRow не может быть элементом в DataTemplate.
  2. Идеологически правильно сделать Refresh, что-то мне подсказывает что биндинг надо по другому настраивать :)
  3. Разобраться как в блогах постить исходные коды, а то запарился вручную <pre> ставить и заменять больше-меньше на lt-gt.
  4. Разобраться куда бы положить архив с исходниками, если кому будет интересно. А думал его к блогу можно приделать, но либо не разобрался как, либо оно умеет только картинки и видео в блог вставлять. А заводить отдельный сайт лениво, потому как никогда не делал, надо думать какой лучше (на корпоративный вроде как некрасиво, на narod.ru стыдно :) ).

Предистория

Чукча не писатель, чукча читатель. Пробуем переориентироваться...

Всю долгую жизнь сидели, писали на Delphi. Но жизнь не стоит на месте, надо же куда-то развиваться. Как-то уже давно посмотрел что придумали умные люди на .NET. Когда был .NET 1.0 впечатление что в принципе тоже самое, но тормознее. NET 2.0 уже побыстрее. Потом придумали 3.5, в частности WPF. Посмотрели, с виду прикольно. На первый взгляд вроде как запихнули dfm в xml и радуются. Но при более подробном взгяде оказалось что все намного интереснее. В общем решили копать.

Как всегда сначала было: ура, форма в xml, можно динамически нагенерить XAML и радоваться. Потом присмотрелись, прочувсвовали DataBinding - и понравилось совсем.

В общем лениво расписывать как все интересно, перейдем к более содержательной части.

Постоянные читатели