WPF quick start series - in depth analysis of WPF event mechanism

1, Introduction

WPF not only creates a new dependency attribute system, but also replaces the ordinary one with more advanced routing event function NET events.

Routing events are events with stronger propagation ability -- they can bubble up and tunnel down the element tree, and are processed by the event handler along the propagation path. As with dependent properties, you can use the traditional event method to route events. Although routing events are used in the same way as traditional events, it is important to understand how they work.

2, Detailed description of routing events

For NET, you should be familiar with it. Event refers to the message sent by the object to notify the code when something happens. Routing events in WPF allow events to be delivered. For example, routing events allow a click event from a toolbar button to be passed to the toolbar before being processed, and then to the window containing the toolbar. So now the question comes, how can I define a routing event in WPF?

2.1 how to define routing events

Now that there is a problem, it is natural to solve it. Before defining a dependency attribute by ourselves, we must first learn how to define it in the WPF framework, and then try to define a dependency attribute by ourselves in the way defined in the WPF framework. Next, use the Reflector tool to view the WPF Button How the Click event of the button is defined.

Because the Click event of the Button button is inherited from ButtonBase Base class, so let's directly view the definition of Click event in ButtonBase. The specific definition code is as follows:

[Localizability(LocalizationCategory.Button), DefaultEvent("Click")]
public abstract class ButtonBase : ContentControl, ICommandSource
{
    // Event definition
    public static readonly RoutedEvent ClickEvent;
   
    // Event registration
    static ButtonBase()
    {
        ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
        CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(ButtonBase), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(ButtonBase.OnCommandChanged)));
      .......
    }

    // Traditional event packaging
    public event RoutedEventHandler Click
    {
        add
        {
            base.AddHandler(ClickEvent, value);
        }
        remove
        {
            base.RemoveHandler(ClickEvent, value);
        }
    }
    .......
}
 

It can be seen from the above code that the definition of routing event is similar to that of dependent attribute. Routing event is represented by read-only static field and passed through a static constructor EventManager.RegisterRoutedEvent Function register, and pass a NET event definition.

Now that you know how routing events are defined and implemented in the WPF framework, it's natural to define a routing event yourself.

2.2 shared routing events

As with dependent properties, definitions of routing events can be shared between classes. That is to realize the inheritance of routing events. for example UIElement Class and ContentElement class both use the MouseUp event, but the MouseUp event is generated by System.Windows.Input.Mouse Class defined. UIElement class and ContentElement class only pass RouteEvent.AddOwner Method reuses the MouseUp event. You can find the following code in the static constructor of UIElement class:

static UIElement()
{
    _typeofThis = typeof(UIElement);
    
     PreviewMouseUpEvent =  Mouse.PreviewMouseUpEvent.AddOwner(_typeofThis);
    MouseUpEvent = Mouse.MouseUpEvent.AddOwner(_typeofThis);
}

2.3 raising and handling routing events

Although routing events through traditional NET event, but the routing event is not through NET event. Instead, the RaiseEvent method is used to trigger the event. All elements inherit this method from the UIElement class. The following code is the code that triggers the routing event in the specific ButtonBase class:

1  protected virtual void OnClick()
2 {
3        RoutedEventArgs e = new RoutedEventArgs(ClickEvent, this);
4        base.RaiseEvent(e);// Trigger routing events through RaiseEvent method
5        CommandHelpers.ExecuteCommandSource(this);
6 }

And in WinForm, Button The Click event of is triggered by calling the delegate. The specific implementation code is as follows:

1  protected virtual void OnClick(EventArgs e)
2         {
3             EventHandler handler = (EventHandler)base.Events[EventClick];
4             if (handler != null)
5             {
6                 handler(this, e); // Directly call the delegate to trigger the event
7             }
8         }

For the processing of routing events, the same as the original WinForm method, you can directly connect an event handler in XAML. The specific implementation code is as follows:

<TextBlock Margin="3" MouseUp="SomethingClick" Name="tbxTest">
    text label
</TextBlock>
// Background cs code
private void SomethingClick(object sender, MouseButtonEventArgs e)
{
}

At the same time, the event handler can also be connected through background code. The specific implementation code is as follows:

    tbxTest.MouseUp += new MouseButtonEventHandler(SomethingClick);
    // Or omit the delegate type
    tbxTest.MouseUp += SomethingClick;

3, The particularity of routing events

The particularity of routing events lies in their transitivity. There are three kinds of routing events in WPF.

  • And ordinary NET event is similar to direct event. It originates from one element and is not passed to other elements. For example, the MouseEnter event (triggered when the mouse moves over an element) is a direct routing event.
  • Bubbling events passed up the containment hierarchy. For example, the MouseDown event is a bubbling routing event. It is triggered first by the clicked element, then by the parent element of the element, and so on until WPF reaches the top of the element tree.
  • Tunneling events that are passed down in the containment hierarchy. For example, PreviewKeyDown is a tunnel routing event. Press a key on a window, first the window, then the more specific container, until you reach the element that has focus when you press the key.

Since there are three forms of routing events, how can we distinguish which specific routing events belong to? When the method of event registration is used EventManager.RegisterEvent Method to register a routing event RoutingStrategy Enumeration values to identify the event behavior that you want to apply to the event.

3.1 bubbling routing events

The following code demonstrates the event bubbling process:

<Window x:Class="BubbleLabelClick.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" MouseUp="SomethingClick">
    <Grid Margin="3" MouseUp="SomethingClick">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue"
               BorderBrush="Black" BorderThickness="2" MouseUp="SomethingClick">
            <StackPanel MouseUp="SomethingClick">
                <TextBlock Margin="3" MouseUp="SomethingClick" Name="tbxTest">
                    Image and text label
                </TextBlock>
                <Image Source="pack://application:,,,/BubbleLabelClick;component/face.png" Stretch="None"  MouseUp="SomethingClick"/>
                <TextBlock Margin="3" MouseUp="SomethingClick">
                    Courtest for the StackPanel
                </TextBlock>            
            </StackPanel>
        </Label>
        
        <ListBox Grid.Row="1" Margin="3" Name="lstMessage">     
        </ListBox>
        <CheckBox Margin="5" Grid.Row="2" Name="chkHandle">Handle first event</CheckBox>
        <Button Click="cmdClear_Click"  Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button>
    </Grid>
</Window>

Its background code is:

 1     public partial class MainWindow : Window
 2     {
 3         public MainWindow()
 4         {
 5             InitializeComponent();
 6             
 7         }
 8 
 9         private int eventCounter = 0;
10 
11         private void SomethingClick(object sender, RoutedEventArgs e)
12         {
13             eventCounter++;
14             string message = "#" + eventCounter.ToString() + ":\r\n" + "Sender: " + sender.ToString() + "\r\n" +
15                 "Source: " + e.Source + "\r\n" +
16                 "Original Source: " + e.OriginalSource;
17             lstMessage.Items.Add(message);
18             e.Handled = (bool)chkHandle.IsChecked;
19         }
20 
21         private void cmdClear_Click(object sender, RoutedEventArgs e)
22         {
23             eventCounter = 0;
24             lstMessage.Items.Clear();
25         }
26     }

The effect diagram after operation is as follows:

After clicking the smiling face image in the window, the running result of the program is shown in the figure below.

It can be seen from the above figure that the MouseUp event passes 5 levels from bottom to top until the end of the window level. In addition, if the Handle first event check box is selected, the SomethingClicked method will routedeventargs If the handled property is set to true, it indicates that the event has been processed and the event will stop bubbling upward. Therefore, only Image events can be seen in the list. The specific operation results are shown in the following figure:

And Click in the list box or the blank space of the window. At this time, the MouseUp event will only appear once. But Click one place. When you Click the Clear List button, the MouseUp event will not be raised at this time. This is because the button contains some special processing codes. These codes will suspend the MouseUp event (that is, if the MouseUp event will not be triggered, the corresponding event handler will not be called) and raise a higher-level Click event. At the same time, the Handled flag is set to true (this means that the Handled flag will be set to true when the Click event is triggered), so as to prevent the MouseUp event from continuing to pass up.

yiifans, a blogger, pointed out that there is a mistake in the above. When the Handled = true is set, whether it is a bubbling or tunnel event, it will continue to spread, but the corresponding event will not be processed again. The reason why the above error explanation is not deleted here, but explained separately here is to emphasize, because the WPF programming dictionary also says that it will prevent propagation. If you want to continue to respond to the corresponding event, you can AddHandler Method. At this time, you can remove the registration of MouseUp in XAMLStackPanel and register MouseUp events through background code. The specific implementation code is as follows:

            // stackpanel1 is the name of the StackPanel
            stackpanel1.AddHandler(UIElement.MouseUpEvent, new RoutedEventHandler(SomethingClick), true);

The reason why the upload will continue is that the call stack can be found by setting a breakpoint in SomethingClick event handler. The specific screenshot of stack call is as follows:

From the above figure, you can know what operations are performed before SomethingClick is called, including routedeventhandleinfo The implementation code of invokehandler method is the key to this problem. Next, check the source code of this method through Reflector. The specific source code is as follows:

internal void InvokeHandler(object target, RoutedEventArgs routedEventArgs)
{
    if (!routedEventArgs.Handled || this._handledEventsToo)
    {
        if (this._handler is RoutedEventHandler)
        {
            ((RoutedEventHandler) this._handler)(target, routedEventArgs);
        }
        else
        {
            routedEventArgs.InvokeHandler(this._handler, target);
        }
    }
}

In the above code, the red mark is the key code to explain this problem. Before triggering the event handler, the Handled property and handleEventsToo field of RoutedEventArgs will be checked. In this way, we can fully understand that when Handle=true, in fact, the routing event will still be passed. Only when it is passed to the corresponding event handler, it is only because Handle is true and_ handleEventsToo is false, which causes the event handler not to run. If the event handler is registered through AddHandler (RoutedEvent, Delegate, Boolean), the_ handleEventToo is explicitly set to true, so even if the Handle is true, the event handler of the element will still execute, because the if condition is true at this time.

3.2 tunnel routing events

Tunnel routing events work in the same way as bubble routing events, but in the opposite direction. That is, if a tunnel routing event is triggered in the above example, if you click on the image, the window will trigger the tunnel routing event first, then the Grid control, then the StackPanel panel, and so on until the actual source, that is, the image in the label, is reached.

Read the introduction above. Tunnel routing events must be quite easy to understand. It is the opposite of the way bubble routing events are delivered. But how do we distinguish tunnel routing events? The identification of tunnel routing events is quite easy, because tunnel routing events begin with the word Preview. Moreover, WPF generally defines bubble routing events and tunnel routing events in pairs. This means that if a bubbling MouseUp event is found, the corresponding PreviewMouseUp is a tunnel routing event. In addition, tunnel routing events are always triggered before bubbling routing events.

Another thing to note is that if the tunnel routing event is marked as processed, the bubbling routing event will not occur. This is because the two events share the same instance of the RoutedEventArgs class. Tunnel routing events are very useful for performing some preprocessing operations. For example, scenarios such as performing specific operations according to specific keys on the keyboard or filtering out specific mouse operations can be processed in the tunnel routing event handler. The following example demonstrates the tunneling of the PreviewKeyDown event. The XAML code is shown below.

<Window x:Class="TunneleEvent.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" PreviewKeyDown="SomeKeyPressed">
    <Grid Margin="3" PreviewKeyDown="SomeKeyPressed">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue"
               BorderBrush="Black" BorderThickness="2" PreviewKeyDown="SomeKeyPressed">
            <StackPanel>
                <TextBlock Margin="3" PreviewKeyDown="SomeKeyPressed">
                    Image and text label
                </TextBlock>
                <Image Source="face.png" Stretch="None" PreviewMouseUp="SomeKeyPressed"/>
                <DockPanel Margin="0,5,0,0"  PreviewKeyDown="SomeKeyPressed">
                    <TextBlock Margin="3" 
                     PreviewKeyDown="SomeKeyPressed">
                        Type here:
                    </TextBlock>
                    <TextBox PreviewKeyDown="SomeKeyPressed" KeyDown="SomeKeyPressed"></TextBox>
                </DockPanel>
            </StackPanel>
        </Label>

        <ListBox Grid.Row="1" Margin="3" Name="lstMessage">
        </ListBox>
        <CheckBox Margin="5" Grid.Row="2" Name="chkHandle">Handle first event</CheckBox>
        <Button Click="cmdClear_Click"  Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button>
    </Grid>
</Window>

The corresponding background cs code is implemented as follows:

 1 public partial class MainWindow : Window
 2     {
 3         public MainWindow()
 4         {
 5             InitializeComponent();
 6         }
 7 
 8         private int eventCounter = 0;
 9 
10         private void SomeKeyPressed(object sender, RoutedEventArgs e)
11         {
12             eventCounter++;
13             string message = "#" + eventCounter.ToString() + ":\r\n" +
14                 " Sender: " + sender.ToString() + "\r\n" +
15                 " Source: " + e.Source + "\r\n" +
16                 " Original Source: " + e.OriginalSource + "\r\n" +
17                 " Event: " + e.RoutedEvent;
18             lstMessage.Items.Add(message);
19             e.Handled = (bool)chkHandle.IsChecked;
20         }
21 
22         private void cmdClear_Click(object sender, RoutedEventArgs e)
23         {
24             eventCounter = 0;
25             lstMessage.Items.Clear();
26         }
27     }

The effect diagram after running the program is as follows:

When a key is pressed in a text box, the event is first triggered in the window and then passed down the hierarchy. The specific operation results are shown in the figure below:

If the PreviewKeyDown event is marked as handled anywhere, the bubbling KeyDown event will not be triggered. When the Handle first event check box is checked, when a key is pressed in the input box, only one record will be displayed in the listbox. Because the PreviewKeyDown event triggered by the window has marked the tunnel routing event as processed, the PreviewKeyDown event will not be passed down, so only one record triggered by MainWindow will be displayed at this time. Moreover, at this time, you can notice that the corresponding character on the key we pressed is not displayed in the input box, because the KeyDown event in the Textbox is not triggered at this time, because the processing of changing the content of the text box is processed in the KeyDown event. The specific operation results are shown in the figure below:

3.3 additional events

In the above example, because all elements support MouseUp and PreviewKeyDown events. However, many controls have their own special events. For example, the click event of a button is defined in any other class. Suppose there is a scenario where the StackPanel panel contains a bunch of buttons and you want to handle all the click events of these buttons in an event handler. The first idea is to associate the click event of each button with the same event handler. However, click event supports event bubbling, so there is a better solution. Click events can be associated with higher-level elements to handle the click events of all buttons. The specific XAML code implementation is as follows:

<Window x:Class="AttachClickEvent.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel Margin="3" Button.Click="DoSomething">
        <Button Name="btn1">Button 1</Button>
        <Button Name="btn2">Button 2</Button>
        <Button Name="btn3">Button 3</Button>
    </StackPanel>
</Window>

You can also associate additional events in the code, but you need to use uielement Addhandle method instead of the + = operator. The specific implementation code is as follows:

 // The StackPanel panel is named ButtonsPanel
 ButtonsPanel.AddHandler(Button.ClickEvent, new RoutedEventHandler(DoSomething));

4, WPF event lifecycle

The start of WPF event life cycle is similar to that in WinForm. The life cycle of events in WPF is explained in detail below.

The FrameworkElement class implements ISupportInitialize Interface, which provides two methods for controlling the initialization process. The first is BeginInit Method, which is called immediately after the element is instantiated. After the BeginInit method is called, the XAML parser sets the attributes of all elements and adds content. The second is the EndInit method, which is called after initialization. Triggered at this time Initialized event. More precisely, the XAML parser is responsible for calling the BeginInit method and EndInit method.

When creating a window, each element branch is initialized in a bottom-up manner. This means that deeply nested elements are initialized before their containers. When an initialization event is raised, you can ensure that all the elements below the current element in the element tree have been initialized. However, the container containing the current element has not been initialized, and it cannot be assumed that other parts of the window have been initialized. After each element is initialized, it needs to be laid out, styled and, if necessary, data bound in their container.

Once the initialization process is completed, the Loaded event will be raised. The Loaded event and the Initialized event occur in reverse. That is, a window containing all elements raises the Loaded event first, and then the deeper nested elements. When all elements raise the Loaded event, the window becomes visible and the elements have been rendered. The following figure lists some lifecycle events.

5, Summary

At this point, the introduction of WPF routing events ends. This paper first introduces the definition of routing events, then introduces three kinds of routing events. WPF includes direct routing events, bubbling routing events and tunnel routing events, and finally introduces the life cycle of WPF events. In a later article, we will introduce element binding in WPF.

Download all source code of this article: WPFRouteEventDemo.zip

Tags: WPF

Posted by racer x on Mon, 02 May 2022 22:49:47 +0300