NoesisGUI

Events in NoesisGUI

github Tutorial Data

NoesisGUI is an event driven framework where all controls expose events you can subscribe to, which means that your application will be notified when they occur and you may react to them.

There are many types of events, but some of the most commonly used are there to respond to the user's interaction. On most controls you will find events like KeyDown, KeyUp, MouseDown, MouseUp, TouchDown, TouchUp. For example, in the events section of the UIElement documentation you can find a list of all the events exposed. Similar documentation is available for the rest of classes.

Subscription

Three different ways can be used to subscribe to events in NoesisGUI:

  • Direct subscription using delegates
  • Code-behind events
  • View-Model patterns

Although the first two options are a bit easier to use, only the third option enables true separation between the view and the model and it is the recommended pattern.

Direct subscription

The easiest way to subscribe to an event is by directly adding a callback to it by using a delegate. The object is reached using FindName, so you need to use the x:Name keyword to set the name of the desired instance. For example:

<Grid
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Button x:Name="Button" Width="100" Content="Click me"/>
</Grid>
C++
Ptr<Grid> root = Noesis::GUI::LoadXaml<Grid>("Grid.xaml");
Button* button = root->FindName<Button>("Button");
button->Click() += [](BaseComponent* sender, const RoutedEventArgs& args)
{
    printf("Button was clicked");
};
C#
Grid root = (Grid)Noesis.GUI.LoadXaml("Grid.xaml");
Button button = (Button)root.FindName("Button");
button.Click += (object sender, RoutedEventArgs args) =>
{
    System.Console.WriteLine("Button was clicked");
};

Code-behind subscription

The alternative to direct subscription is using a code-behind class and connecting events in the XAML by using method names. These method names need to be implemented in the code-behind class using the correct event signature. For example:

<StackPanel
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  x:Class="MyGrid" VerticalAlignment="Center">
  <Button Width="100" Content="Click me" Click="OnButton1Click"/>
  <Button Width="100" Content="Click me" Click="OnButton2Click"/>
</StackPanel>

Note

Read the Extending Noesis tutorial to know more about implementing code-behind classes

FrameworkElement exposes the virtual function ConnectEvent that is invoked for each hooked event when the XAML is loaded. You need to override that function accordingly. In C++ the macro NS_CONNECT_EVENT is provided as a helper to easily implement ConnectEvent. You must use it for each event you want to connect to.

C++
class MyGrid: public Grid
{
public:
    MyGrid()
    {
        InitializeComponent();
    }

private:
    void InitializeComponent()
    {
        GUI::LoadComponent(this, "Grid.xaml");
    }

    bool ConnectEvent(BaseComponent* source, const char* event, const char* handler) override
    {
        NS_CONNECT_EVENT(Button, Click, OnButton1Click);
        NS_CONNECT_EVENT(Button, Click, OnButton2Click);

        return false;
    }

    void OnButton1Click(BaseComponent* sender, const RoutedEventArgs& args)
    {
        printf("Button1 was clicked");
    }

    void OnButton2Click(BaseComponent* sender, const RoutedEventArgs& args)
    {
        printf("Button2 was clicked");
    }

    NS_IMPLEMENT_INLINE_REFLECTION_(MyGrid, Grid)
};
C#
public class MyGrid: Grid
{
    public MyGrid()
    {
        InitializeComponent();
    }

    private void InitializeComponent()
    {
        GUI.LoadComponent(this, "Grid.xaml");
    }

    protected override bool ConnectEvent(object source, string eventName, string handlerName)
    {
        if (eventName == "Click" && handlerName == "OnButton1Click")
        {
            ((Button)source).Click += this.OnButton1Click;
            return true;
        }

        if (eventName == "Click" && handlerName == "OnButton2Click")
        {
            ((Button)source).Click += this.OnButton2Click;
            return true;
        }

        return false;
    }

    private void OnButton1Click(object sender, RoutedEventArgs args)
    {
        System.Console.WriteLine("Button1 was clicked");
    }

    private void OnButton2Click(object sender, RoutedEventArgs args)
    {
        System.Console.WriteLine("Button2 was clicked");
    }
}

Note that sometimes using FindName is not a valid option and using code-behind member functions is the only way to connect to events. For example when using a DataTemplate the named elements in the visual tree cannot be accessed using FindName because the data template is replicated for each item.

<Grid x:Class="MyGrid"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Grid.Resources>
    <DataTemplate x:Key="BookItemTemplate">
      <StackPanel Orientation="Horizontal">
        <Button Content="+" Click="OnButtonClick" />
        <TextBlock Text="{Binding Title}"/>
      </StackPanel>
    </DataTemplate>
  </Grid.Resources>
  <ListBox ItemsSource="{Binding Books}" ItemTemplate="{StaticResource BookItemTemplate}"/>
</Grid>

Commands

For a pure MVVM approach to event subscription we recommend using a combination of EventTrigger and InvokeCommandAction. Triggers and Actions are part of the Interactivity package.

<Grid
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:b="http://schemas.microsoft.com/xaml/behaviors">
  <TextBlock Text="{Binding Name}">
    <b:Interaction.Triggers>
      <b:EventTrigger EventName="MouseEnter">
        <b:InvokeCommandAction Command="{Binding MouseInTextCommand}"/>
      </b:EventTrigger>
    </b:Interaction.Triggers>
  </TextBlock>
</Grid>

Several elements, like Button, already provide support for commands without using Interactivity.

<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
  <Button Width="100" Content="Click me" Command="{Binding ClickCommand}"/>
</Grid>

Lifetime Events

Any Noesis framework-level element deriving from FrameworkElement class will undergo Initialized, Loaded, Reloaded and Unloaded. Understanding when these events happen is very important to properly initialize any control class. Here is some background on how these events work.

EventsTutorialImg1.png
C++
class MainWindow final: public Window
{
public:
    MainWindow()
    {
        Initialized() += [](BaseComponent*, const EventArgs&)
        {
            printf("Initialized");
        };

        Loaded() += [](BaseComponent*, const RoutedEventArgs&)
        {
            printf("Loaded");
        };

        Reloaded() += [](BaseComponent*, const RoutedEventArgs&)
        {
            printf("Reloaded");
        };

        Unloaded() += [](BaseComponent*, const RoutedEventArgs&)
        {
            printf("Unloaded");
        };

        InitializeComponent();
    }

private:
    void MainWindow::InitializeComponent()
    {
        GUI::LoadComponent(this, "MainWindow.xaml");
    }
};
C#
public partial class MainWindow: Window
{
    public MainWindow()
    {
        this.Initialized += (s, e) => { Console.WriteLine("Initialized"); };
        this.Loaded += (s, e) => { Console.WriteLine("Loaded"); };
        this.Reloaded += (s, e) => { Console.WriteLine("Reloaded"); };
        this.Unloaded += (s, e) => { Console.WriteLine("Unloaded"); };

        this.InitializeComponent();
    }

    private void InitializeComponent()
    {
        GUI.LoadComponent(this, "MainWindow.xaml");
    }
}

Initialized

Initialized is raised first, and roughly corresponds to the initialization of the object by the call to its constructor. Because the event happens in response to initialization, you are guaranteed that all properties of the object are set. (An exception is expression usages such as dynamic resources or binding; these will be unevaluated expressions). As a consequence of the requirement that all properties are set, the sequence of Initialized being raised by nested elements that are defined in markup appears to occur in order of deepest elements in the element tree first, then parent elements toward the root. This order is because the parent-child relationships and containment are properties, and therefore the parent cannot report initialization until the child elements that fill the property are also completely initialized.

When you are writing handlers in response to the Initialized event, you must consider that there is no guarantee that all other elements in the element tree (either logical tree or visual tree) around where the handler is attached have been created, particularly parent elements. Member variables may be null, or data sources might not yet be populated by the underlying binding (even at the expression level).

Instead of having code in the constructor of the class, you should always use the Initialized event.

Loaded

Sometimes the Initialized event is not enough. For example, you may want to know the ActualWidth of an element, but when Initialized is fired, the ActualWidth value hasn't been calculated yet. Or you may want to look at the value of a data-bound property, but that hasn't been resolved yet either.

To deal with this, the Loaded event says that the element is not only built and initialized, but layout has run on it, data has been bound, it's connected to a View and you're on the verge of being rendered. When that point is reached, the Loaded event is broadcasted, starting at the root of the tree. This event corresponds to the IsLoaded property.

The mechanism by which the Loaded event is raised is different than Initialized. The Initialized event is raised element by element, without a direct coordination by a completed element tree. By contrast, the Loaded event is raised as a coordinated effort throughout the entire element tree. When all elements in the tree are in a state where they are considered loaded, the Loaded event is first raised on the root element. The Loaded event is then raised successively on each child element.

Note

If you're not sure which event to use, 'Initialized' or 'Loaded', use the 'Loaded' event; it's more often the right choice.

Reloaded

When a FrameworkElement derived object is Hot-Reloaded, the event Reloaded is fired. Hot-Reloading happens when your application is running and XAMLs are modified without restarting. In most scenarios, handling the Reloaded event is not necessary but sometimes it is useful to know when this is happening to react accordingly and avoid breaking parts of the UI.

Unloaded

Symmetrically to the Loaded event, the Unloaded event occurs when the element is removed from within an element tree of loaded elements. When Unloaded is raised and handled, the element that is the event source parent or any given element upwards in the logical or visual trees may have already been unset, meaning that data binding, resource references, and styles may not be set to their normal or last known run-time value.

Loaded and Unloaded events can be raised more than once, every time an element gets added or removed from the View.

Weak Events (C#)

Ordinarily, attaching an event handler for a listener causes the listener to have an object lifetime that is influenced by the object lifetime of the source (unless the event handler is explicitly removed). But in certain circumstances, you might want the object lifetime of the listener to be controlled by other factors, such as whether it currently belongs to the visual tree of the application, and not by the lifetime of the source. Whenever the source object lifetime extends beyond the object lifetime of the listener, the normal event pattern leads to a memory leak: the listener is kept alive longer than intended.

C#
public partial class MainWindow : Window
{
    public MainWindow()
    {
        // MEMORY LEAK! These delegates keep strong references to the MainWindow instance
        this.MouseDown += OnMouseDown;
        this.MouseUp += OnMouseUp;
        this.MouseMove += OnMouseMove;

        this.InitializeComponent();
    }

    private void InitializeComponent()
    {
        GUI.LoadComponent(this, "MainWindow.xaml");
    }

    private void OnMouseDown(object sender, MouseButtonEventArgs args) { }
    private void OnMouseUp(object sender, MouseButtonEventArgs args) { }
    private void OnMouseMove(object sender, MouseEventArgs args) { }
}

To avoid creating strong references from the event source to the event listener we recommend using WeakReference instances. This pattern allows the listener to register for and receive the event without affecting the object lifetime characteristics of the listener in any way. The listener can be garbage collected or otherwise destroyed, and the source can continue without retaining noncollectible handler references to a now destroyed object.

C#
public partial class MainWindow : Window
{
    public MainWindow()
    {
        // Break circular reference using a WeakReference as intermediary
        WeakReference weak = new WeakReference(this);
        this.MouseDown += (s, e) => { ((MainWindow)weak.Target)?.OnMouseDown(s, e); };
        this.MouseUp += (s, e) => { ((MainWindow)weak.Target)?.OnMouseUp(s, e); };
        this.MouseMove += (s, e) => { ((MainWindow)weak.Target)?.OnMouseMove(s, e); };

        this.InitializeComponent();
    }

    private void InitializeComponent()
    {
        GUI.LoadComponent(this, "MainWindow.xaml");
    }

    private void OnMouseDown(object sender, MouseButtonEventArgs args) { }
    private void OnMouseUp(object sender, MouseButtonEventArgs args) { }
    private void OnMouseMove(object sender, MouseEventArgs args) { }
}

Routed events

Routed events propagate through an element tree and invoke event handlers on multiple listeners in the tree. Several Noesis events are routed events, such as BaseButton.Click. This section discusses basic routed event concepts and offers guidance on when and how to respond to routed events.

Noesis applications typically contain many elements, which were either declared in XAML or instantiated in code. An application's elements exists within its element tree. Depending on how a routed event is defined, when the event is raised on a source element it:

  • Bubbles up through element tree from the source element to the root element.
  • Tunnels down through the element tree from the root element to the source element.
  • Doesn't travel through the element tree, and only occurs on the source element.

Consider the following partial element tree:

<Border Height="30" Width="200" BorderBrush="Gray" BorderThickness="1">
  <StackPanel Background="LightBlue" Button.Click="YesNoCancelButton_Click">
    <Button Name="YesButton">Yes</Button>
    <Button Name="NoButton">No</Button>
    <Button Name="CancelButton">Cancel</Button>
  </StackPanel>
</Border>

Each of the three buttons is a potential Click event source. When one of the buttons is clicked, it raises the Click event that bubbles up from the button to the root element. The Button and Border elements don't have event handlers attached, but the StackPanel does. Possibly other elements higher up in the tree that aren't shown also have Click event handlers attached. When the Click event reaches the StackPanel element, the Noesis event system invokes the YesNoCancelButton_Click handler that's attached to it. The following image illustrates the event route for the Click event.

EventsTutorialImg3.png

Implementation

A routed event is backed by an instance of the RoutedEvent class, and processed by the Noesis event system. The RoutedEvent instance is typically stored as a public static member of the class that registered it. That class is referred to as the event "owner" class. Typically, a routed event implements an identically named event "wrapper". The event wrapper implements '+=' and '-=' operators to add or remove event handlers. This wrapper calls the routed event AddHandler and RemoveHandler methods.

The following example registers the DownloadCompleted routed event with the bubbling strategy and implements an event wrapper. It also exposes a virtual function, OnDownloadCompleted, that raises the event and allows inheritors to handle the event and cancel it. The event is raised by calling UIElement.RaiseEvent.

C++
class DownloadControl: public Control
{
public:
    typedef Delegate<void (BaseComponent*, const RoutedEventArgs&)> DownloadCompletedEventHandler;

    UIElement::RoutedEvent_<DownloadCompletedEventHandler> DownloadCompleted()
    {
        return UIElement::RoutedEvent_<DownloadCompletedEventHandler>(this, DownloadCompletedEvent);
    }

    static const RoutedEvent* DownloadCompletedEvent;

protected:
    virtual void OnDownloadCompleted(const RoutedEventArgs& args)
    {
        RaiseEvent(args);
    }

private:
    void RaiseDownloadCompletedEvent()
    {
        RoutedEventArgs args(this, DownloadCompletedEvent);
        OnDownloadCompleted(args);
    }

    NS_IMPLEMENT_INLINE_REFLECTION(DownloadControl, Control, "DownLoadControl")
    {
        UIElementData* data = NsMeta<UIElementData>(TypeOf<SelfClass>());
        data->RegisterEvent(DownloadCompletedEvent, "DownloadCompleted", RoutingStrategy_Bubble);
    }
};
C#
public class DownloadControl: Control
{
    public static readonly RoutedEvent DownloadCompletedEvent = EventManager.RegisterRoutedEvent
    (
        name: "DownloadCompleted",
        routingStrategy: RoutingStrategy.Bubble,
        handlerType: typeof(RoutedEventHandler),
        ownerType: typeof(DownloadControl)
    );

    public event RoutedEventHandler DownloadCompleted
    {
        add { AddHandler(DownloadCompletedEvent, value); }
        remove { RemoveHandler(DownloadCompletedEvent, value); }
    }

    public void RaiseDownloadCompletedEvent()
    {
        RoutedEventArgs args = new RoutedEventArgs(DownloadCompletedEvent, this);
        OnDownloadCompleted(args);
    }

    protected virtual void OnDownloadCompleted(RoutedEventArgs args)
    {
        RaiseEvent(args);
    }
}

Routing strategies

Routed events use one of three routing strategies:

  • Bubbling: Initially, event handlers on the event source are invoked. The routed event then routes to successive parent elements, invoking their event handlers in turn, until it reaches the element tree root. Most routed events use the bubbling routing strategy. Bubbling routed events are generally used to report input or state changes from composite controls or other UI elements.
  • Tunneling: Initially, event handlers at the element tree root are invoked. The routed event then routes to successive child elements, invoking their event handlers in turn, until it reaches the event source. Events that follow a tunneling route are also referred to as Preview events. Input events are generally implemented as a preview and bubbling pairs.
  • Direct: Only event handlers on the event source are invoked.

Input Events

By convention, routed events that follow a tunneling route have a name that's prefixed with "Preview". The Preview prefix signifies that the preview event completes before the paired bubbling event starts. Input events often come in pairs, with one being a preview event and the other a bubbling routed event. For example, PreviewKeyDown and KeyDown. The event pairs share the same instance of event data, which for PreviewKeyDown and KeyDown is of type KeyEventArgs. Occasionally, input events only have a bubbling version, or only a direct routed version. Our API documentation clarifies the routing strategy for each event. For example, the list of events corresponding to Button.

Input events that come in pairs are implemented so that a single user action from an input device, such as a mouse button press, will raise the preview and bubbling routed events in sequence. First, the preview event is raised and completes its route. On completion of the preview event, the bubbling event is raised and completes its route. The RaiseEvent method call in the implementing class that raises the bubbling event reuses the event data from the preview event for the bubbling event.

A preview input event that's marked as handled won't invoke any normally registered event handlers for the remainder of the preview route, and the paired bubbling event won't be raised. This handling behavior is useful for composite controls designers who want hit-test based input events or focus-based input events to be reported at the top-level of their control. Top-level elements of the control have the opportunity to class-handle preview events from control subcomponents in order to "replace" them with a top-level control-specific event.

To illustrate how input event processing works, consider the following input event example. In the following tree illustration, leaf element #2 is the source of both the PreviewMouseDown and MouseDown paired events:

EventsTutorialImg2.png

The order of event processing following a mouse-down action on leaf element #2 is:

  1. PreviewMouseDown tunneling event on the root element.
  2. PreviewMouseDown tunneling event on intermediate element #1.
  3. PreviewMouseDown tunneling event on leaf element #2, which is the source element.
  4. MouseDown bubbling event on leaf element #2, which is the source element.
  5. MouseDown bubbling event on intermediate element #1.
  6. MouseDown bubbling event on the root element.

The routed event handler delegate provides references to both the object that raised the event and the object where the handler was invoked. The object that originally raised the event is reported by the Source property in the event data. The object where the handler was invoked is reported by the Sender parameter. For any given routed event instance, the object that raised the event doesn't change as the event travels through the element tree, but the Sender does. In steps 3 and 4 of the preceding diagram, the Source and Sender are the same object.

If your input event handler completes the application-specific logic needed to address the event, you should mark the input event as handled. Typically, once an input event is marked Handled, handlers further along the event route aren't invoked.

Handled Events

All routed events share a common base class for event data, which is the RoutedEventArgs class. The RoutedEventArgs class defines the boolean Handled property. The purpose of the Handled property is to let any event handler along the event route to mark the routed event as handled. To mark an event as handled, set the value of Handled to true in the event handler code.

The value of Handled affects how a routed event is processed as it travels along the event route. If Handled is true in the shared event data of a routed event, then handlers attached to other elements further along the event route typically won't be invoked for that particular event instance. For most common handler scenarios, marking an event as handled effectively stops subsequent handlers along the event route from responding to that particular event instance.

© 2017 Noesis Technologies