Маркировка перенаправленных событий как обработанных и обработка классов (WPF .NET)

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

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

Важно!

Документация по рабочему столу для .NET 7 и .NET 6 находится в стадии разработки.

Необходимые компоненты

Для понимания статьи нужно иметь общее представление о перенаправленных событиях и прочитать статью Общие сведения о перенаправленных событиях. Чтобы понимать примеры в этой статье полезно познакомиться с языком XAML и знать, как создавать приложения Windows Presentation Foundation (WPF).

Когда перенаправленные события следует помечать как обработанные

Как правило, только один обработчик должен давать значительный отклик для каждого перенаправленного события. Следует избегать использования системы перенаправленных событий для обеспечения существенного отклика между несколькими обработчиками. Определение того, что представляет собой существенный отклик, является субъективным и зависит от вашего приложения. Общее руководство:

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

Некоторые элементы управления WPF подавляют события уровня компонентов, которые не нуждаются в дальнейшей обработке, помечая их как обработанные. Если требуется обработать событие, помеченное как обработанное элементом управления, см. статью Обход скрытия события элементами управления.

Чтобы пометить событие как обработанное, задайте свойству Handled в его данных события значение true. Хотя значение false можно вернуть, необходимость в этом должна возникать редко.

Пары перенаправленных событий предварительного просмотра и восходящей маршрутизация

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

Обработчики перенаправленных событий вызываются в порядке, соответствующем стратегии маршрутизации события:

  1. Событие предварительного просмотра перемещается от корневого элемента приложения вниз к элементу, вызвавшему перенаправленное событие. Обработчики событий предварительного просмотра, прикрепленные к корневому элементу приложения, вызываются первыми, а затем вызываются обработчики, прикрепленные к последующим вложенным элементам.
  2. После завершения события предварительного просмотра сопряженное событие восходящей маршрутизации перемещается от элемента, вызвавшего перенаправленное событие, к корневому элементу приложения. Обработчики событий восходящей маршрутизации, прикрепленные к тому же элементу, который вызвал перенаправленное событие, вызываются первыми, а затем вызываются обработчики, прикрепленные к последующим родительским элементам.

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

Так как каждая пара событий предварительного просмотра и восходящей маршрутизации совместно использует один и тот же экземпляр данных событий, если перенаправленное событие предварительного просмотра помечается как обработанное, то также будет обрабатываться сопряженное ему событие восходящей маршрутизации. Если перенаправленное событие восходящей маршрутизации помечается как обработанное, оно не повлияет на сопряженное событие предварительного просмотра, так как событие предварительного просмотра завершено. Будьте осторожны, помечая пары событий предварительного просмотра и восходящей маршрутизации как обработанные. Обработанное входное событие предварительного просмотра не вызывает обычно зарегистрированные обработчики событий для остальной части маршрута туннелирования, и сопряженное событие восходящей маршрутизации не будет вызываться. Обработанное входное событие восходящей маршрутизации не вызывает обычно зарегистрированные обработчики событий для остальной части маршрута восходящей маршрутизации.

Обработчики перенаправленных событий экземпляров и классов

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

Обработчики событий экземпляров

Обработчики экземпляров можно прикрепить к объектам или элементам XAML, напрямую вызывая метод AddHandler. Перенаправленные события WPF реализуют оболочку событий среды CLR, которая использует метод AddHandler для прикрепления обработчиков событий. Так как синтаксис атрибутов XAML для прикрепления обработчиков событий приводит к вызову оболочки событий CLR, даже прикрепление обработчиков в XAML разрешается вызовом AddHandler. Для обработанных событий:

  • Обработчики, прикрепленные с использованием синтаксиса атрибутов XAML или общей сигнатуры AddHandler, не вызываются.
  • Вызываются обработчики, прикрепленные с помощью перегрузки метода AddHandler(RoutedEvent, Delegate, Boolean) с параметром handledEventsToo, которому задано значениеtrue. Эта перегрузка доступна в редких случаях, когда необходимо реагировать на обработанные события. Например, некоторые элементы в дереве элементов помечают событие как обработанное, но другие элементы далее по маршруту событий должны реагировать на это обработанное событие.

В следующем примере XAML добавляется пользовательский элемент управления с именемcomponentWrapper, который заключает элемент TextBox с именем componentTextBox в элемент StackPanel с именем outerStackPanel. Обработчик событий экземпляров для события PreviewKeyDown прикрепляются к componentWrapper с помощью синтаксиса атрибутов XAML. В результате обработчик экземпляров реагирует только на необработанные события туннелирования PreviewKeyDown, вызванные элементом componentTextBox.

<StackPanel Name="outerStackPanel" VerticalAlignment="Center">
    <custom:ComponentWrapper
        x:Name="componentWrapper"
        TextBox.PreviewKeyDown="HandlerInstanceEventInfo"
        HorizontalAlignment="Center">
        <TextBox Name="componentTextBox" Width="200" />
    </custom:ComponentWrapper>
</StackPanel>

Конструктор MainWindow прикрепляет обработчик экземпляров для события восходящей маршрутизации KeyDown к componentWrapper, используя перегрузку UIElement.AddHandler(RoutedEvent, Delegate, Boolean) с параметром handledEventsToo, которому задано значениеtrue. В результате обработчик событий экземпляров реагирует как на необработанное, так и на обработанные события.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // Attach an instance handler on componentWrapper that will be invoked by handled KeyDown events.
        componentWrapper.AddHandler(KeyDownEvent, new RoutedEventHandler(Handler.InstanceEventInfo),
            handledEventsToo: true);
    }

    // The handler attached to componentWrapper in XAML.
    public void HandlerInstanceEventInfo(object sender, KeyEventArgs e) => 
        Handler.InstanceEventInfo(sender, e);
}
Partial Public Class MainWindow
    Inherits Window

    Public Sub New()
        InitializeComponent()

        ' Attach an instance handler on componentWrapper that will be invoked by handled KeyDown events.
        componentWrapper.[AddHandler](KeyDownEvent, New RoutedEventHandler(AddressOf InstanceEventInfo),
                                      handledEventsToo:=True)
    End Sub

    ' The handler attached to componentWrapper in XAML.
    Public Sub HandlerInstanceEventInfo(sender As Object, e As KeyEventArgs)
        InstanceEventInfo(sender, e)
    End Sub

End Class

Реализация ComponentWrapper в коде программной части показана в следующем разделе.

Обработчики событий статических классов

Обработчики событий статических классов можно прикреплять, вызывая метод RegisterClassHandler в статическом конструкторе класса. Каждый класс в иерархии классов может зарегистрировать собственный обработчик статического класса для каждого перенаправленного события. В результате может быть несколько обработчиков статических классов, вызываемых для одного и того же события в любом заданном узле на маршруте события. При построении маршрута события к нему добавляются все обработчики статических классов для каждого узла. Порядок вызова обработчиков статических классов в узле начинается с наиболее производного обработчика статических классов, за которым следуют обработчики статических классов из каждого последующего базового класса.

Обработчики событий статических классов, зарегистрированные с помощью перегрузки RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) с параметром handledEventsToo, которому задано значение true, будут реагировать как на необработанные, так и обработанные перенаправленные событий.

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

В следующем примере кода показана иерархия классов для пользовательского элемента управления ComponentWrapper, на который ссылается предыдущий код XAML. Класс ComponentWrapper является производным от класса ComponentWrapperBase, который, в свою очередь, является производным от класса StackPanel. Метод RegisterClassHandler, используемый в статическом конструкторе классов ComponentWrapper и ComponentWrapperBase, регистрирует обработчик событий статических классов для каждого из этих классов. Система событий WPF вызывает обработчик статических классов ComponentWrapper раньше обработчика статических классов ComponentWrapperBase.

public class ComponentWrapper : ComponentWrapperBase
{
    static ComponentWrapper()
    {
        // Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(typeof(ComponentWrapper), KeyDownEvent, 
            new RoutedEventHandler(Handler.ClassEventInfo_Static));
    }

    // Class event handler that overrides a base class virtual method.
    protected override void OnKeyDown(KeyEventArgs e)
    {
        Handler.ClassEventInfo_Override(this, e);

        // Call the base OnKeyDown implementation on ComponentWrapperBase.
        base.OnKeyDown(e);
    }
}

public class ComponentWrapperBase : StackPanel
{
    // Class event handler implemented in the static constructor.
    static ComponentWrapperBase()
    {
        EventManager.RegisterClassHandler(typeof(ComponentWrapperBase), KeyDownEvent, 
            new RoutedEventHandler(Handler.ClassEventInfoBase_Static));
    }

    // Class event handler that overrides a base class virtual method.
    protected override void OnKeyDown(KeyEventArgs e)
    {
        Handler.ClassEventInfoBase_Override(this, e);

        e.Handled = true;
        Debug.WriteLine("The KeyDown routed event is marked as handled.");

        // Call the base OnKeyDown implementation on StackPanel.
        base.OnKeyDown(e);
    }
}
Public Class ComponentWrapper
    Inherits ComponentWrapperBase

    Shared Sub New()
        ' Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(GetType(ComponentWrapper), KeyDownEvent,
                                          New RoutedEventHandler(AddressOf ClassEventInfo_Static))
    End Sub

    ' Class event handler that overrides a base class virtual method.
    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        ClassEventInfo_Override(Me, e)

        ' Call the base OnKeyDown implementation on ComponentWrapperBase.
        MyBase.OnKeyDown(e)
    End Sub

End Class

Public Class ComponentWrapperBase
    Inherits StackPanel

    Shared Sub New()
        ' Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(GetType(ComponentWrapperBase), KeyDownEvent,
                                          New RoutedEventHandler(AddressOf ClassEventInfoBase_Static))
    End Sub

    ' Class event handler that overrides a base class virtual method.
    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        ClassEventInfoBase_Override(Me, e)

        e.Handled = True
        Debug.WriteLine("The KeyDown event is marked as handled.")

        ' Call the base OnKeyDown implementation on StackPanel.
        MyBase.OnKeyDown(e)
    End Sub

End Class

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

Обработчики событий классов переопределения

Некоторые базовые классы визуальных элементов предлагают пустые виртуальные методы On<имя события> и OnPreview<имя события> для каждого из своих публичных перенаправленных входных событий. Например, UIElement реализует обработчики виртуальных событий OnKeyDown и OnPreviewKeyDown, а также многих других. Обработчики виртуальных событий базовых классов можно переопределить для реализации обработчиков событий классов переопределения для своих производных классов. Например, можно добавить обработчик классов переопределения для события DragEnter в любом производном классе UIElement, переопределив виртуальный метод OnDragEnter. Переопределение виртуальных методов базовых классов — этот способ реализации обработчиков классов проще, чем регистрация обработчиков классов в статическом конструкторе. В рамках переопределения можно вызывать события, инициировать характерную для классов логику, чтобы изменять свойства элементов в экземплярах, помечать события как обработанные или выполнять другую логику обработки событий.

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

В предыдущем примере кода виртуальный метод базового класса OnKeyDown переопределяется в обоих классах ComponentWrapper и ComponentWrapperBase. Так как система событий WPF вызывает только обработчик событий классов переопределения ComponentWrapper.OnKeyDown, этот обработчик использует base.OnKeyDown(e) для вызова обработчика событий классов переопределения ComponentWrapperBase.OnKeyDown, который, в свою очередь, использует base.OnKeyDown(e) для вызова виртуального метода StackPanel.OnKeyDown. Порядок событий в предыдущем примере кода:

  1. Обработчик экземпляров, прикрепленные к componentWrapper, активируется перенаправленным событием PreviewKeyDown.
  2. Обработчик статических классов, прикрепленный к componentWrapper, активируется перенаправленным событием KeyDown.
  3. Обработчик статических классов, прикрепленный к componentWrapperBase, активируется перенаправленным событием KeyDown.
  4. Обработчик классов переопределения, прикрепленный к componentWrapper, активируется перенаправленным событием KeyDown.
  5. Обработчик классов переопределения, прикрепленный к componentWrapperBase, активируется перенаправленным событием KeyDown.
  6. Перенаправленное событие KeyDown помечается как обработанное.
  7. Обработчик экземпляров, прикрепленные к componentWrapper, активируется перенаправленным событием KeyDown. Обработчик был зарегистрирован с параметром handledEventsToo, которому задано значение true.

Подавление входных событий в составных элементах управления

Некоторые составные элементы управления подавляют входные события на уровне компонентов, чтобы заменить их настраиваемым событием высокого уровня, которое несет больше информации или подразумевает более конкретное поведение. Составной элемент управления по определению состоит из нескольких фактических элементов управления или базовых классов элементов управления. Классический пример — элемент управления Button, который преобразует различные события мыши в перенаправленное событиеClick. Базовый класс Button  — это класс ButtonBase, который косвенно наследуется из UIElement. Большая часть инфраструктуры событий, необходимой для обработки входных данных управления, доступна на уровне UIElement. UIElement предоставляет несколько событий Mouse, таких как MouseLeftButtonDown и MouseRightButtonDown. UIElement также реализует пустые виртуальные методы OnMouseLeftButtonDown и OnMouseRightButtonDown как предварительно зарегистрированные обработчики классов. ButtonBase переопределяет эти обработчики классов, а в обработчике переопределения задает свойству Handled значение true и вызывает событие Click. Конечный результат для большинства прослушивателей — скрытие событий MouseLeftButtonDown и MouseRightButtonDown, а событие высокого уровня Click остается видимым.

Временное решение для обхода подавления входных событий

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

  • Прикрепите обработчики, вызвав метод UIElement.AddHandler(RoutedEvent, Delegate, Boolean) и задав параметру handledEventsToo значение true. Этот подход требует присоединения обработчика событий к коду программной части после получения ссылки на объект для элемента, к которому он будет присоединен.

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

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

См. также