Опубликован: 13.12.2011 | Доступ: свободный | Студентов: 1021 / 34 | Оценка: 4.29 / 4.57 | Длительность: 13:56:00
Лекция 8:

Использование событий, команд и триггеров в технологиях WPF и Silverlight

Команды и привязка к данным

Одна из наиболее интересных и мощных возможностей команд – это интеграция с привязкой к данным. Поскольку у элементов есть свойства Command и CommandParameter, их можно привязать к данным. А, значит, именно от данных будет зависеть происходящее в программе.

Чтобы понять, как все стыкуется, напишем приложение, которое выводит список всех файлов на диске c:\. Определим простое диалоговое окно со списковым полем и шаблон данных для отображения имени одного файла:

<Window x:Class=’EssentialWPF.DataAndCommands’
        xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’
        xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’
        Title=’Data and Commands’>
    <ListBox Margin=’2’ Name=’_files’>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text=’{Binding Path=Name}’ />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Window>

Затем запишем в свойство ItemsSource список файлов:

public partial class DataAndCommands: Window
{
    public DataAndCommands()
    {
        InitializeComponent();
        FileInfo[] fileList = new DirectoryInfo("c:\\").GetFiles("*.*");
        _files.ItemsSource = fileList;
    }
}

Теперь хотелось бы добавить кнопку для вывода содержимого файла. Но нам не нужно, чтобы запускались произвольные приложения, поэтому следует поставить фильтр на те типы файлов, которые мы согласны загружать. У нас будет две команды: Open и Blocked.

public partial class DataAndCommands: Window
{
    public static readonly RoutedCommand OpenCommand =
        new RoutedCommand("Open", typeof(DataAndCommands));
    public static readonly RoutedCommand BlockedCommand =
        new RoutedCommand("Blocked", typeof(DataAndCommands));
    ...
}

Понадобятся также обработчики этих команд:

public partial class DataAndCommands: Window
{
    public DataAndCommands()
    {
        InitializeComponent();
        
        CommandBindings.Add(new CommandBinding(OpenCommand,
           delegate (object sender, ExecutedRoutedEventArgs e)
              {Process.Start("notepad.exe", (string)e.Parameter);}));

        CommandBindings.Add(new CommandBinding(BlockedCommand,
           delegate (object sender, ExecutedRoutedEventArgs e)
              {MessageBox.Show((string)e.Parameter, "Blocked");}));
    }
}

Определив обе команды, мы можем модифицировать шаблон данных, включив в него кнопку. Можно воспользоваться привязкой к данным для задания параметра команды (имени файла). Что касается самой команды, то, поскольку мы хотим, чтобы для одних элементов списка вызывалась команда OpenCommand, а для других – BlockedCommand, то реализуем интерфейс IValueConverter для перехода от имени файла к объекту ICommand:

<DataTemplate>
    <WrapPanel>
        <TextBlock Text=’{Binding Path=Name}’ />
        <Button CommandParameter=’{Binding Path=FullName}’>
            <Button.Command>
                <Binding>
                     <Binding.Converter>
                         <l:FileToCommandConverter />
                     </Binding.Converter>
                </Binding>
            </Button.Command>
            Show
        </Button>
    </WrapPanel>
</DataTemplate>

Конвертер может проанализировать данные и решить, какую команду выполнять. Например, можно проверить расширение файла:

public class FileToCommandConverter: IValueConverter
{
    public object Convert(object value,
        Type targetType, object parameter, CultureInfo culture)
    {
        string ext = ((FileInfo)value).Extension.ToLowerInvariant();
        if (ext == ".txt")
        {
            return DataAndCommands.OpenCommand;
        }
        return DataAndCommands.BlockedCommand;
    }
...
}

Запустив эту программу, мы убедимся, что можно просматривать содержимое только текстовых файлов. Этот пример слишком прост; того же самого можно было бы достичь с помощью метода CanExecute с последующим блокированием выполнения команды для "плохих" файлов. Но важно подчеркнуть, что мы могли бы вернуть любую команду. Таким образом, поведение элемента определяется данными.

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

Триггеры

Триггер может сработать по одному из трех условий:

  • изменение состояния свойства отображения (Trigger);
  • изменение состояние свойства данных (DataTrigger);
  • событие (EventTrigger).

Все три типа триггеров при срабатывании запускают некоторую последовательность действий. Существует также два типа триггеров для наборов: MultiTrigger и MultiDataTrigger. Триггерами можно пользоваться только внутри шаблона или стиля. Объекты Trigger и EventTrigger допустимы внутри шаблона элемента управления или стиля, а объекты DataTrigger – только внутри шаблона данных.

Добавление триггеров к данным

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

Класс DataTrigger дает декларативный способ задать действия, которые следует выполнить для указанных значений из модели данных. Тем самым DataTrigger – это, по сути дела, простой конвертер значений, определенный в разметке. С помощью привязки он получает значение из модели данных, а когда это значение соответствует заранее заданному, использует последовательность объектов Setter и EventSetter.

Для демонстрации возьмем последний пример из раздела, посвященного командам, и переделаем конвертер значений в DataTrigger. Вместо того, чтобы пользоваться конвертером в привязке к свойству кнопки Command, мы привяжем к нему значение по умолчанию (в данном случае BlockedCommand):

<DataTemplate>
    <WrapPanel>
        <TextBlock Text=’{Binding Path=Name}’ />
        <Button Command=’{x:Static l:DataAndTriggers.BlockedCommand}’
                CommandParameter=’{Binding Path=FullName}’>
             Show
        </Button>
    </WrapPanel>
</DataTemplate>

Далее следует описать триггер данных, который будет срабатывать, когда файл имеет расширение ".txt":

<DataTemplate>
    <WrapPanel>
        <TextBlock Text=’{Binding Path=Name}’ />
        <Button Command=’{x:Static l:DataAndTriggers.BlockedCommand}’
                CommandParameter=’{Binding Path=FullName}’>
           Show
        </Button>
    </WrapPanel>
     <DataTemplate.Triggers>
         <DataTrigger Binding=’{Binding Path=Extension}’ Value=’.txt’/>
     </DataTemplate.Triggers>
</DataTemplate>

Если расширение равно ".txt", то свойству Command должно быть присвоено значение OpenCommand. Для этого нужно включить в триггер объект Setter, который и запишет нужное значение в свойство. В данном случае нужно еще задать объект, которому принадлежит свойство, поскольку мы не хотим изменять свойства в модели данных:

<DataTemplate>
    <WrapPanel>
        <TextBlock Text=’{Binding Path=Name}’ />
        <Button x:Name=’_showButton’
             Command=’{x:Static l:DataAndTriggers.BlockedCommand}’
                CommandParameter=’{Binding Path=FullName}’>
            Show
        </Button>
    </WrapPanel>

    <DataTemplate.Triggers>
        <DataTrigger Binding=’{Binding Path=Extension}’ Value=’.txt’>
            <Setter TargetName=’_showButton’ Property=’Command’
                    Value=’{x:Static l:DataAndTriggers.OpenCommand}’ />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

Поскольку DataTrigger может содержать более одного объекта Setter, то легко организовать выполнение нескольких действий, когда элемент данных примет интересующее нас значение. Добавим для примера еще один объект, который будет показывать подлежащую выполнению команду. Определенный порядок вызова установщиков не гарантируется, хотя в текущей версии они применяются в порядке объявления.

<DataTemplate>
    <WrapPanel>
        <TextBlock Text=’{Binding Path=Name}’ />
        <Button x:Name=’_showButton’
                Command=’{x:Static l:DataAndTriggers.BlockedCommand}’
                CommandParameter=’{Binding Path=FullName}’
                Content=’Block’/>
    </WrapPanel>
    <DataTemplate.Triggers>
         <DataTrigger Binding=’{Binding Path=Extension}’ Value=’.txt’>
              <Setter TargetName=’_showButton’ Property=’Command’
                   Value=’{x:Static l:DataAndTriggers.OpenCommand}’ />
              <Setter TargetName=’_showButton’ Property=’Content’
                   Value=’Show’ />
         </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

При работе с классом DataTrigger мы можем только сравнивать значения на равенство. Предыдущий пример отличается от варианта с использованием конвертера значений в одной существенной детали: в первоначальной версии для обработки возможных различий в регистре букв в имени файла вызывался метод ToLowerInvariant. Чтобы сохранить эту возможность, мы можем создать простой конвертер значений для приведения к нижнему регистру:

public class ToLowerInvariantConverter: IValueConverter
{
    public object Convert(object value, Type targetType,
                               object parameter, CultureInfo culture)
    {
        return ((string)value).ToLowerInvariant();
    }
    public object ConvertBack(object value, Type targetType,
                              object parameter, CultureInfo culture)
    {
        return value;
    }
}

Подключить конвертер к триггеру очень просто:

<DataTrigger Value=’.txt’>
    <DataTrigger.Binding>
         <Binding Path=’Extension’>
             <Binding.Converter>
                 <l:ToLowerInvariantConverter />
             </Binding.Converter>
         </Binding>
    </DataTrigger.Binding>
    <Setter TargetName=’_showButton’ Property=’Command’
         Value=’{x:Static l:DataAndTriggers.OpenCommand}’ />
    <Setter TargetName=’_showButton’ Property=’Content’
         Value=’Show’ />
</DataTrigger>

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

Добавление триггеров к элементам управления

Цель класса ControlTemplate – позволить полностью изменять внешний вид элемента, не меняя его логику:

<ControlTemplate TargetType=’{x:Type Button}’>
    <Border x:Name=’border’>
        <ContentPresenter />
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property=’IsPressed’ Value=’true’>
             <Setter TargetName=’border’ Property=’Background’ Value=’Red’ />
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

DataTrigger тут помогает только частично: мы можем декларативно выполнить преобразование данных и привязку. Но для отображения элемента управления нужна большая гибкость. Класс Trigger дополняет функциональность DataTrigger, добавляя свойства EnterActions и ExitActions. Следовательно, в ответ на переход свойства из одного состояния в другое мы можем, например, начинать и заканчивать анимацию. С другой стороны, объект EventTrigger получает извещение от события (к примеру, MouseEnter или Loaded) и позволяет управлять несколькими анимациями.

Краткие итоги

В этой главе мы рассмотрели, как добавить поведение в ответ на событие, инициированное пользователем или системой, с помощью различных моделей действий: событий, команд и триггеров.

Набор для практики

Вопросы:

  1. Маршрутизируемые события. Их виды и назначения.
  2. Команды. Интерфейс ICommand. Реализация команд.
  3. Триггеры. Виды триггеров и их особенности.