NoesisGUI

Events in NoesisGUI

github Tutorial Data

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

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 by that class. The same for the rest of classes.

Note

Find more information about events in the Events Overview document.

Subscription

Two different ways can be used to subscribe to events in NoesisGUI: with and without code-behind. There is also a third alternative using Commands and Interactivity that totally decouples the view from the model.

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" VerticalAlignment="Center" 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 x:Class="MyGrid"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  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>

Lifetime events

Any Noesis object that derives from FrameworkElement class will undergo Initialized, Loaded, Reloaded and Unloaded. Understanding when those events happen is very important to properly initialize any control class. Here is some background on how these events work.

EventsTutorialImg1.png

Initialized event

The Initialized event says that an element has been created and its properties have all been set, and as a consequence this usually fires on children before their parent. So when Initialized is raised on an element, its whole sub-tree is likely initialized, but its parent is not. The Initialized event is fired only once, typically when a XAML gets loaded. This event corresponds to the IsInitialized property.

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

C++
class MainWindow final: public UserControl
{
public:
    MainWindow()
    {
        Initialized() += MakeDelegate(this, &MainWindow::OnInitialized);
        InitializeComponent();
    }

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

    void MainWindow::OnInitialized(BaseComponent*, const EventArgs&)
    {
        SetDataContext(MakePtr<ViewModel>());
    }
}
C#
public partial class MainWindow: UserControl
{
    public MainWindow()
    {
        WeakReference weak = new WeakReference(this);
        this.Initialized += (s, e) => { ((MainWindow)weak.Target)?.OnInitialized(s, e); };

        this.InitializeComponent();
    }

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

    private void OnInitialized(object sender, EventArgs args)
    {
        this.DataContext = new ViewModel();
    }
}

Loaded event

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.

Symmetrically to the Loaded event, the Unloaded event occurs when the element is removed from within an element tree of loaded elements. Therefore these events can be raised more than once, every time an element gets added or removed from the UI tree.

Note

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

Reloaded event

When a FrameworkElement derived object is Hot-Reloaded, the event Reloaded is fired. Hot-Reloading happens when your application is running and a XAML is modified without restarting, for example when using the Inspector. 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.

Weak Events in C#

Event registrations in C# are the most common cause of memory leaks. When subscribing to events of a control in C# a strong reference is stored in the event handler delegate. In Noesis this creates a circular reference that makes it impossible to release the control, producing memory leaks.

C#
public partial class MainWindow : UserControl
{
    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 this scenario we recommend creating an intermediary WeakReference to call the appropriate method, as you can see in the following code.

C#
public partial class MainWindow : UserControl
{
    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) { }
}
© 2017 Noesis Technologies