Saturday, January 24, 2015

Using Actions in Caliburn Micro

Сегодня мы с вами продолжим тему об использовании Caliburn Micro в своих приложениях на WPF. На сайте http://caliburnmicro.com/ можно найти много начальной информации для того, чтобы начать работу с данным фреймворком, но каких-либо серьезных примеров там, к сожалению, нет. Но не нужно отчаиваться, так как Caliburn Micro достаточно простой фреймворк в освоении, вы сможете практически сразу после прочтения начальных глав писать полнофункциональные простые примеры. Понятное дело, чтобы написать что-то посложнее, вам нужно будет потратить много времени, чтобы разобраться, как использовать Event Aggregator, ServiceLocator; как использовать, например, сторонний IoC контейнер, а не Simple IoC контейнер, который идет в Caliburn Micro. Вы не найдете глубокого описания всех этих тем в документации. Наверное, самый простой путь − это скачать сразу исходники с примерами с github и разбираться с ними. Как показывает практика, этот способ работает практически всегда. Лично я после прочтения какой-то новой темы смотрю, есть ли примеры, на которых можно закрепить изучаемый материал.
Сегодня мы рассмотрим, как работает связывание модели представления с представлением. Начнем с самого простого, а закончим более сложным. В предыдущей статье я уже рассказывал немного о том, как работает байндинг в Caliburn Micro. Сегодня же закрепим наши знания по связыванию данных и напишем по возможности несколько примеров, которые будут нам нужны для того, чтобы продемонстрировать, как это работает. 
Для начала создадим WPF Application проект, который назовем "CaliburnMicroBindingSample".
Следующим делом необходимо добавить себе в проект с помощью Manage NuGet Packages ссылку на данный фреймворк.
Пока что все должно быть предельно просто. Затем разбиваем нашу структуру проекта на такое количество папок:
Такая структура папок создается для того, чтобы не путаться в том, какая часть с паттерна MVVM где хранится, так как модели хранятся в папке Models, модели представления − в ViewModels, а сами представления − папке Views. В нашем примере уже создано по умолчанию окно MainWindow, поэтому рекомендую вам пойти не по принципу MVF (model –  viewfirst ) как основному подходу при программировании с использованием Caliburn Micro, а начать с модели как таковой, поскольку она у нас уже есть. Точнее, сделать из нее сразу заготовку и лишь затем наполнить ее в xaml коде нужными нам контролами и поведением. Для этого нам нужно наше окно перенести в папку Views и переименовать на ShellView. Сразу же, чтобы не забыть, удалим ссылку на это окно при старте приложения; для этого переходим в App.xaml и удаляем свойство StartupUri. Теперь у нас есть окно, но нет на него никаких ссылок. Это очень хорошо, потому что загрузкой окна и заданием начальных настроек в Caliburn Micro занимается загрузчик (класс, который наследуется от BootstrapperBase).
Небольшой интересный факт по поводу Caliburn Micro заключается в том, что логика данного фреймворка в основном строится вокруг библиотеки System.Windows.Interactivity.dll. Как она используется, мы сегодня и разберем.
Затем начнем с реализации модели представления, которая будет связана с данным представлением. Для этого мы должны не забывать об одном правиле, что в Caliburn Micro для того, чтобы у вас нормально работала автоматическая привязка ViewModel с View, нам нужно, чтобы окончание модели представления заканчивалось приставкой "ViewModel". То есть, если у вас представление называется ShellView, то модель представления должна иметь название ShellViewModel. Поэтому так как мы уже знаем, что нашу модель представления нужно назвать ShellViewModel (об этом можно сделать вывод с предыдущего предложения), мы ее так и назовем и добавим ее в папку ViewModels. Реализация данной модели приведена ниже.
public class ShellViewModel : PropertyChangedBase
{
}
Наследоваться от PropertyChangeBase не обязательно на данном этапе. Достаточно просто создать пустой класс и сделать его public, для того чтобы проверить, что у нас окно нормально запустится.
Теперь осталось реализовать наш загрузчик. Назовем его ShellBootstrapper, наследуемся от класса BootstrapperBase и добавим его в корень проекта.
public class ShellBootstrapper : BootstrapperBase
{
    public ShellBootstrapper()
    {
        Initialize();
    }

    protected override void OnStartup(object sender, StartupEventArgs e)
    {
        DisplayRootViewFor<ShellViewModel>();
    }
}
Теперь осталось добавить создание данного класса в App.xaml. Для этого перейдем в файл App.xaml и модифицируем его следующим образом:
<Application x:Class="CaliburnMicroBindingSample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:caliburnMicroBindingSample="clr-namespace:CaliburnMicroBindingSample">
    <Application.Resources>
      <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
          <ResourceDictionary>
            <caliburnMicroBindingSample:ShellBootstrapper x:Key="Bootstrapper" />
          </ResourceDictionary>
        </ResourceDictionary.MergedDictionaries>
      </ResourceDictionary>
    </Application.Resources>
</Application>
Теперь вы можете запустить ваш проект и посмотреть, что все получилось. Вы увидите пустое окно. Если же вы увидели следующее сообщение:
значит, Caliburn Micro не смог связать вашу модель представления с самим представлением. Такая проблема  может возникнуть, если ваша модель не смогла найтись из-за пространства имен, либо вы ошиблись в имени модели представления. У меня эта проблема была в том, что пришлось указать для модели представления пространство имен базового уровня.
namespace CaliburnMicroBindingSample
Только после этого все заработало. Как видите, автоматическое связывание может иметь свои проблемы. Если же мы настроили все верно, то увидим на экране нормально окно.
Давайте теперь слегка модифицируем наше окно и добавим в него текстовое поле для ввода имени и кнопку, которая покажет это имя на экране. Как в примере All About Actions, который приведен в документации.
<Window x:Class="CaliburnMicroBindingSample.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
      <StackPanel Orientation="Vertical">
        <TextBox x:Name="FirstName" />
        <Button x:Name="ShowFirstName" Content="Show Message" VerticalAlignment="Center" HorizontalAlignment="Center"/>
      </StackPanel>
    </Grid>
</Window>
И небольшие модификации в самой модели представления.
public class ShellViewModel : PropertyChangedBase
{
    private string _firstName;

    public string FirstName
    {
        get { return _firstName; }
        set
        {
            _firstName = value;
            NotifyOfPropertyChange();
        }
    }

    public void ShowFirstName()
    {
        MessageBox.Show(string.Format("Hello {0}", FirstName));
    }
}
Если мы запустим наш проект, чтобы посмотреть на экран, то сможем увидеть, что автоматическое связывание неплохо работает.
Если мы нажмем на кнопку “Show Message”, то увидим такой текст:
По ссылке, приведённой выше, можно увидеть пример того, как вы можете написать связывание через ActionMessage, подобный примеру, который мы привели ниже.
<Window x:Class="CaliburnMicroBindingSample.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:cal="http://www.caliburnproject.org"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
      <StackPanel Orientation="Vertical">
        <TextBox x:Name="FirstName" />
        <Button Content="Show Message" VerticalAlignment="Center" HorizontalAlignment="Center">
          <i:Interaction.Triggers>
            <i:EventTrigger EventName="Click">
              <cal:ActionMessage MethodName="ShowFirstName" />
            </i:EventTrigger>
          </i:Interaction.Triggers>
        </Button>
      </StackPanel>
    </Grid>
</Window>
Рекомендую не злоупотреблять с Interaction.Trigger по той причине, что использование их в большом количестве приводит к тому, что студия время от времени просто крешится. У разработчиков, которые используют Visual Studio 2010, из-за стилей, которые размещены в ресурсах, или множества триггеров падает окно дизайнера, что приводит к полному падению студии. В Visual Studio 2013/2013 ситуация в этом плане улучшилась. Теперь просто может закрешиться окно дизайнера, без падения самой студии, что не может не радовать. Вторая проблема заключается в том, что с большим количеством Interaction.Trigger с такой подпиской на события не справляется нормально утилита для отладки WPF кода под названием WPF Inspector. В этом плане намного лучше Snoop, но и эта тулза может иногда не справляться. Третья проблема, как мне кажется, самая важная из всех, − это то, что ActionMessage позволяет находить, как автоматически связать методы и свойства с контролами в нашем представлении, а это сказывается на скорости запуска нашего приложения. Если вы работали с интерфейсом ICommand с WPF, используя паттерн Command, то этот подход наверняка покажется вам знакомым, за исключением того, что это все происходит неявно. Но этот подход, к сожалению, работает криво для WPF (прикладные программы для рабочего стола). Например, я расширил нашу модель и добавил метод с таким  же методом, который был у меня с приставкой “Can”.
public bool CanShowFirstName
{
    get { return !string.IsNullOrWhiteSpace(FirstName); }
}

public void ShowFirstName()
{
    MessageBox.Show(string.Format("Hello {0}", FirstName));
}
После запуска проекта никакой реакции особо и не последовало. Метод CanShowFirstName вызывается, что можно заметить, если поставить точку остановки в этот метод, но результат на экране говорит сам за себя.
Как видим, кнопка “Show Message” как была недоступной, так и осталась.
Получается, в том месте, где обновляется наше свойство FirstName, нужно вызвать функцию NotifyOfPropertyChange для конкретного свойства CanShowFirstName, как показано в примере ниже. Тогда все заработает. Мне кажется, это не совсем интуитивно понятно сразу.
private string _firstName;

public string FirstName
{
    get { return _firstName; }
    set
    {
        _firstName = value;
        NotifyOfPropertyChange(() => FirstName);
        NotifyOfPropertyChange(() => CanShowFirstName);
    }
}
Так как все работает через Actions в Caliburn Micro, мы имеем несколько attached properties, для того чтобы связать нашу модель представления с представлением, например, в том случае, если представление имеет другое имя и т.д. Их немного, поэтому давайте их перечислим.
  • Action.Target устанавливает свойство Action.Target и DataContext конкретного экземпляра класса. Строковое значение используется для связывания с помощью IoC контейнера;
  • Action.TargetWithoutContext устанавливает для свойства Action.Target указанный инстанс класса;
  • Bind.ModelView-First – установить для Action.Target конкретный экземпляр класса, используя связывание с помощью Caliburn Micro (используется внутри DataTemplate).
  • View.Model – находит для представления связанную с ним модель представления. Устанавливает модель представления для свойств Action.Target и DataContext.
Caliburn Micro имеет еще одну классную фишку, благодаря которой мне, собственно, этот фреймворк и нравится, − это расширения синтаксиса для событий и их параметров. Давайте посмотрим, как это использовать. Для этого наш пример модифицируем так, чтобы наша функция ShowFirstName принимала имя, которое мы будем передавать как параметр.
public void ShowFirstName(string name)
{
    MessageBox.Show(string.Format("Hello {0}", name));
}
Теперь посмотрим, как можно этот параметр передать в xaml.
<Window x:Class="CaliburnMicroBindingSample.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:cal="http://www.caliburnproject.org"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
    <StackPanel Orientation="Vertical">
      <TextBox x:Name="FirstName" />
      <Button Content="Show Message" VerticalAlignment="Center" HorizontalAlignment="Center" >
        <i:Interaction.Triggers>
          <i:EventTrigger EventName="Click">
            <cal:ActionMessage MethodName="ShowFirstName">
              <cal:Parameter Value="{Binding ElementName=FirstName, Path=Text}"></cal:Parameter>
            </cal:ActionMessage>
          </i:EventTrigger>
        </i:Interaction.Triggers>
      </Button>
      </StackPanel>
    </Grid>
</Window>
Если вы запустите пример, то увидите, что он отлично работает. На в Caliburn Micro есть более короткий синтаксис, для того чтобы сделать предыдущую запись. Правда, привыкнуть к такому синтаксису не так-то просто. Например, предыдущий пример можно переписать следующим образом:
<Window x:Class="CaliburnMicroBindingSample.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:cal="http://www.caliburnproject.org"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
    <StackPanel Orientation="Vertical">
      <TextBox x:Name="FirstName" />
      <Button Content="Show Message" VerticalAlignment="Center" HorizontalAlignment="Center"
              cal:Message.Attach="[Event Click] = [Action ShowFirstName(FirstName.Text)]"/>
      </StackPanel>
    </Grid>
</Window>
Наши изменения превратились компактную запись:
<Button Content="Show Message" VerticalAlignment="Center" HorizontalAlignment="Center"
              cal:Message.Attach="[Event Click] = [Action ShowFirstName(FirstName.Text)]"/>
Вы можете сравнить на следующем рисунке наши изменения.
Но, как вы понимаете, всегда нужно чем-то жертвовать. И в данном случае мы жертвуем поддержкой InelliSense. Не знаю, является это плюсом или нет для вас, но мне кажется, в таком коде без поддержки InelliSense мы можем допустить много ошибок. Ну и вы сами понимаете, что такой парсинг будет работать медленнее, чем установка свойств напрямую.
Короткий синтаксис мы, в принципе, осилили, теперь скажу еще одну интересную вещь об Action параметрах. Первое негласное правило, связанное с параметрами, заключается в том, что Caliburn будет автоматически пробовать подставлять значения с контрола, имя которого совпадает без учета регистра с именем параметра из сигнатуры метода. Если вы будете полагаться постоянно на автоматическое связывание, то ваш проект будет долго стартовать при первом запуске, поскольку нужно будет найти контролы и связать их с нужными свойствами.
Мы, пожалуй, затронем еще одну интересную тему, которая в Caliburn Micro носит название Action Bubbling. Это синтаксис, который позволяет с помощью указания имени метода находить тот, который нам подходит. Как пузырьки, сверху вниз. Давайте в нашу папку Models добавим новый класс TestModel.
public class TestModel : PropertyChangedBase
{
    private string _id;

    public string Id
    {
        get { return _id; }
        set
        {
            _id = value;
            NotifyOfPropertyChange();
        }
    }
}
Затем обновим нашу модель представления ShellViewModel, чтобы она работала с этим классом.
public class ShellViewModel : PropertyChangedBase
{
    public ShellViewModel()
    {
        Items = new BindableCollection<TestModel>();
        for (int i = 0; i < 1000; i++)
        {
            Items.Add(new TestModel
            {
                Id = Guid.NewGuid().ToString()
            });
        }
    }

    public BindableCollection<TestModel> Items { get; set; }

    public void Add()
    {
        Items.Add(new TestModel { Id = Guid.NewGuid().ToString() });
    }

    public void Remove(TestModel child)
    {
        Items.Remove(child);
    }
}
И немного перепишем наше представление.
<Window x:Class="CaliburnMicroBindingSample.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:cal="http://www.caliburnproject.org"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
      </Grid.RowDefinitions>
        <ListBox Grid.Row="0" x:Name="Items">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Button Content="Remove"
                                cal:Message.Attach="Remove($dataContext)" />
                        <TextBlock Text="{Binding Id}" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Grid.Row="1" VerticalAlignment="Center" Content="Add"
                cal:Message.Attach="Add" />
    </Grid>
</Window>
Результат выполнения можно увидеть на экране.
Этот пример подобный тому, который можно найти в документации. Если вы внимательно посмотрите, то сможете увидеть, что мы использовали как параметр ключевое слово $dataContext. Это свойство передает DataContext элемента в метод, приатаченный с помощью Message.Attach. Существует несколько таких ключевых параметров, которые мы можем использовать, и мы их постараемся рассмотреть. 
  • $dataContext – передача DataContext как параметр в метод.
  • $eventArgs – для передачи в метод EventArgs (использование событий).
  • $source – FrameworkElement, который вызвал ActionMessage.
  • $view – наше представление.
  • $executionContext  контекст выполнения этого действия содержит всю вышеизложенную информацию.
  • $this – текущий UI элемент, к которому привязан метод. Для нашего примера это кнопка.

Ну и напоследок еще добавлю несколько слов о коротком синтаксисе, который мы рассматривали. Мы можем в один ActionMessage указать несколько методов.
Например, так, как приведено в документации по ссылке actions.
<Button Content="Let's Talk" cal:Message.Attach="[Event MouseEnter] = [Action Talk('Hello', Name.Text)]; [Event MouseLeave] = [Action Talk('Goodbye', Name.Text)]" />
Итоги

Мы рассмотрели, наверное, все сценарии связывания данных с событиями с использованием Caliburn Micro. Я решил не разделять это на две отдельные статьи и описал как простой подход, так и экспертный с action bubbling, передачи специальных параметров и т.д. В целом, этого материала должно быть достаточно, для того чтобы использовать привязку данных в Caliburn Micro в полную мощь. Только помните о том, что чем больше Caliburn-у нужно будет искать, как связать ваши свойства с модели представления с контролами в самом представлении, тем сильнее это будет сказываться на производительности. Лучше, по возможности, указывайте явно, что и с чем вы связываете. Иначе вам будет сложно потом найти, какой метод или параметр отвалился, и почему какой-то участок кода перестал работать. При работе с Caliburn Micro такое, к сожалению, случается. Надеюсь, что статья получилась не очень скучная. В следующей статье мы постараемся рассмотреть использование Simple IoC простой IoC контейнер, который идет в поставке и предназначен для управления зависимостями.