Главная страница

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


Скачать 186 Kb.
НазваниеСвойства зависимости. Маршрутизируемые события
Анкорlet. docx
Дата19.05.2021
Размер186 Kb.
Формат файлаdoc
Имя файлаLektsia_2021_WPF_8_Svoystva_zavisimosti_Marshrutiziruemye_sobyti.doc
ТипДокументы
#206903

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


Рассмотренные свойства элементов, как например, Width или Height, являются не просто стандартными свойствами языка C#. Они фактически скрывают свойства зависимостей или dependency property. Без свойств зависимостей были бы невозможны многие ключевые особенности WPF, как привязка данных, стили, анимация и т.д.

Рассмотрим, как они определяются. Возьмем, к примеру, элемент TextBlock, у которого есть свойство Text:

public class TextBlock : FrameworkElement, IContentHost, IAddChildInternal, IServiceProvider

{

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

    public static readonly DependencyProperty TextProperty;

 

    static TextBlock()

    {

        // Регистрация свойства

     TextProperty = DependencyProperty.Register(

           "Text",

           typeof(string),

           typeof(TextBlock),

           new FrameworkPropertyMetadata(

               string.Empty,

               FrameworkPropertyMetadataOptions.AffectsMeasure |

               FrameworkPropertyMetadataOptions.AffectsRender,

               new PropertyChangedCallback(OnTextChanged),

               new CoerceValueCallback(CoerceText)));

        // остальной код

    }

 // Обычное свойство .NET  - обертка над свойством зависимостей

    public string Text

    {

        get { return (string) GetValue(TextProperty); }

        set { SetValue(TextProperty, value); }

    } 

     

    private static object CoerceText(DependencyObject d, object value)

    {

        //.................................

    }

    // метод, вызываемый при изменении значения свойства

    private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

    {

        //...............................

    }

    // остальной код

}

Статическое свойство TextProperty является свойством зависимостей, представляя объект System.Windows.DependencyProperty. По соглашениям по именованию все свойства зависимостей представляют статические публичные поля (public static) с суффиксом Property.

Затем в статическом конструкторе класса происходит регистрация свойства с помощью метода DependencyProperty.Register(), в который передается ряд параметров:

  • имя свойства (в данном случае "Text"). Как правило, соответствует названию свойства зависимостей без суффикса Property

  • тип свойства (в данном случае string)

  • тип, который владеет свойством - собственно тот тип, в котором свойство определено или в данном случае тип TextBlock

  • Необязательный параметр  FrameworkPropertyMetadata устанавливает дополнительные настройки свойства

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

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

new FrameworkPropertyMetadata(string.Empty,

    FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,

    new PropertyChangedCallback(OnTextChanged), new CoerceValueCallback(CoerceText)))

Этот конструктор устанавливает в качестве значения по умолчанию пустую строку, указывает, что при изменении значения элемент будет перерисовываться (собственно, что мы и видим - при изменении значения свойства Text новое значение отображается), и при изменении значения свойства будут вызываться методы OnTextChanged и CoerceText.

Далее после регистрации свойства идет обертка - обычное свойство .NET, которое имеет сеттер и геттер и которое вызывает методы GetValue и SetValue для получения и установки значения соответственно. Эти методы определены в классе System.Windows.DependencyObject, который является базовым для всех элементов WPF, в том числе и для TextBlock.

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

Кроме того, DependencyObject поддерживает еще ряд свойств для управления свойствами зависимостей:

  • ClearValue: очищает значение объекта DependencyProperty

  • InvalidateProperty: повторно вычисляет действующее значение объекта DependencyProperty

  • ReadLocalValue: считывает значение объекта DependencyProperty

Например:

TextBlock textBlock = new TextBlock();

textBlock.Text = "Hello";

string text = (string) textBlock.ReadLocalValue(TextBlock.TextProperty); // Hello

textBlock.ClearValue(TextBlock.TextProperty); // теперь значение отсутствует

Провайдеры свойств


В WPF определение значения свойств представляет многоэтапный процесс. На различных стадиях этого процесса применяются различные провайдеры свойств, которые помогают получить значение для свойства зависимостей.

При извлечении значения свойства система использует 10 провайдеров:

  1. Получение локального значение свойства (то есть то, которое установлено разработчиком через XAML или через код C#)

  2. Вычисление значения с помощью триггеров из шаблона родительского элемента

  3. Вычисление значения из шаблона родительского элемента

  4. Вычисление значения с помощью триггеров из применяемых стилей

  5. Вычисление значения с помощью триггеров из применяемого шаблона

  6. Получение значения из сеттеров применяемых стилей

  7. Вычисление значения с помощью триггеров из применяемых тем

  8. Получение значения из сеттеров применяемых тем

  9. Получение унаследованного значения (если свойство FrameworkPropertyMetadata.Inherits имеет значение true)

  10. Извлечение значения по умолчанию, которое устанавливается через объект FrameworkPropertyMetadata

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

Все десять перечисленных этапов обычно объединяются в одну стадию - получение базового значения.

Но кроме получения значения есть еще процесс установки значения. Он вовлекает ряд дополнительных шагов:

  1. Вышеописанные 10 шагов - получение базового значения

  2. Если значение свойства, полученное на шаге 1, представляет собой сложное выражение (например, выражение привязки данных), то WPF вычисляет значение этого выражения и получает конкретный результат

  3. Если для свойства применяется анимация, то далее она используется для получения нового значения

  4. После получения значения WPF применяет делегат  CoerceValueCallback, который задается в объекте  FrameworkPropertyMetadata при регистрации свойства. С помощью метода, на который указывает данный делегат, проверяется, входит ли значение в диапазон допустимых значений. Если не входит, то в заисимости от логики задается новое значение

  5. В конце применяется делегат ValidateValueCallback (если он указан при регистрации свойства в качестве пятого параметра), который выполняет валидацию. Метод, на который ссылается делегат, возвращает true при прохождении валидации. Иначе возвращается false и генерируется исключение

Маршрутизация событий


Чтобы взаимодействовать с элементами управления, нам надо использовать модель событий. WPF предлагает новую концепцию событий - маршрутизированные события (routed events).

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

  • События клавиатуры

  • События мыши

  • События стилуса

  • События сенсорного экрана/мультитач

  • События жизненного цикла

Подключение обработчиков событий


Подключить обработчики событий можно декларативно в файле xaml-кода, а можно стандартным способом в файле отделенного кода.

Декларативное подключение:

x:Name="Button1" Content="Click" Click="Button_Click" />

И подключим еще один обработчик в коде, чтобы при нажатии на кнопку срабатывали сразу два обработчика:

public partial class MainWindow : Window

{

    public MainWindow()

    {

        InitializeComponent();

        Button1.Click += Button1_Click;

    }

    // обработчик, подключаемый в XAML

    private void Button_Click(object sender, RoutedEventArgs e)

    {

        MessageBox.Show("Hi from Button_Click");

    }

    // обработчик, подключаемый в конструкторе

    private void Button1_Click(object sender, RoutedEventArgs e)

    {

        MessageBox.Show("Hi from Button1_Click");

    }

}

Определение маршрутизированных событий


Определение маршрутизированных событий отличается от стандартного определения событий в языке C#. Для определения маршрутизированных событий в классе создавалось статическое поле по типу  RoutedEvent:

public static RoutedEvent СобытиеEvent

Это поле, как правило, имеет суффикс Event. Затем это событие регистрируется в статическом конструкторе.

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

К примеру, возьмем встроенные класс ButtonBase - базовый класс для всех кнопок, который определяет ряд событий, в том числе событие Click:

public abstract class ButtonBase : ContentControl, ...

{

    // определение событие

    public static readonly RoutedEvent ClickEvent;

     

    static ButtonBase()

    {

        // регистрация маршрутизированного события

        ButtonBase.ClickEvent = EventManager.RegisterRoutedEvent("Click",

            RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));

        //................................

    }

    // обертка над событием

    public event RoutedEventHandler Click

    {

        add

        {

            // добавление обработчика

            base.AddHandler(ButtonBase.ClickEvent, value);

        }

        remove

        {

            // удаление обработчика

            base.RemoveHandler(ButtonBase.ClickEvent, value);

        }

    }

  // остальное содержимое класса

}

Маршрутизированные события регистрируются с помощью метода  EventManager.RegisterRoutedEvent(). В этот метод передаются последовательно имя события, тип события (поднимающееся, прямое, опускающееся), тип делегата, предназначенного для обработки события, и класс, который владеет этим событием.

Маршрутизация событий


Модель событий WPF отличается от событий WinForms не только декларативным подключением. События, возникнув на одном элементе, могут обрабатываться на другом. События могут подниматься и опускаться по дереву элементов.

Так, маршрутизируемые события делятся на три вида:

  • Прямые (direct events) - они возникают и отрабытывают на одном элементе и никуда дальше не передаются. Действуют как обычные события.

  • Поднимающиеся (bubbling events) - возникают на одном элементе, а потом передаются дальше к родителю - элементу-контейнеру и далее, пока не достигнет наивысшего родителя в дереве элементов.

  • Опускающиеся, туннельные (tunneling events) - начинает отрабатывать в корневом элементе окна приложения и идет далее по вложенным элементам, пока не достигнет элемента, вызвавшего это событие.

Все маршрутизируемые события используют класс  RoutedEventArgs  (или его наследников), который представляет доступ к следующим свойствам:

  • Source: элемент логического дерева, являющийся источником события.

  • OriginalSource: элемент визуального дерева, являющийся источником события. Обычно то же самое, что и Source

  • RoutedEvent: представляет имя события

  • Handled: если это свойство установлено в True, событие не будет подниматься и опускаться, а ограничится непосредственным источником.

Поднимающиеся события


Допустим, у нас имеется такая разметка xaml:


        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

        xmlns:local="clr-namespace:EventsApp"

        mc:Ignorable="d"

        Title="MainWindow" Height="250" Width="400">

    

        

            

            

        


        

            

        


        

    




В данном случае мы получаем следующее дерево элементов:



Три элемента имеют привязку к одному обработчику события, которое возникает при нажатии правой кнопки мыши или тачпада. Определим этот обработчик в файле кода C#:

Обработчик в данном случае выводит информацию о событии в текстовый блок.

И так как это событие MouseDown является поднимающимся, то при нажатии правой кнопкой мыши на элемент самого нижнего уровня - Ellipse, событие MouseDown будет подниматься к контейнерам и отработает три раза последовательно для всех элементов Ellipse, Button, StackPanel:


Туннельные события


Туннельные события действуют прямо противоположным способом. Как правило, все они начинаются со слова Preview. Возьмем выше приведенный пример и заменим событие MouseDown на PreviewMouseDown



    



Нажмем на элемент Ellipse. Тогда событие сначала отработает на элементе StackPanel и затем последовательно на элементе Button и закончится на элементе Ellipse.


Прикрепляемые события (Attached events)


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

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



    

    

    

    



Обработчик для прикрепляемого события задается в формате Имя_класса.Название_события="Обработчик".

Здесь атрибут RadioButton.Checked="RadioButton_Click" закрепляет все радиокнопки на StackPanel за одним обработчиком. Тогда в коде можно прописать:

private void RadioButton_Click(object sender, RoutedEventArgs e)

{

    RadioButton selectedRadio = (RadioButton)e.Source;

    textBlock1.Text = "Вы выбрали: " + selectedRadio.Content.ToString();

}

И на текстовый блок выводится выбранный пункт.

Также обработчик для прикрепляемого события мы можем задать в коде c#:

menuSelector.AddHandler(RadioButton.CheckedEvent, new RoutedEventHandler(RadioButton_Click));

События клавиатуры


К событиям клавиатуры можно отнести следующие события:

Событие

Тип события

Описание

KeyDown

Поднимающееся

Возникает при нажатии клавиши

PreviewKeyDown

Туннельное

Возникает при нажатии клавиши

KeyUp

Поднимающееся

Возникает при освобождении клавиши

PreviewKeyUp

Туннельное

Возникает при освобождении клавиши

TextInput

Поднимающееся

Возникает при получении элементом текстового ввода (генерируется не только клавиатурой, но и стилусом)

PreviewTextInput

Туннельное

Возникает при получении элементом текстового ввода

Большинство событий клавиатуры (KeyUp/PreviewKeyUp, KeyDown/PreviewKeyDown) принимает в качестве аргумента объект KeyEventArgs, у которого можно отметить следующие свойства:

  • Key позволяет получить нажатую или отпущенную клавишу

  • SystemKey позволяет узнать, нажата ли системная клавиша, например, Alt

  • KeyboardDevice получает объект KeyboardDevice, представляющее устройство клавиатуры

  • IsRepeat указывает, что клавиша удерживается в нажатом положении

  • IsUp и IsDown указывает, была ли клавиша нажата или отпущена

  • IsToggled указывает, была ли клавиша включена - относится только к включаемым клавишам Caps Lock, Scroll Lock, Num Lock

Например, обработаем событие KeyDown для текстового поля и выведем данные о нажатой клавише в текстовый блок:


        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

        xmlns:local="clr-namespace:EventsApp"

        mc:Ignorable="d"

        Title="MainWindow" Height="250" Width="400">

    

        

            

            

        


        

            

        


        

    




А в файле кода пропишем обработчик TextBox_KeyDown:

private void TextBox_KeyDown(object sender, KeyEventArgs e)

{

    textBlock1.Text += e.Key.ToString();

}

Здесь в текстовый блок добавляется текстовое представление нажатой клавиши в текстовом поле:



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

Если нам надо отловить нажатие какой-то опредленной клавиши, то мы можем ее проверить через перечисление Key:

if (e.Key == Key.OemQuotes)

    textBlock1.Text += "'"; // добавляем кавычки

else

    textBlock1.Text += e.Key.ToString();

Объект KeyboardDevice позволяет нам получить ряд дополнительных данных о собыиях клавиатуры через ряд свойств и методов:

  • Modifiers позволяет узнать, какая клавиша была нажата вместе с основной (Ctrl, Shift, Alt)

  • IsKeyDown() определяет, была ли нажата определенная клавиша во время события

  • IsKeyUp() позволяет узнать, была ли отжата определенная клавиша во время события

  • IsKeyToggled() позволяет узнать, была ли во время события включена клавиша Caps Lock, Scroll Lock или Num Lock

  • GetKeyStates() возвращает одно из значений перечисления KeyStates, которое указывает на состояние клавиши

Пример использования KeyEventArgs при одновременном нажатии двух клавиш Shift и F1:

private void TextBox_KeyDown(object sender, KeyEventArgs e)

{

    if (e.KeyboardDevice.Modifiers == ModifierKeys.Shift && e.Key == Key.F1)

        MessageBox.Show("HELLO");

}

События TextInput/PreviewTextInput в качестве параметра принимают объект TextCompositionEventArgs. Из его свойств стоит отметить, пожалуй, только свойство Text, которое получает введенный текст, именно текст, а не текстовое представление клавиши. Для этого добавим к текстовому полю обработчик:

Height="40" Width="260" PreviewTextInput="TextBox_TextInput" />И определим обработчик в файле кода:

private void TextBox_TextInput(object sender, TextCompositionEventArgs e)

{

    textBlock1.Text += e.Text;

}

Причем в данном случае я обрабатываю именно событие PreviewTextInput, а не TextInput, так как элемент TextBox подавляет событие TextInput, и вместо него генерирует событие TextChanged. Для большинства других элементов управления, например, кнопок, событие TextInput прекрасно срабатывает.

Валидация текстового ввода


События открывают нам большой простор для валидации текстового ввода. Нередко при вводе используются те или иные ограничения: нельзя вводить цифровые символы или, наоборот, можно только цифровые и т.д. Посмотрим, как мы можем провети валидацию ввода. К примеру, возьмем ввод номера телефона. Сначала зададим обработку двух событий в xaml:

PreviewTextInput="TextBox_PreviewTextInput" PreviewKeyDown="TextBox_PreviewKeyDown"  />

И определим в файле кода обработчики:

private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)

{

    int val;

    if (!Int32.TryParse(e.Text, out val) && e.Text!="-")

    {

        e.Handled = true; // отклоняем ввод

    }

}

 

private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)

{

    if (e.Key == Key.Space)

    {

        e.Handled = true; // если пробел, отклоняем ввод

    }

}

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

Сами обработчики проверяют ввод и если ввод соответствует критериям, то он отклоняется с помощью установки e.Handled = true. Тем самым мы говорим, что событие обработано, а введенные текстовые сиволы не будут появляться в текстовом поле. Конкретно в данном случае пользователь может вводить только цифровые символы и пробел в соответствии с форматом телефонного номера.

События мыши и фокуса


В WPF для мыши определены следующие события:

Событие

Тип события

Описание

GotMouseCapture

Поднимающееся

Возникает при получении фокуса с помощью мыши

LostMouseCapture

Поднимающееся

Возникает при потере фокуса с помощью мыши

MouseEnter

Прямое

Возникает при вхождении указателя мыши в пределы элемента

MouseLeave

Прямое

Возникает, когда указатель мыши выходит за пределы элемента

MouseLeftButtonDown

Поднимающееся

Возникает при нажатии левой кнопки мыши

PreviewMouseLeftButtonDown

Туннельное

Возникает при нажатии левой кнопки мыши

MouseLeftButtonUp

Поднимающееся

Возникает при освобождении левой кнопки мыши

PreviewMouseLeftButtonUp

Туннельное

Возникает при освобождении левой кнопки мыши

MouseRightButtonDown

Поднимающееся

Возникает при нажатии правой кнопки мыши

PreviewMouseRightButtonDown

Туннельное

Возникает при нажатии правой кнопки мыши

MouseRightButtonUp

Поднимающееся

Возникает при освобождении правой кнопки мыши

PreviewMouseRightButtonUp

Туннельное

Возникает при освобождении правой кнопки мыши

MouseDown

Поднимающееся

Возникает при нажатии кнопки мыши

PreviewMouseDown

Туннельное

Возникает при нажатии кнопки мыши

MouseUp

Поднимающееся

Возникает при освобождении кнопки мыши

PreviewMouseUp

Туннельное

Возникает при освобождении кнопки мыши

MouseMove

Поднимающееся

Возникает при передвижении указателя мыши

PreviewMouseMove

Туннельное

Возникает при передвижении указателя мыши

MouseWheel

Поднимающееся

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

PreviewMouseWheel

Туннельное

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

Если вдруг мы не хотим, чтобы элемент генерировал события мыши, то мы можем у него установить свойство IsHitTestVisible="False"

Большинство обработчиков событий мыши в качестве параметра получают объект MouseEventArgs, имеющий ряд интересных свойств и методов, которые мы можем использовать:

  • ButtonState: возвращает состояние кнопки мыши. Хранит одно из значений перечисления MouseButtonState:

    • Pressed: кнопка наата

    • Released: кнопка отжата

  • ChangedButton: получает кнопку, которая ассоциирована с данным событием. Хранит одно из значений перечисления MouseButton:

    • Left: левая кнопка мыши

    • Middle: средняя кнопка мыши

    • Right: правая кнопка мыши

    • XButton1: дополнительная кнопка мыши

    • XButton2: дополнительная кнопка мыши

  • ClickCount: хранит число сделанных нажатий

  • LeftButton: хранит состояние левой кнопки мыши в виде MouseButtonState

  • MiddleButton: хранит состояние средней кнопки мыши в виде MouseButtonState

  • RightButton: хранит состояние правой кнопки мыши в виде MouseButtonState

  • XButton1: хранит состояние первой дополнительной кнопки

  • XButton2: хранит состояние второй дополнительной кнопки

  • GetPosition(): метод, который возвращает координаты нажатия в виде объекта Point







написать администратору сайта