Разработка приложений на базе WPF и Silverlight

Базовые инструменты WPF

Разбить на страницы
Показывать лекцию целиком

Цель

Получить начальные знания о свойствах зависимостей и маршрутизируемых событиях, способах их определения, создания и использования. Изучить назначение и возможности привязки данных в приложении, их преобразования и проверки в процессе ввода.

Свойства зависимостей и маршрутизируемые события

В технологии WPF определены , . Свойства зависимостей обеспечивают возможность работы с основными средствами WPF, такими как анимация, привязка данных и стили. Большинство свойств, которыми обладают элементы WPF, являются свойствами зависимостей. Их можно рассматривать как обычные свойства .Net, но они обладают дополнительным набором возможностей WPF. В концептуальном отношении поведение свойств зависимостей не отличается от поведения обычных свойств, но на более низком уровне представления имеется иная реализация. Свойства зависимостей эффективно потребляют память и поддерживают такие высокоуровневые особенности, как уведомления об изменениях и наследование значений свойств. Кроме того, в WPF введены маршрутизируемые события, которые обладают большими возможностями по сравнению с обычными событиями .NET. Маршрутизируемые события могут перемещаться по дереву элементов (спускаться и подниматься) и позволяют выполнять обработку события в одном элементе при его возникновении в другом элементе.

Свойства зависимостей

При создании свойства зависимостей необходимо определить экземпляр класса DependencyProperty, который должен быть статическим и доступным только для чтения. Предположим, что в классе NodeImage необходимо определить свойство зависимости Fill, которое будет описывать кисть для заливки каких-либо графических объектов.

public class NodeImage: Control
    {
	... 
public static readonly DependencyProperty FillProperty;
...  
    }

В соответствии с принятыми правилами именования свойств зависимостей имя объявляемого свойства должно состоять из двух частей – имени, в нашем случае Fill, и слова Property, то есть FillProperty. Модификатор readonly означает, что создаваемое свойство зависимости FillProperty доступно только для чтения и может получить значение только в статическом конструкторе класса, содержащем данное поле, то есть в нашем случае – конструкторе класса NodeImage. Объявленное свойство зависимости должно быть зарегистрировано в WPF, что выполняется в статическом конструкторе соответствующего класса.

static NodeImage()
{
	FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();
	metadata.DefaultValue = null;
	metadata.AffectsRender = true;
	FillProperty = DependencyProperty.Register(
		"Fill", typeof(Brush), typeof(NodeImage), metadata);
}

В конструкторе для регистрации свойства зависимости вначале создаются метаданные metadata – объект класса FrameworkPropertyMetadata. В метаданных для свойства FillProperty задается значение по умолчанию ( null ) и необходимость перерисовки объекта при изменении его свойств ( AffectsRender = true ). Регистрация производится методом DependencyProperty.Register(), для которого параметрами задается имя свойства ( Fill ), его тип ( typeof(Brush) ), тип объекта, в котором регистрируется свойство ( typeof(NodeImage) ) и метаданные ( metadata ). В общем случае при регистрации свойства зависимостей дополнительно может задаваться метод обратного вызова, который будет производить проверку правильности свойств.

Для обеспечения доступа к свойствам зависимостей необходимо объявить свойство Fill, то есть упаковать его в обычную оболочку свойств, используя методы SetValue() и GetValue().

public Brush Fill
        {
            set { SetValue(FillProperty, value); }
            get { return (Brush)GetValue(FillProperty); }
        }

Созданное свойство зависимостей Fill можно использовать в коде класса NodeImage как обычное свойство.

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

Разновидностью свойств зависимостей являются присоединяемые свойства зависимостей. Данные свойства применяется к классу, отличному от того, в котором оно определено. Для регистрации присоединяемого свойства зависимостей используется метод RegisterAttached(). Упаковывать данное свойство нет необходимости

Свойства зависимостей поддерживают уведомление об изменениях и динамическое разрешение значений.

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

Динамическое разрешение значений свойств зависимостей определяет механизм извлечения значений из свойств зависимостей. При извлечении значений из свойств система WPF определяет базовое значение, учитывая следующие факторы:

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

    Маршрутизируемые события

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

    Например, для класса NodeImage необходимо определить маршрутизируемое событие DeleteNode. Для этого создаем статический экземпляр класса RoutedEvent, который объявляется с модификатором readonly. К имени события должен добавлять постфикс Event.

    public static readonly RoutedEvent DeleteNodeEvent;

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

    DeleteNodeEvent = EventManager.RegisterRoutedEvent("DeleteNode",
    	RoutingStrategy.Bubble,  typeof(RoutedEventHandler), typeof(NodeImage));

    При регистрации задается имя события ( DeleteNode ), тип маршрута ( RoutingStrategy.Bubble ), делегат, определяющий синтаксис обработчика события (в нашем случае RoutedEventHandler ) и класс, к которому принадлежит событие ( NodeImage ).

    Упаковщик события определяет методы для добавления и удаления прослушивания событий: AddHandler() и RemoveHandler().

    public event RoutedEventHandler DeleteNode
            {
                add { AddHandler(DeleteNodeEvent, value); }
                remove { RemoveHandler(DeleteNodeEvent, value); }
            }

    Класс, в котором создано маршрутизируемое событие, должен отвечать за его генерацию. В классе NodeImage в контекстном меню имеется пункт ( itemDelete ), который отвечает за генерацию события DeleteNodeEvent.

    void ContextMenu_OnClick(object sender, RoutedEventArgs e)
    {
        MenuItem item = e.Source as MenuItem;
        if (item == itemDelete)
        {
            RoutedEventArgs argsEvent = new RoutedEventArgs();
            argsEvent.RoutedEvent = NodeImage.DeleteNodeEvent;
            argsEvent.Source = this;
            RaiseEvent(argsEvent);
        }
    }

    В обработчике ContextMenu_OnClick() создается экземпляр ( argsEvent ) класса RoutedEventArgs, который содержит информацию о состоянии и данные события, связанные с маршрутизируемым событием.

    RoutedEventArgs argsEvent = new RoutedEventArgs();

    Для объекта argsEvent задаются событие и источник.

    argsEvent.RoutedEvent = NodeImage.DeleteNodeEvent;
    argsEvent.Source = this;

    Метод RaiseEvent() производит генерацию события для вызывающего объекта.

    RaiseEvent(argsEvent);

    Присоединение обработчика событий можно выполнить в коде. Если в каком-либо классе объявлен и создан объект nodeSet класса NodeImage, то подключение обработчика Node_Delete() к событию DeleteNode можно выполнить следующим образом.

    nodeSet.DeleteNode += Node_Delete;

    Подключение обработчиков события в WPF также можно выполнить в XAML описании элемента управления.

    Маршрутизируемые события бывают трех видов:

  • прямые события, которые возникают в одном элементе и не передаются в другой;
  • поднимающиеся события, которые перемещаются вверх по иерархии элементов дерева визуализации;
  • туннельные события, которые перемещаются вниз по иерархии.
  • Тип события определяется при регистрации параметром перечисления RoutingStrategy, который может принимать следующие значения:

  • Direct – маршрутизируемое событие не проходит через дерево элементов, но поддерживает другие возможности маршрутизируемых событий, например, обработку класса, EventTrigger или EventSetter ;
  • Bubble – маршрутизируемое событие использует восходящую стратегию, где экземпляр события перемещается вверх по дереву, от источника события к корневому элементу;
  • Tunnel – маршрутизируемое событие использует нисходящую стратегию, где экземпляр события перемещается вниз по дереву, от корневого элемента к исходному.
  • Обрабатывать поднимающиеся и опускающиеся события можно не только в том элементе, где они были сгенерированы, но и в других элементах визуального дерева.

    Привязка данных в WPF

    Основы привязки данных

    В общем случае, , , , . Привязка данных применяется для отображения некоторой информации из объекта-источника в пользовательском интерфейсе – свойстве целевого объекта. Кроме отображения информации в элементе управления возникает задача изменения отображаемых данных. При этом необходимо проводить синхронизацию данных объекта-источника и свойства целевого объекта при изменении любого из них.

    Привязка данных представляет собой отношение, которое определяет для WPF какую информацию необходимо извлечь из объекта-источника и использовать её для установки свойства целевого объекта. Целевое свойство должно обязательно быть свойством зависимостей и, как правило, принадлежать элементу WPF. Объект-источник может быть объектом данных приложения – общедоступным свойством класса, элементом WPF, объектом данных ADO.NET.

    При проектировании приложений возникает задача создания привязки для некоторых свойств объектов и элементов интерфейса. Такая привязка возможна только в том случае, если свойство объекта-источника является общедоступным.

    Для изучения технологии привязки данных рассмотрим пример, в котором ставится следующая задача. Необходимо разработать интерфейсный элемент для ввода параметра, который представляется вещественным числом (тип с плавающей точкой) в диапазоне от 0 до 1. Функциональность интерфейсного элемента должна предусматривать проверку достоверности вводимой информации.

    Привязка данных, не являющихся элементами WPF

    Создадим проект для интерфейсного элемента ввода данных (рис 3.1).

    (рис 3.1) Окно ввода вещественных чисел

    XAML-описание окна Window1 приведен ниже:

    <Window x:Class="Wpf_Primer1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Ввод вещественного числа" Height="140" Width="300">
        <Grid Name="gridPrimir" Height="85">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <Label Margin="3,3,3,15" Name="label1">Параметр</Label>
            <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1" />
            <Button Grid.Column="1" Grid.Row="1" Margin="5,5,28,12" Name="button1" 
                    Click="button1_Click">Проверить</Button>
        </Grid>
    </Window>

    Код класса Window1:

    using System;
    using System.Windows;
    using System.Windows.Controls;
    namespace Wpf_Primer1
    {
        public partial class Window1 : Window
        {
            public static float Factor { get; set; }
           
            public Window1()
            {
                InitializeComponent();
            }
            private void button1_Click(object sender, RoutedEventArgs e)
            {
                MessageBox.Show(Factor.ToString(), "Проверка значения параметра");
            }
        }
    }

    Свойство Factor класса Window1 необходимо для приема вводимого вещественного числа и его целесообразно сделать статическим для доступа из других классов приложения. Элемент TextBox используется для ввода вещественных чисел с плавающей точкой. Вводимое число должно передаваться свойству Factor. Кнопка button1 введена в форму для инициации проверки – вывода сообщения со значением свойства Factor. У нас встает задача привязки свойства Factor к свойству зависимостей Text элемента TextBox.

    В общем случае для привязки к объекту, не являющемуся элементом, можно использовать следующие свойства:

  • Source, которое указывает на объект-источник, поставляющий данные;
  • RelativeSource, которое указывает на объект-источник, используя объект RelativeSource, позволяющий базировать ссылку на текущем элементе;
  • DataContent, которому необходимо присвоить значение объекта-источника для текущего или более высокого элемента в дереве визуальных элементов приложения.
  • В нашем случае целесообразно использовать свойство RelativeSource. Для реализации привязки введем изменение в XAML-код элемента TextBox, добавив описание свойства Text:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1" 
                 Text="{Binding Path = Factor, RelativeSource = 
                 {RelativeSource FindAncestor, AncestorType = {x:Type Window}}}"/>

    В выражении привязки данных применяется расширение разметки XAML, которое заключается в фигурные скобки. Выражение начинается со слова Binding означающее, что создается экземпляр класса System.Windows.Data.Binding. Свойству Path присваивается значение объекта-источника, в нашем случае это свойство Factor. Объект RelativeSource задается с помощью расширенной разметки. Режим FindAncestor является значением перечисления RelativeSourceMode и определяет выражение привязки к родительскому элементу в визуальном дереве интерфейсного элемента. Кроме значения FindAncestor перечисление RelativeSourceMode может задавать следующие режимы:

  • Self - для привязки к другому свойству того же элемента;
  • PreviousData – для привязки к предыдущему элементу данных в привязываемых к данным списке;
  • TemplateParent – для привязывания к элементу, к которому применен шаблон.
  • Свойство AncestorType определяет элемент в визуальном дереве, где содержится объект-источник. В нашем случае свойство Factor является членом класса Window1 элемента Window.

    Протестируем созданное приложение. При вводе строки 0.1, что соответствует формату вещественного числа, получаем результат проверки вещественное число 0,1 (рис 3.2).

    (рис 3.2) Проверка ввода строки "0.1"

    При выводе вещественное число преобразуется к строковому представлению методом ToString():

    MessageBox.Show(Factor.ToString(), "Проверка значения параметра");

    В результате этого десятичная точка заменяется на запятую и в окне "Проверка значения параметра" выводится строка "0,1", а не введенная "0.1":

    При вводе строки "0,1", что не соответствует формату вещественного числа с плавающей точкой, получаем в результате преобразования по умолчания целое число "1" (рис 3.3).

    (рис 3.3) Проверка ввода строки "0,1"

    При вводе недопустимого символа, получаем в результате проверки целое число 1, которое было присвоено свойству Factor в предыдущем сеансе тестирования программы (рис 3.4).

    (рис 3.4) Проверка ввода недопустимого символа

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

    При корректном вводе данных мы убедились, что взаимодействие между свойством Text элемента TextBox и свойством Factor класса Window1 правильно поддерживается созданной привязкой, то есть информация от целевого объекта передается к объекту-источнику.

    В приложениях часто возникают ситуации, когда данные изменяются в коде программы и это изменение желательно увидеть в интерфейсном элементе. Проверим корректность связи от объекта-источника к целевому объекту. Для этого добавим в форму ещё одну кнопку, при нажатии на которую значение свойства ).

    (рис 3.5) Проверка корректности программного изменения объекта-источника

    Проверка корректности программного изменения объекта-источника

    Это является результатом того, что свойство Text элемента TextBox не уведомляется об изменении свойства Factor, которое не является свойством зависимостей. Для обеспечения уведомления между объектом-источником и целевым объектом можно поступить следующим образом:

  • сделать свойство Factor свойством зависимостей;
  • инициировать событие изменения свойства Factor ;
  • реализовать в классе, содержащем свойство Factor интерфейс INotifyPropertyChanged.
  • Продемонстрируем первый и третий способ решения задачи обеспечения синхронизации представления данных между объектом-источником и целевым объектом.

    Определим в классе Window1 свойство зависимостей Factor. Измененный код принимает следующий вид:

    public partial class Window1 : Window
    {
        public static readonly DependencyProperty FactorProperty;
        public float Factor
        {
            set{SetValue(FactorProperty, value);}
            get{return (float)GetValue(FactorProperty);}
        }
        static Window1()
        {
            FactorProperty = DependencyProperty.Register("Factor", 
           typeof(float), typeof(Window1), 
           new FrameworkPropertyMetadata (0.0F,
          FrameworkPropertyMetadataOptions.AffectsRender));
        }
        public Window1()
        {
            InitializeComponent();
        }
        private void button1_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show(Factor.ToString(), "Проверка значения параметра");
        }
        private void button2_Click(object sender, RoutedEventArgs e)
        {
            Factor *= 2;
        }
    }

    Вначале определяется свойство зависимостей:

    public static readonly DependencyProperty FactorProperty;

    Затем формируют методы доступа к свойству зависимостей:

    public float Factor
    {
        set{SetValue(FactorProperty, value);}
         get{return (float)GetValue(FactorProperty);}
    }

    На последнем шаге свойство зависимостей регистрируется в статическом конструкторе:

    static Window1()
    {
        FactorProperty = DependencyProperty.Register("Factor", 
        typeof(float), typeof(Window1), 
        new FrameworkPropertyMetadata (0.0F,
       FrameworkPropertyMetadataOptions.AffectsRender));
    }

    Теперь при нажатии кнопки button2 изменяется значение свойства Factor и целевого объекта свойства Text элемента TextBox.

    Для реализации интерфейса INotifyPropertyChanged создадим класс FactorClass:

    public class FactorClass: INotifyPropertyChanged
    {
      public event PropertyChangedEventHandler PropertyChanged;
      public void OnPropertyChanged(PropertyChangedEventArgs e)
      {
           if (PropertyChanged != null)
               PropertyChanged(this, e);
      }
      private float factor;
      public float Factor
      {
        get { return factor; }
        set 
        { 
          factor = value;
          OnPropertyChanged((new PropertyChangedEventArgs("Factor"));
        }
      }
    }

    В классе объявляется событие PropertyChanged:

    event PropertyChangedEventHandler PropertyChanged;

    Затем кодируется обработчик события OnPropertyChanged:

    public void OnPropertyChanged(PropertyChangedEventArgs e)
      {
           if (PropertyChanged != null)
               PropertyChanged(this, e);
    }

    Для свойства Factor при его изменении производится генерация события:

    public float Factor
      {
        get { return factor; }
        set 
        { 
          factor = value;
          OnPropertyChanged OnPropertyChanged(new
                 PropertyChangedEventArgs("Factor"));
        }
      }

    С учетом создания класса FactorClass код окна Window1 требует корректировки:

    public partial class Window1 : Window
    {
        private static FactorClass factor = new FactorClass();
        public static FactorClass Factor
        {
            get { return Window1.factor; }
        }
       
        public Window1()
        {
            InitializeComponent();
        }
        private void button1_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show(Factor.Factor.ToString(), 
                           "Проверка значения параметра");
        }
        private void button2_Click(object sender, RoutedEventArgs e)
        {
            Factor.Factor *= 2;
        }
    }

    В коде окна Window1 добавляется поле и свойства Factor типа FactorClass:

    private static FactorClass factor = new FactorClass();
    public static FactorClass Factor
    {
        get { return Window1.factor; }
    }

    В обработчиках кнопок обращение к свойству Factor производят через экземпляр класса FactorClass.

    Тестирование внутреннего изменения значение свойства Factor и целевого объекта свойства Text элемента TextBox показывают синхронизацию изменений свойств.

    При использовании класса FactorClass выражение привязки можно упростить. В этом случае создается привязка без указания источника, то есть необходимо указать только свойство Factor класса FactorClass:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1" 
    Text="{Binding Path = Factor}"/>

    Элемент Grid спроектированного окна является контейнером для всех визуальных элементов пользовательского интерфейса. Если свойству DataContext элемента Grid задать значение экземпляра класса FactorClass, то выражение привязки данных будет использовать общедоступные свойства класса FactorClass для заполнения себя данными. Установка свойства DataContext элемента Grid может быть сделана в конструкторе окна:

    public Window1()
    {
        InitializeComponent();
        gridPrimir.DataContext = Factor;
    }

    Проведем тестирование созданного приложения. Вначале введем значение 0.3 и проверим результат (рис 3.6).

    (рис 3.6) Проверка корректности привязки данных

    Далее нажмем кнопку "Увеличить в 2 раза" и посмотрим реакцию приложения (рис 3.7). Приложение работает корректно.

    (рис 3.7) Проверка корректности привязки данных при программном изменении объекта-источника

    В рассматриваемом примере поддерживается двусторонняя связь между объектом-источником и целевым объектом. В общем случае направление привязки может быть однонаправленным и двунаправленным. Это определяется свойством Binding.Mode, которое может принимать следующие значения:

  • OneWay определяет, что целевое свойство обновляется при изменении свойства-источника;
  • TwoWay определяет, что и источник и целевое свойство обновляются при изменении какого-либо свойства;
  • OneTime определяет, что целевое свойство устанавливается изначально на основе значения свойства-источника;
  • OneWaySource определяет, что исходное свойство-источника обновляется при изменении целевого свойства, которое никогда не обновляется;
  • Default определяет, что тип привязки зависит от целевого свойства. Так для свойства TextBox.Text по умолчанию будет TwoWay.
  • Для явного задания направления привязки необходимо сформировать свойство Mode целевого объекта:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1" 
    Text="{Binding Path = Factor, Mode = TwoWay}" />

    При применении привязки нужно принимать во внимание режим обновления данных между источником и целевым свойством. Режим обновления задается свойством Binding.UpdateSourceTrigger, которое может принимать следующие значения:

  • PropertyChanged задает обновление источника немедленно после обновления целевого свойства;
  • LostFocus задает обновление источника, когда целевой элемент теряет фокус;
  • Expicit задает обновление источника только при вызове метода BindingExpression.UpdateSource() ;
  • Default определяет, что обновления определяются метаданными целевого свойства.
  • Если нам необходимо немедленное обновление целевого свойства и источника, то необходимо добавить изменения с XAML разметку элемента TextBox:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1" 
    Text="{Binding Path = Factor, Mode = TwoWay, UpdateSourceTrigger = PropertyChanged}" />

    Преобразование и проверка данных

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

    В технологии WPF преобразование данных используется для:

  • форматирования данных к строковому представлению;
  • создания специфических типов WPF;
  • условного изменения свойств элемента на основе привязанных данных.
  • Для проектирования преобразователя значений необходимо выполнить следующее:

  • Создать класс, реализующий интерфейс IValueConverter.
  • Добавить атрибут ValueConversion в объявление класса и специфицировать исходный и отображаемый формат.
  • Реализовать метод Convert(), преобразующий данные из исходного формата в отображаемый формат.
  • Реализовать метод ConvertBack(), выполняющий обратное преобразование значения данные из отображаемого форма в исходный формат.
  • В случае преобразования вещественного значения в строковое можно использовать метод ToString(), а для обратного преобразования - Single.Parse().

    Спроектируем класс конвертора StringToFloatConvert для преобразования строковых данных в вещественное число.

    [ValueConversion(typeof(float),typeof(string))]
    public class StringToFloatConvert : IValueConverter
    {
        public object Convert(object value, Type typeTarget,
           object param, CultureInfo culture)
        {
            if (value != null)
             return value.ToString();
            else
                return string.Empty;
        }
        public object ConvertBack(object value, Type typeTarget,
            object param, CultureInfo culture)
        {
            if ((value != null)  (value.ToString() != ""))
                return Single.Parse(value.ToString(), NumberStyles.Float);
            else
                return 0;
        }
    }

    Обратите внимание на то, что для преобразования строки данных в вещественное число используется метод Single.Parse(value.ToString(), NumberStyles.Float). Данный метод преобразует строковое представление числа в указанном стиле в эквивалентное ему число одиночной точности с плавающей запятой, то есть для ввода используется не точка, а запятая.

    Для подключения преобразователя необходимо к дескриптору окна добавить атрибут с отображением пространства имен проекта на префикс пространства имен XML:

    xmlns:s="clr-namespace:Wpf_Primer1"

    Далее необходимо создать экземпляр класса StringToFloatConvert и присвоить его свойству Convert в привязке элемента TextBox:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1">
     <TextBox.Text>
      <Binding Path="Factor" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
          <Binding.Converter>
              <s:StringToFloatConvert />
          </Binding.Converter>
      </Binding>
     </TextBox.Text>
    </TextBox>

    Тестирование приложения показывает, что при вводе цифровой строки с запятой, то есть вещественного числа с плавающей запятой, например "0,3", выводится также строка "0,3" (рис 3.8). При вводе цифровой строки с точкой формируется программное прерывание методом Single.Parse(value.ToString(), NumberStyles.Float).

    (рис 3.8) Проверка корректности привязки с преобразованием данных

    Теперь вернемся к вопросу проверки достоверности вводимых данных. Логика проверки достоверности вводимых данных должна перехватывать некорректные значения и отвергать их.

    Технология WPF предполагает два варианта проверки достоверности данных для перехвата неверных значений:

  • инициирование ошибки в объекте данных путем генерации исключения при установке свойства;
  • определение проверки достоверности на уровне привязки.
  • Спроектируем правило проверки достоверности ввода данных путем создания класса ValidationFactor, который должен быть наследником класса правил ValidationRule из пространства имен System.Windows.Control:

    public class ValidationFactor: ValidationRule
    {
      public float Min { get; set;}
      public float Max { get; set;}
      public override ValidationResult Validate(object value,
                 System.Globalization.CultureInfo cultureInfo)
      {
        try
        {
          if (((string)value).Length > 0)
            Window1.Factor.Factor = Single.Parse((string)value, 
                                            NumberStyles.Float);
          if((string)value == "")
                        Window1.Factor.Factor = 0;
        }
        catch (Exception e)
        {
          return new ValidationResult(false, " Введен недопустимый символ! ");
        }
        if ((Window1.Factor.Factor < Min) || (Window1.Factor.Factor > Max))
          return new ValidationResult(false, " Вводимое значение вне диапазона от
                                                  " + Min + " до " + Max + ". ");
        else
          return new ValidationResult(true, null);
       }
    }

    В классе ValidationFactor определены свойства Min и Max, описывающие минимальное и максимально возможное значения вводимого вещественного числа и переопределен метод Validate() для требуемой проверки достоверности. При проверке достоверности используют перегруженную версию метода Single.Parse(), принимающего значение из перечисления NumberStyles. Это связано с тем, что проверка достоверности всегда выполняется перед преобразованием данных. Если применяется средство проверки достоверности и преобразователь к одному полю, то нужно обеспечить успех проверки достоверности. Успех или неудача логики проверки достоверности определяется возвращаемым объектом ValidationResult. Свойство IsValid указывает на успех проверки достоверности, и если проверка не прошла, то свойство ErrorContent представляет объект, описывающий ошибку.

    Создадим экземпляр класса ValidationFactor и присвоим его свойству ValidationRules в привязке элемента TextBox:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1">
        <TextBox.Text>
            <Binding Path="Factor" Mode="TwoWay"
                                   UpdateSourceTrigger="PropertyChanged">
                <Binding.Converter>
                    <s:StringToFloatConvert />
                </Binding.Converter>
                <Binding.ValidationRules>
                    <s:ValidationFactor Min="0" Max="1"/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>

    Свойствам Min и Max класса ValidationFactor определим значения соответственно 0 и 1.

    Тестирование приложения показывает, что при вводе корректного значения приложение работает правильно, а при вводе некорректного символа или числа вне интервала [0,1] происходит подсвет рамки элемента TextBox красным цветом (рис 3.9).

    (рис 3.9) Проверка корректности ввода символов

    На рис 3.9 показан ввод недопустимого нецифрового символа и недопустимого по значению числа.

    Следует отметить, что коллекция Binding.ValidationRules может состоять из неограниченного числа правил. Когда значение фиксируется в источнике, WPF будет проверять каждое правило проверки достоверности по порядку. Если все проверки достоверности пройдут успешно, то WPF вызовет преобразователь данных и применит значения к источнику. Если проверка не пройдет, то текстовое поле будет очерчено красным цветом, будут установлены свойства HasError и Error и инициируется событие Error.

    Для обработки события Error необходимо установить свойство Binding.NotifyOnValidationError в True:

    <Binding Path="Factor" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" 
     NotifyOnValidationError="True">

    Событие Error является маршрутизируемым, использует передачу вверх и поэтому его можно обработать в родительском контейнере, например в Grid окна:

    <Grid Name="gridPrimir" Height="85" Validation.Error="gridPrimir_Error">

    Код обработчика будет отображать сообщение с информацией об ошибке:

    private void gridPrimir_Error(object sender, ValidationErrorEventArgs e)
    {
        if (e.Action == ValidationErrorEventAction.Added)
        {
            MessageBox.Show(e.Error.ErrorContent.ToString(), "Предупреждение",
                                 MessageBoxButton.OK, MessageBoxImage.Warning);
        }
    }

    Свойство и рис 3.11 приведены реакция приложения на ввод некорректных данных.

    (рис 3.11) Реакция на ввод недопустимого символа(рис 3.10) Реакция на ввод недопустимого числа

    Информацию об ошибке можно получить и другим способом. Можно отобразить сообщение об ошибке в ToolTip элемента TextBox. При этом сообщение об ошибке будет появляться, когда пользователь поместим курсор мыши над элементом TextBox. Для реализации такого варианта необходимо создать шаблон ошибок, который будет содержать триггер, реагирующий на присваивание свойству Validation.HasError значения true и применит ToolTip с сообщением об ошибке:

    <Window.Resources>
      <Style TargetType="{x:Type TextBox}" >
        <Style.Triggers>
           <Trigger Property="Validation.HasError" Value="True">
              <Setter Property="ToolTip" Value=
                               "{Binding RelativeSource = {RelativeSource Self}, 
                Path = (Validation.Errors)[0].ErrorContent}" />
           </Trigger>
        </Style.Triggers>
      </Style>
    </Window.Resources>

    При формировании свойства и рис 3.13 приведены примеры применения подсказки для проверки ввода данных.

    (рис 3.13) Подсказка при вводе недопустимого символа(рис 3.12) Подсказка при вводе недопустимого числа

    Ключевые термины

    Свойство зависимостей, присоединяемое свойство зависимостей, маршрутизируемое событие, привязка данных, объект-источник, целевое свойство, преобразование данных, проверка достоверности вводимых данных.

    DependencyProperty, FrameworkPropertyMetadata, DependencyProperty.Register, RoutedEvent, RoutedEventHandler, RoutedEventArgs, IValueConverter, ValidationRule.

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

    Свойства зависимостей и маршрутизируемые события являются эффективными инструментами технологии WPF для построения привязки объектов и обеспечения гибкости обработки разнообразных событий в приложении. Преобразование данных расширяет возможности привязки их привязки, проверка корректности ввода обеспечивает повышение надежности работы приложения.

    Ресурсы для углубленного изучения

  • Свойства // http://msdn.microsoft.com/ru-ru/library/ms753192.aspx.
  • Событя // http://msdn.microsoft.com/ru-ru/library/ms753115.aspx.
  • Привязка данных // http://msdn.microsoft.com/ru-ru/library/cc278072(v=VS.95).aspx.
  • , стр. 175 – 219.
  • , стр. 978 – 1104, 1171 - 1212.
  • , стр. 150 – 192.
  • , стр. 857 – 890, 995 - 1101.
  • Вопросы для самопроверки

  • Поясните назначение свойств зависимости в WPF.
  • Как именуются свойства зависимости?
  • Как объявляются свойства зависимости?
  • Как регистрируются свойства зависимости?
  • Как происходит динамическое разрешение значений свойств зависимостей?
  • Поясните назначение маршрутизируемого события.
  • Как именуются маршрутизируемое событие?
  • Как объявляются маршрутизируемое событие?
  • Как регистрируются маршрутизируемое событие?
  • Какие существуют типы маршрутизируемых событий?
  • Поясните основные принципы привязки данных.
  • Какие требования предъявляются к источнику и приемнику данных при выполнении привязки данных?
  • Какие свойства используются при привязке к объекту, не являющемуся элементом?
  • Как определяется направление привязки данных?
  • Для чего в технологии WPF используется преобразование данных?
  • Каким требованиям должен соответствовать класс, используемый как конвертор данных?
  • Сколько правил может содержать коллекция Binding.ValidationRules?
  • Страницы:

    Цель

    Получить начальные знания о свойствах зависимостей и маршрутизируемых событиях, способах их определения, создания и использования. Изучить назначение и возможности привязки данных в приложении, их преобразования и проверки в процессе ввода.

    Свойства зависимостей и маршрутизируемые события

    В технологии WPF определены , . Свойства зависимостей обеспечивают возможность работы с основными средствами WPF, такими как анимация, привязка данных и стили. Большинство свойств, которыми обладают элементы WPF, являются свойствами зависимостей. Их можно рассматривать как обычные свойства .Net, но они обладают дополнительным набором возможностей WPF. В концептуальном отношении поведение свойств зависимостей не отличается от поведения обычных свойств, но на более низком уровне представления имеется иная реализация. Свойства зависимостей эффективно потребляют память и поддерживают такие высокоуровневые особенности, как уведомления об изменениях и наследование значений свойств. Кроме того, в WPF введены маршрутизируемые события, которые обладают большими возможностями по сравнению с обычными событиями .NET. Маршрутизируемые события могут перемещаться по дереву элементов (спускаться и подниматься) и позволяют выполнять обработку события в одном элементе при его возникновении в другом элементе.

    Свойства зависимостей

    При создании свойства зависимостей необходимо определить экземпляр класса DependencyProperty, который должен быть статическим и доступным только для чтения. Предположим, что в классе NodeImage необходимо определить свойство зависимости Fill, которое будет описывать кисть для заливки каких-либо графических объектов.

    public class NodeImage: Control
        {
    	... 
    public static readonly DependencyProperty FillProperty;
    ...  
        }

    В соответствии с принятыми правилами именования свойств зависимостей имя объявляемого свойства должно состоять из двух частей – имени, в нашем случае Fill, и слова Property, то есть FillProperty. Модификатор readonly означает, что создаваемое свойство зависимости FillProperty доступно только для чтения и может получить значение только в статическом конструкторе класса, содержащем данное поле, то есть в нашем случае – конструкторе класса NodeImage. Объявленное свойство зависимости должно быть зарегистрировано в WPF, что выполняется в статическом конструкторе соответствующего класса.

    static NodeImage()
    {
    	FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();
    	metadata.DefaultValue = null;
    	metadata.AffectsRender = true;
    	FillProperty = DependencyProperty.Register(
    		"Fill", typeof(Brush), typeof(NodeImage), metadata);
    }

    В конструкторе для регистрации свойства зависимости вначале создаются метаданные metadata – объект класса FrameworkPropertyMetadata. В метаданных для свойства FillProperty задается значение по умолчанию ( null ) и необходимость перерисовки объекта при изменении его свойств ( AffectsRender = true ). Регистрация производится методом DependencyProperty.Register(), для которого параметрами задается имя свойства ( Fill ), его тип ( typeof(Brush) ), тип объекта, в котором регистрируется свойство ( typeof(NodeImage) ) и метаданные ( metadata ). В общем случае при регистрации свойства зависимостей дополнительно может задаваться метод обратного вызова, который будет производить проверку правильности свойств.

    Для обеспечения доступа к свойствам зависимостей необходимо объявить свойство Fill, то есть упаковать его в обычную оболочку свойств, используя методы SetValue() и GetValue().

    public Brush Fill
            {
                set { SetValue(FillProperty, value); }
                get { return (Brush)GetValue(FillProperty); }
            }

    Созданное свойство зависимостей Fill можно использовать в коде класса NodeImage как обычное свойство.

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

    Разновидностью свойств зависимостей являются присоединяемые свойства зависимостей. Данные свойства применяется к классу, отличному от того, в котором оно определено. Для регистрации присоединяемого свойства зависимостей используется метод RegisterAttached(). Упаковывать данное свойство нет необходимости

    Свойства зависимостей поддерживают уведомление об изменениях и динамическое разрешение значений.

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

    Динамическое разрешение значений свойств зависимостей определяет механизм извлечения значений из свойств зависимостей. При извлечении значений из свойств система WPF определяет базовое значение, учитывая следующие факторы:

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

    Маршрутизируемые события

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

    Например, для класса NodeImage необходимо определить маршрутизируемое событие DeleteNode. Для этого создаем статический экземпляр класса RoutedEvent, который объявляется с модификатором readonly. К имени события должен добавлять постфикс Event.

    public static readonly RoutedEvent DeleteNodeEvent;

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

    DeleteNodeEvent = EventManager.RegisterRoutedEvent("DeleteNode",
    	RoutingStrategy.Bubble,  typeof(RoutedEventHandler), typeof(NodeImage));

    При регистрации задается имя события ( DeleteNode ), тип маршрута ( RoutingStrategy.Bubble ), делегат, определяющий синтаксис обработчика события (в нашем случае RoutedEventHandler ) и класс, к которому принадлежит событие ( NodeImage ).

    Упаковщик события определяет методы для добавления и удаления прослушивания событий: AddHandler() и RemoveHandler().

    public event RoutedEventHandler DeleteNode
            {
                add { AddHandler(DeleteNodeEvent, value); }
                remove { RemoveHandler(DeleteNodeEvent, value); }
            }

    Класс, в котором создано маршрутизируемое событие, должен отвечать за его генерацию. В классе NodeImage в контекстном меню имеется пункт ( itemDelete ), который отвечает за генерацию события DeleteNodeEvent.

    void ContextMenu_OnClick(object sender, RoutedEventArgs e)
    {
        MenuItem item = e.Source as MenuItem;
        if (item == itemDelete)
        {
            RoutedEventArgs argsEvent = new RoutedEventArgs();
            argsEvent.RoutedEvent = NodeImage.DeleteNodeEvent;
            argsEvent.Source = this;
            RaiseEvent(argsEvent);
        }
    }

    В обработчике ContextMenu_OnClick() создается экземпляр ( argsEvent ) класса RoutedEventArgs, который содержит информацию о состоянии и данные события, связанные с маршрутизируемым событием.

    RoutedEventArgs argsEvent = new RoutedEventArgs();

    Для объекта argsEvent задаются событие и источник.

    argsEvent.RoutedEvent = NodeImage.DeleteNodeEvent;
    argsEvent.Source = this;

    Метод RaiseEvent() производит генерацию события для вызывающего объекта.

    RaiseEvent(argsEvent);

    Присоединение обработчика событий можно выполнить в коде. Если в каком-либо классе объявлен и создан объект nodeSet класса NodeImage, то подключение обработчика Node_Delete() к событию DeleteNode можно выполнить следующим образом.

    nodeSet.DeleteNode += Node_Delete;

    Подключение обработчиков события в WPF также можно выполнить в XAML описании элемента управления.

    Маршрутизируемые события бывают трех видов:

  • прямые события, которые возникают в одном элементе и не передаются в другой;
  • поднимающиеся события, которые перемещаются вверх по иерархии элементов дерева визуализации;
  • туннельные события, которые перемещаются вниз по иерархии.
  • Тип события определяется при регистрации параметром перечисления RoutingStrategy, который может принимать следующие значения:

  • Direct – маршрутизируемое событие не проходит через дерево элементов, но поддерживает другие возможности маршрутизируемых событий, например, обработку класса, EventTrigger или EventSetter ;
  • Bubble – маршрутизируемое событие использует восходящую стратегию, где экземпляр события перемещается вверх по дереву, от источника события к корневому элементу;
  • Tunnel – маршрутизируемое событие использует нисходящую стратегию, где экземпляр события перемещается вниз по дереву, от корневого элемента к исходному.
  • Обрабатывать поднимающиеся и опускающиеся события можно не только в том элементе, где они были сгенерированы, но и в других элементах визуального дерева.

    Привязка данных в WPF

    Основы привязки данных

    В общем случае, , , , . Привязка данных применяется для отображения некоторой информации из объекта-источника в пользовательском интерфейсе – свойстве целевого объекта. Кроме отображения информации в элементе управления возникает задача изменения отображаемых данных. При этом необходимо проводить синхронизацию данных объекта-источника и свойства целевого объекта при изменении любого из них.

    Привязка данных представляет собой отношение, которое определяет для WPF какую информацию необходимо извлечь из объекта-источника и использовать её для установки свойства целевого объекта. Целевое свойство должно обязательно быть свойством зависимостей и, как правило, принадлежать элементу WPF. Объект-источник может быть объектом данных приложения – общедоступным свойством класса, элементом WPF, объектом данных ADO.NET.

    При проектировании приложений возникает задача создания привязки для некоторых свойств объектов и элементов интерфейса. Такая привязка возможна только в том случае, если свойство объекта-источника является общедоступным.

    Для изучения технологии привязки данных рассмотрим пример, в котором ставится следующая задача. Необходимо разработать интерфейсный элемент для ввода параметра, который представляется вещественным числом (тип с плавающей точкой) в диапазоне от 0 до 1. Функциональность интерфейсного элемента должна предусматривать проверку достоверности вводимой информации.

    Привязка данных, не являющихся элементами WPF

    Создадим проект для интерфейсного элемента ввода данных (рис 3.1).

    (рис 3.1) Окно ввода вещественных чисел

    XAML-описание окна Window1 приведен ниже:

    <Window x:Class="Wpf_Primer1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Ввод вещественного числа" Height="140" Width="300">
        <Grid Name="gridPrimir" Height="85">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <Label Margin="3,3,3,15" Name="label1">Параметр</Label>
            <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1" />
            <Button Grid.Column="1" Grid.Row="1" Margin="5,5,28,12" Name="button1" 
                    Click="button1_Click">Проверить</Button>
        </Grid>
    </Window>

    Код класса Window1:

    using System;
    using System.Windows;
    using System.Windows.Controls;
    namespace Wpf_Primer1
    {
        public partial class Window1 : Window
        {
            public static float Factor { get; set; }
           
            public Window1()
            {
                InitializeComponent();
            }
            private void button1_Click(object sender, RoutedEventArgs e)
            {
                MessageBox.Show(Factor.ToString(), "Проверка значения параметра");
            }
        }
    }

    Свойство Factor класса Window1 необходимо для приема вводимого вещественного числа и его целесообразно сделать статическим для доступа из других классов приложения. Элемент TextBox используется для ввода вещественных чисел с плавающей точкой. Вводимое число должно передаваться свойству Factor. Кнопка button1 введена в форму для инициации проверки – вывода сообщения со значением свойства Factor. У нас встает задача привязки свойства Factor к свойству зависимостей Text элемента TextBox.

    В общем случае для привязки к объекту, не являющемуся элементом, можно использовать следующие свойства:

  • Source, которое указывает на объект-источник, поставляющий данные;
  • RelativeSource, которое указывает на объект-источник, используя объект RelativeSource, позволяющий базировать ссылку на текущем элементе;
  • DataContent, которому необходимо присвоить значение объекта-источника для текущего или более высокого элемента в дереве визуальных элементов приложения.
  • В нашем случае целесообразно использовать свойство RelativeSource. Для реализации привязки введем изменение в XAML-код элемента TextBox, добавив описание свойства Text:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1" 
                 Text="{Binding Path = Factor, RelativeSource = 
                 {RelativeSource FindAncestor, AncestorType = {x:Type Window}}}"/>

    В выражении привязки данных применяется расширение разметки XAML, которое заключается в фигурные скобки. Выражение начинается со слова Binding означающее, что создается экземпляр класса System.Windows.Data.Binding. Свойству Path присваивается значение объекта-источника, в нашем случае это свойство Factor. Объект RelativeSource задается с помощью расширенной разметки. Режим FindAncestor является значением перечисления RelativeSourceMode и определяет выражение привязки к родительскому элементу в визуальном дереве интерфейсного элемента. Кроме значения FindAncestor перечисление RelativeSourceMode может задавать следующие режимы:

  • Self - для привязки к другому свойству того же элемента;
  • PreviousData – для привязки к предыдущему элементу данных в привязываемых к данным списке;
  • TemplateParent – для привязывания к элементу, к которому применен шаблон.
  • Свойство AncestorType определяет элемент в визуальном дереве, где содержится объект-источник. В нашем случае свойство Factor является членом класса Window1 элемента Window.

    Протестируем созданное приложение. При вводе строки 0.1, что соответствует формату вещественного числа, получаем результат проверки вещественное число 0,1 (рис 3.2).

    (рис 3.2) Проверка ввода строки "0.1"

    При выводе вещественное число преобразуется к строковому представлению методом ToString():

    MessageBox.Show(Factor.ToString(), "Проверка значения параметра");

    В результате этого десятичная точка заменяется на запятую и в окне "Проверка значения параметра" выводится строка "0,1", а не введенная "0.1":

    При вводе строки "0,1", что не соответствует формату вещественного числа с плавающей точкой, получаем в результате преобразования по умолчания целое число "1" (рис 3.3).

    (рис 3.3) Проверка ввода строки "0,1"

    При вводе недопустимого символа, получаем в результате проверки целое число 1, которое было присвоено свойству Factor в предыдущем сеансе тестирования программы (рис 3.4).

    (рис 3.4) Проверка ввода недопустимого символа

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

    При корректном вводе данных мы убедились, что взаимодействие между свойством Text элемента TextBox и свойством Factor класса Window1 правильно поддерживается созданной привязкой, то есть информация от целевого объекта передается к объекту-источнику.

    В приложениях часто возникают ситуации, когда данные изменяются в коде программы и это изменение желательно увидеть в интерфейсном элементе. Проверим корректность связи от объекта-источника к целевому объекту. Для этого добавим в форму ещё одну кнопку, при нажатии на которую значение свойства ).

    (рис 3.5) Проверка корректности программного изменения объекта-источника

    Проверка корректности программного изменения объекта-источника

    Это является результатом того, что свойство Text элемента TextBox не уведомляется об изменении свойства Factor, которое не является свойством зависимостей. Для обеспечения уведомления между объектом-источником и целевым объектом можно поступить следующим образом:

  • сделать свойство Factor свойством зависимостей;
  • инициировать событие изменения свойства Factor ;
  • реализовать в классе, содержащем свойство Factor интерфейс INotifyPropertyChanged.
  • Продемонстрируем первый и третий способ решения задачи обеспечения синхронизации представления данных между объектом-источником и целевым объектом.

    Определим в классе Window1 свойство зависимостей Factor. Измененный код принимает следующий вид:

    public partial class Window1 : Window
    {
        public static readonly DependencyProperty FactorProperty;
        public float Factor
        {
            set{SetValue(FactorProperty, value);}
            get{return (float)GetValue(FactorProperty);}
        }
        static Window1()
        {
            FactorProperty = DependencyProperty.Register("Factor", 
           typeof(float), typeof(Window1), 
           new FrameworkPropertyMetadata (0.0F,
          FrameworkPropertyMetadataOptions.AffectsRender));
        }
        public Window1()
        {
            InitializeComponent();
        }
        private void button1_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show(Factor.ToString(), "Проверка значения параметра");
        }
        private void button2_Click(object sender, RoutedEventArgs e)
        {
            Factor *= 2;
        }
    }

    Вначале определяется свойство зависимостей:

    public static readonly DependencyProperty FactorProperty;

    Затем формируют методы доступа к свойству зависимостей:

    public float Factor
    {
        set{SetValue(FactorProperty, value);}
         get{return (float)GetValue(FactorProperty);}
    }

    На последнем шаге свойство зависимостей регистрируется в статическом конструкторе:

    static Window1()
    {
        FactorProperty = DependencyProperty.Register("Factor", 
        typeof(float), typeof(Window1), 
        new FrameworkPropertyMetadata (0.0F,
       FrameworkPropertyMetadataOptions.AffectsRender));
    }

    Теперь при нажатии кнопки button2 изменяется значение свойства Factor и целевого объекта свойства Text элемента TextBox.

    Для реализации интерфейса INotifyPropertyChanged создадим класс FactorClass:

    public class FactorClass: INotifyPropertyChanged
    {
      public event PropertyChangedEventHandler PropertyChanged;
      public void OnPropertyChanged(PropertyChangedEventArgs e)
      {
           if (PropertyChanged != null)
               PropertyChanged(this, e);
      }
      private float factor;
      public float Factor
      {
        get { return factor; }
        set 
        { 
          factor = value;
          OnPropertyChanged((new PropertyChangedEventArgs("Factor"));
        }
      }
    }

    В классе объявляется событие PropertyChanged:

    event PropertyChangedEventHandler PropertyChanged;

    Затем кодируется обработчик события OnPropertyChanged:

    public void OnPropertyChanged(PropertyChangedEventArgs e)
      {
           if (PropertyChanged != null)
               PropertyChanged(this, e);
    }

    Для свойства Factor при его изменении производится генерация события:

    public float Factor
      {
        get { return factor; }
        set 
        { 
          factor = value;
          OnPropertyChanged OnPropertyChanged(new
                 PropertyChangedEventArgs("Factor"));
        }
      }

    С учетом создания класса FactorClass код окна Window1 требует корректировки:

    public partial class Window1 : Window
    {
        private static FactorClass factor = new FactorClass();
        public static FactorClass Factor
        {
            get { return Window1.factor; }
        }
       
        public Window1()
        {
            InitializeComponent();
        }
        private void button1_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show(Factor.Factor.ToString(), 
                           "Проверка значения параметра");
        }
        private void button2_Click(object sender, RoutedEventArgs e)
        {
            Factor.Factor *= 2;
        }
    }

    В коде окна Window1 добавляется поле и свойства Factor типа FactorClass:

    private static FactorClass factor = new FactorClass();
    public static FactorClass Factor
    {
        get { return Window1.factor; }
    }

    В обработчиках кнопок обращение к свойству Factor производят через экземпляр класса FactorClass.

    Тестирование внутреннего изменения значение свойства Factor и целевого объекта свойства Text элемента TextBox показывают синхронизацию изменений свойств.

    При использовании класса FactorClass выражение привязки можно упростить. В этом случае создается привязка без указания источника, то есть необходимо указать только свойство Factor класса FactorClass:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1" 
    Text="{Binding Path = Factor}"/>

    Элемент Grid спроектированного окна является контейнером для всех визуальных элементов пользовательского интерфейса. Если свойству DataContext элемента Grid задать значение экземпляра класса FactorClass, то выражение привязки данных будет использовать общедоступные свойства класса FactorClass для заполнения себя данными. Установка свойства DataContext элемента Grid может быть сделана в конструкторе окна:

    public Window1()
    {
        InitializeComponent();
        gridPrimir.DataContext = Factor;
    }

    Проведем тестирование созданного приложения. Вначале введем значение 0.3 и проверим результат (рис 3.6).

    (рис 3.6) Проверка корректности привязки данных

    Далее нажмем кнопку "Увеличить в 2 раза" и посмотрим реакцию приложения (рис 3.7). Приложение работает корректно.

    (рис 3.7) Проверка корректности привязки данных при программном изменении объекта-источника

    В рассматриваемом примере поддерживается двусторонняя связь между объектом-источником и целевым объектом. В общем случае направление привязки может быть однонаправленным и двунаправленным. Это определяется свойством Binding.Mode, которое может принимать следующие значения:

  • OneWay определяет, что целевое свойство обновляется при изменении свойства-источника;
  • TwoWay определяет, что и источник и целевое свойство обновляются при изменении какого-либо свойства;
  • OneTime определяет, что целевое свойство устанавливается изначально на основе значения свойства-источника;
  • OneWaySource определяет, что исходное свойство-источника обновляется при изменении целевого свойства, которое никогда не обновляется;
  • Default определяет, что тип привязки зависит от целевого свойства. Так для свойства TextBox.Text по умолчанию будет TwoWay.
  • Для явного задания направления привязки необходимо сформировать свойство Mode целевого объекта:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1" 
    Text="{Binding Path = Factor, Mode = TwoWay}" />

    При применении привязки нужно принимать во внимание режим обновления данных между источником и целевым свойством. Режим обновления задается свойством Binding.UpdateSourceTrigger, которое может принимать следующие значения:

  • PropertyChanged задает обновление источника немедленно после обновления целевого свойства;
  • LostFocus задает обновление источника, когда целевой элемент теряет фокус;
  • Expicit задает обновление источника только при вызове метода BindingExpression.UpdateSource() ;
  • Default определяет, что обновления определяются метаданными целевого свойства.
  • Если нам необходимо немедленное обновление целевого свойства и источника, то необходимо добавить изменения с XAML разметку элемента TextBox:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1" 
    Text="{Binding Path = Factor, Mode = TwoWay, UpdateSourceTrigger = PropertyChanged}" />

    Преобразование и проверка данных

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

    В технологии WPF преобразование данных используется для:

  • форматирования данных к строковому представлению;
  • создания специфических типов WPF;
  • условного изменения свойств элемента на основе привязанных данных.
  • Для проектирования преобразователя значений необходимо выполнить следующее:

  • Создать класс, реализующий интерфейс IValueConverter.
  • Добавить атрибут ValueConversion в объявление класса и специфицировать исходный и отображаемый формат.
  • Реализовать метод Convert(), преобразующий данные из исходного формата в отображаемый формат.
  • Реализовать метод ConvertBack(), выполняющий обратное преобразование значения данные из отображаемого форма в исходный формат.
  • В случае преобразования вещественного значения в строковое можно использовать метод ToString(), а для обратного преобразования - Single.Parse().

    Спроектируем класс конвертора StringToFloatConvert для преобразования строковых данных в вещественное число.

    [ValueConversion(typeof(float),typeof(string))]
    public class StringToFloatConvert : IValueConverter
    {
        public object Convert(object value, Type typeTarget,
           object param, CultureInfo culture)
        {
            if (value != null)
             return value.ToString();
            else
                return string.Empty;
        }
        public object ConvertBack(object value, Type typeTarget,
            object param, CultureInfo culture)
        {
            if ((value != null)  (value.ToString() != ""))
                return Single.Parse(value.ToString(), NumberStyles.Float);
            else
                return 0;
        }
    }

    Обратите внимание на то, что для преобразования строки данных в вещественное число используется метод Single.Parse(value.ToString(), NumberStyles.Float). Данный метод преобразует строковое представление числа в указанном стиле в эквивалентное ему число одиночной точности с плавающей запятой, то есть для ввода используется не точка, а запятая.

    Для подключения преобразователя необходимо к дескриптору окна добавить атрибут с отображением пространства имен проекта на префикс пространства имен XML:

    xmlns:s="clr-namespace:Wpf_Primer1"

    Далее необходимо создать экземпляр класса StringToFloatConvert и присвоить его свойству Convert в привязке элемента TextBox:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1">
     <TextBox.Text>
      <Binding Path="Factor" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
          <Binding.Converter>
              <s:StringToFloatConvert />
          </Binding.Converter>
      </Binding>
     </TextBox.Text>
    </TextBox>

    Тестирование приложения показывает, что при вводе цифровой строки с запятой, то есть вещественного числа с плавающей запятой, например "0,3", выводится также строка "0,3" (рис 3.8). При вводе цифровой строки с точкой формируется программное прерывание методом Single.Parse(value.ToString(), NumberStyles.Float).

    (рис 3.8) Проверка корректности привязки с преобразованием данных

    Теперь вернемся к вопросу проверки достоверности вводимых данных. Логика проверки достоверности вводимых данных должна перехватывать некорректные значения и отвергать их.

    Технология WPF предполагает два варианта проверки достоверности данных для перехвата неверных значений:

  • инициирование ошибки в объекте данных путем генерации исключения при установке свойства;
  • определение проверки достоверности на уровне привязки.
  • Спроектируем правило проверки достоверности ввода данных путем создания класса ValidationFactor, который должен быть наследником класса правил ValidationRule из пространства имен System.Windows.Control:

    public class ValidationFactor: ValidationRule
    {
      public float Min { get; set;}
      public float Max { get; set;}
      public override ValidationResult Validate(object value,
                 System.Globalization.CultureInfo cultureInfo)
      {
        try
        {
          if (((string)value).Length > 0)
            Window1.Factor.Factor = Single.Parse((string)value, 
                                            NumberStyles.Float);
          if((string)value == "")
                        Window1.Factor.Factor = 0;
        }
        catch (Exception e)
        {
          return new ValidationResult(false, " Введен недопустимый символ! ");
        }
        if ((Window1.Factor.Factor < Min) || (Window1.Factor.Factor > Max))
          return new ValidationResult(false, " Вводимое значение вне диапазона от
                                                  " + Min + " до " + Max + ". ");
        else
          return new ValidationResult(true, null);
       }
    }

    В классе ValidationFactor определены свойства Min и Max, описывающие минимальное и максимально возможное значения вводимого вещественного числа и переопределен метод Validate() для требуемой проверки достоверности. При проверке достоверности используют перегруженную версию метода Single.Parse(), принимающего значение из перечисления NumberStyles. Это связано с тем, что проверка достоверности всегда выполняется перед преобразованием данных. Если применяется средство проверки достоверности и преобразователь к одному полю, то нужно обеспечить успех проверки достоверности. Успех или неудача логики проверки достоверности определяется возвращаемым объектом ValidationResult. Свойство IsValid указывает на успех проверки достоверности, и если проверка не прошла, то свойство ErrorContent представляет объект, описывающий ошибку.

    Создадим экземпляр класса ValidationFactor и присвоим его свойству ValidationRules в привязке элемента TextBox:

    <TextBox Grid.Column="1" Margin="5,5,28,15" Name="textBox1">
        <TextBox.Text>
            <Binding Path="Factor" Mode="TwoWay"
                                   UpdateSourceTrigger="PropertyChanged">
                <Binding.Converter>
                    <s:StringToFloatConvert />
                </Binding.Converter>
                <Binding.ValidationRules>
                    <s:ValidationFactor Min="0" Max="1"/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>

    Свойствам Min и Max класса ValidationFactor определим значения соответственно 0 и 1.

    Тестирование приложения показывает, что при вводе корректного значения приложение работает правильно, а при вводе некорректного символа или числа вне интервала [0,1] происходит подсвет рамки элемента TextBox красным цветом (рис 3.9).

    (рис 3.9) Проверка корректности ввода символов

    На рис 3.9 показан ввод недопустимого нецифрового символа и недопустимого по значению числа.

    Следует отметить, что коллекция Binding.ValidationRules может состоять из неограниченного числа правил. Когда значение фиксируется в источнике, WPF будет проверять каждое правило проверки достоверности по порядку. Если все проверки достоверности пройдут успешно, то WPF вызовет преобразователь данных и применит значения к источнику. Если проверка не пройдет, то текстовое поле будет очерчено красным цветом, будут установлены свойства HasError и Error и инициируется событие Error.

    Для обработки события Error необходимо установить свойство Binding.NotifyOnValidationError в True:

    <Binding Path="Factor" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" 
     NotifyOnValidationError="True">

    Событие Error является маршрутизируемым, использует передачу вверх и поэтому его можно обработать в родительском контейнере, например в Grid окна:

    <Grid Name="gridPrimir" Height="85" Validation.Error="gridPrimir_Error">

    Код обработчика будет отображать сообщение с информацией об ошибке:

    private void gridPrimir_Error(object sender, ValidationErrorEventArgs e)
    {
        if (e.Action == ValidationErrorEventAction.Added)
        {
            MessageBox.Show(e.Error.ErrorContent.ToString(), "Предупреждение",
                                 MessageBoxButton.OK, MessageBoxImage.Warning);
        }
    }

    Свойство и рис 3.11 приведены реакция приложения на ввод некорректных данных.

    (рис 3.11) Реакция на ввод недопустимого символа(рис 3.10) Реакция на ввод недопустимого числа

    Информацию об ошибке можно получить и другим способом. Можно отобразить сообщение об ошибке в ToolTip элемента TextBox. При этом сообщение об ошибке будет появляться, когда пользователь поместим курсор мыши над элементом TextBox. Для реализации такого варианта необходимо создать шаблон ошибок, который будет содержать триггер, реагирующий на присваивание свойству Validation.HasError значения true и применит ToolTip с сообщением об ошибке:

    <Window.Resources>
      <Style TargetType="{x:Type TextBox}" >
        <Style.Triggers>
           <Trigger Property="Validation.HasError" Value="True">
              <Setter Property="ToolTip" Value=
                               "{Binding RelativeSource = {RelativeSource Self}, 
                Path = (Validation.Errors)[0].ErrorContent}" />
           </Trigger>
        </Style.Triggers>
      </Style>
    </Window.Resources>

    При формировании свойства и рис 3.13 приведены примеры применения подсказки для проверки ввода данных.

    (рис 3.13) Подсказка при вводе недопустимого символа(рис 3.12) Подсказка при вводе недопустимого числа

    Ключевые термины

    Свойство зависимостей, присоединяемое свойство зависимостей, маршрутизируемое событие, привязка данных, объект-источник, целевое свойство, преобразование данных, проверка достоверности вводимых данных.

    DependencyProperty, FrameworkPropertyMetadata, DependencyProperty.Register, RoutedEvent, RoutedEventHandler, RoutedEventArgs, IValueConverter, ValidationRule.

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

    Свойства зависимостей и маршрутизируемые события являются эффективными инструментами технологии WPF для построения привязки объектов и обеспечения гибкости обработки разнообразных событий в приложении. Преобразование данных расширяет возможности привязки их привязки, проверка корректности ввода обеспечивает повышение надежности работы приложения.

    Ресурсы для углубленного изучения

  • Свойства // http://msdn.microsoft.com/ru-ru/library/ms753192.aspx.
  • Событя // http://msdn.microsoft.com/ru-ru/library/ms753115.aspx.
  • Привязка данных // http://msdn.microsoft.com/ru-ru/library/cc278072(v=VS.95).aspx.
  • , стр. 175 – 219.
  • , стр. 978 – 1104, 1171 - 1212.
  • , стр. 150 – 192.
  • , стр. 857 – 890, 995 - 1101.
  • Вопросы для самопроверки

  • Поясните назначение свойств зависимости в WPF.
  • Как именуются свойства зависимости?
  • Как объявляются свойства зависимости?
  • Как регистрируются свойства зависимости?
  • Как происходит динамическое разрешение значений свойств зависимостей?
  • Поясните назначение маршрутизируемого события.
  • Как именуются маршрутизируемое событие?
  • Как объявляются маршрутизируемое событие?
  • Как регистрируются маршрутизируемое событие?
  • Какие существуют типы маршрутизируемых событий?
  • Поясните основные принципы привязки данных.
  • Какие требования предъявляются к источнику и приемнику данных при выполнении привязки данных?
  • Какие свойства используются при привязке к объекту, не являющемуся элементом?
  • Как определяется направление привязки данных?
  • Для чего в технологии WPF используется преобразование данных?
  • Каким требованиям должен соответствовать класс, используемый как конвертор данных?
  • Сколько правил может содержать коллекция Binding.ValidationRules?
  • Вернуться к учебному плану