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 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 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      VerticalAlignment="Center"
      x:Class="MyGrid">
  <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 if for each event you want to connect to.

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

private:
    void InitializeComponent()
    {
        Noesis::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)
    {
        NsMeta<TypeId>("MyGrid");
    }
};
C#
public class MyGrid: Grid
{
    public MyGrid()
    {
        InitializeComponent();
    }

    private void InitializeComponent()
    {
        Noesis.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>

Loaded and the Initialized event

In Noesis, controls have both an Initialized event and a Loaded event. Understanding when those events happen is very important to properly initialize any control class. Here is some background on how these events work.

Initialized Event

The Initialized event says just 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 typically fired when the XAML for a sub-tree is 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 Noesis::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()
    {
        this.Initialized += OnInitialized;
        this.InitializeComponent();
    }

    private void InitializeComponent()
    {
        Noesis.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 calculated 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.

Note

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

© 2017 Noesis Technologies