NoesisGUI

C++ Architecture Guide

Although NoesisGUI is based on WPF, a C# framework, many of the core concepts exposed in that API do not directly translate to C++. For example, reflection a key concept for data binding is not available in C++. There is also no notion of garbage collection in C++. With the purpose of creating a C++ API easier to use, NoesisGUI exposes a few helpers that contribute to make the transition from C# easier and more efficient. Please read carefully the following sections if you are going to use Noesis in C++ language.

Headers and Namespaces

NoesisGUI headers are grouped by modules (prefixed by Ns). The whole API is exposed in the namespace Noesis.

#include <NsCore/Stream.h>
#include <NsCore/File.h>
#include <NsCore/Log.h>
#include <NsCore/Hash.h>
#include <NsCore/SpinMutex.h>
#include <NsRender/Texture.h>
#include <NsRender/RenderTarget.h>
#include <NsGui/TextureProvider.h>

using namespace Noesis;

In case you don't want to manually pick each header we also provide a single header, 'Noesis_pch.h', exposing all NoesisGUI API. This is specially useful when using precompiled headers.

#include <Noesis_pch.h>

using namespace Noesis;

NOTE

Noesis Application framework, a helper library not part of core is exposed in a different namespace, NoesisApp, and its headers are not included in Noesis_pch.h.

Reference Counting

In Noesis each object contains a reference counter that control its lifetime. The reference counter is initialized to 1 when the instance is created using the new operator. This counter is increased or decreased each time AddReference() or Release() is invoked. If an object's reference count reaches zero it is automatically destroyed using the operator delete.

// Create a new instance. Reference counter is '1' at this point
Box* box = new Box(500.0f, 400.0f);

//...

// Release instance and delete it if reference counter reaches zero
box->Release();

You should never destroy Noesis instances using the operator delete because there could be more objects holding references and you would be ignoring them. You should use Release() instead. There are a few asserts in the implementation of BaseRefCounted detecting this kind of scenarios.

Due to the same reason, instances must not be created in the stack because they are automatically destroyed when they go out of the scope creating possible dangling pointers from other existing references.

References to components can be manually handled using AddReference() and Release(). For example:

Box* box0 = new Box(500.0f, 400.0f);    // Ref '1' - Created

Box* box1 = box0;
box0->AddReference();                   // Ref '1' -> '2'

//...

box0->Release();                        // Ref '2' -> '1'
box1->Release();                        // Ref '1' -> '0' - Destroyed

To avoid manually handling the reference counter, Noesis provides Ptr<> a smart pointer that automatically handles lifetime of objects by doing AddReference() and Release(). Ptr<> overloads certain operators in order to behave similar to a pointer but ensuring that objects are deleted when they are no longer needed, to avoid memory leaks.

Ptr<Box> box0 = *new Box(500.0f, 400.0f);
Ptr<Box> box1(box0);

//...

// box0 and box1 are automatically destroyed when they go out of scope

BaseRefCounted objects start with reference counter set to 1. When they are stored inside a Ptr<> the counter is increased by 1 again. As you can see in the example above, to avoid having to do a manual Release() to cancel that extra reference, Ptr<> supports being constructed from a reference. In this case the reference counter is not incremented again. We also provide a better and more flexible alternative using MakePtr:

Ptr<Box> box0 = MakePtr<Box>(500.0f, 400.0f);
Ptr<Box> box1(box0);

//...

// box0 and box1 are automatically destroyed when they go out of scope

Ptr<> implicitly converts to raw pointers, so most of the times its usage is totally transparent and you don't need to care about the details.

// Read XAMLs from current working directory
GUI::SetXamlProvider(MakePtr<LocalXamlProvider>("."));

// Renderer initialization with an OpenGL device
Ptr<RenderDevice> device = GLFactory::CreateDevice();
_view->GetRenderer()->Init(device);

Boxing

Sometimes basic types or structs need to be converted to BaseComponent. This adds the overhead of the polymorphism to the type, the reference counter and the allocation in the heap, but in some scenarios this is needed, for example when implementing value converters.

The mechanism of converting a stack-type instance to a component is called boxing and is performed using Boxing::Box():

Ptr<BaseComponent> boxed = Boxing::Box(50.0f);

The reverse operation is called unboxing. A boxed value is unboxed using Boxing::Unbox():

float val = Boxing::CanUnbox<float>(boxed) ? Boxing::Unbox<float>(boxed) : 0.0f;

Although boxing is optimized internally (for example using pools to avoid memory allocations) it should be avoided whenever possible because it cannot be considered a 'fast' operation.

The following snippet implement a thousand converter. Note how the input value is given as a BaseComponent instance that need to be unboxed and how the result is boxed to a BaseComponent instance.

bool ThousandConverter::TryConvert(BaseComponent* value, const Type*, BaseComponent*, Ptr<BaseComponent>& result)
{
    if (Boxing::CanUnbox<int>(value))
    {
        char str[16];
        int v = Boxing::Unbox<int>(value);
        String::FormatBuffer(str, sizeof(str), "%.2f K", (float)v / 1000.0f);
        result = Boxing::Box(str);
        return true;
    }

    return false;
}

Nullables

Nullable types are instances of the Noesis::Nullable<T> struct. A nullable type can represent the correct range of values for its underlying types, plus an additional null value. For example, a Nullable<bool> can be assigned the values 'true', 'false' or 'null'. Use the HasValue and GetValue functions to test for null and retrieve the value, as shown in the following example:

Nullable<bool> a(nullptr);
assert(!a.HasValue());
assert(a == nullptr);

Nullable<bool> b(false);
assert(b.HasValue());
assert(b == false);
assert(b.GetValue() == false);

NOTE

You can assign a value and compare against a value with nullables just as you would for and ordinary value type.

Objects based on nullable types are only boxed if the object is non-null. If HasValue is false, the object reference is assigned to null instead of boxing:

Ptr<BaseComponent> obj = Boxing::Box(Nullable<float>(nullptr));
assert(obj == nullptr);

Delegates

Delegate is a generic implementation for callbacks. Delegates in Noesis are implemented very similar to .NET delegates. Delegates ensure that the callback method is type-safe. Delegates also integrate the ability to call multiple methods sequentially and support the calling of static methods as well as instance methods.

Declaring Delegates

A delegate is declared using a function signature. For example:

/// A delegate with void return and two parameters: int and float
Delegate<void (int, float)> d;

Non-member Methods

The following code add a static method to the delegate and invoke it:

void Print(int size, float value)
{
    printf("%d %4.3f", size, value);
}

void main()
{
    // Create and Bind the delegate
    Delegate<void (int, float)> d = &Print;

    // Invoke
    d(500, 10.0f);
}

Member Methods

In a similar way, instance methods can be bound to the delegate:

struct Printer
{
    void Print(const char* string) const
    {
        printf("%s", string);
    }
};

void main()
{
    // Create the instance
    Printer printer;

    // Create and Bind the delegate
    Delegate<void (const char*)> d = MakeDelegate(&printer, &Printer::Print);

    // Invoke
    d("hi :)");
}

Lambdas

C++11 lambda expressions can also be used:

void main()
{
    Delegate<uint32_t(uint32_t, uint32_t)> d = [](uint32_t x, uint32_t y) { return x + y; };
    assert(d(123, 456) == 579);
}

MultiDelegates

A delegate can be bound to several callbacks using the overloaded operators += and -=:

struct Printer
{
    void Print(const char* string) const
    {
        printf("Printer: %s", string);
    }
};

struct Screen
{
    void Print(const char* string) const
    {
        printf("Screen: %s", string);
    }
};

void main()
{
    // Create the instances
    Printer printer;
    Screen screen;

    // Create and Bind the delegate
    Delegate<void (const char*)> delegate;

    delegate += MakeDelegate(&printer, &Printer::Print);
    delegate += MakeDelegate(&screen, &Screen::Print);

    // Invoke. This line will call all the callbacks
    delegate("hi :)");
}

When using MultiDelegates the returning value from the delegate invocation is the one obtained from the last invocation.

NOTE

In contrast with C#, delegates do not increment the reference counter of the target instance. This is done to avoid creating circular references that appears when a delegate points back to the object that contains it. This implies that before destroying an instance that is the target of a delegate it must be removed from the delegate.

Reflection

Reflection is the ability of a program to inspect in run-time the structure and state of the data with the possibility of modifying it. Languages as Java or C# incorporate such functionality by default. However, in C++ language it is not possible to obtain this kind of information directly.

Classes

Noesis provides a few macros to easily incorporate reflection information to classes and structs. This is normally used to expose reflection information from client code to Noesis, for example when using Data Binding connecting Views and Models.

There are two kind of macros, a declaration macro (NS_DECLARE_REFLECTION) that you normally use in headers:

struct Quest: public BaseComponent
{
    bool completed;
    NsString title;
    NsString description;
    Ptr<ImageSource> image;

    NS_DECLARE_REFLECTION(Quest, BaseComponent)
};

class ViewModel final: public NotifyPropertyChangedBase
{
public:
    void SetSelectedQuest(Quest* value);
    Quest* GetSelectedQuest() const;

private:
    Ptr<ObservableCollection<Quest>> _quests;
    Ptr<Quest> _selectedQuest;

    NS_DECLARE_REFLECTION(ViewModel, NotifyPropertyChangedBase)
};

And the implementation macro (NS_IMPLEMENT_REFLECTION), to be used in .cpp files:

NS_IMPLEMENT_REFLECTION(Quest)
{
    NsProp("Title", &Quest::title);
    NsProp("Image", &Quest::image);
    NsProp("Description", &Quest::description);
    NsProp("Completed", &Quest::completed);
}

NS_IMPLEMENT_REFLECTION(ViewModel)
{
    NsProp("Quests", &ViewModel::_quests);
    NsProp("SelectedQuest", &ViewModel::GetSelectedQuest, &ViewModel::SetSelectedQuest);
}

Instead of using two macros, you can use only one, but it is not recommended because it will add extra bloating to your headers slightly increasing build time. If possible avoid it although sometimes is mandatory, for example with templates.

template<class T> struct Vector2
{
    T x;
    T y;

    NS_IMPLEMENT_INLINE_REFLECTION(Vector2, NoParent)
    {
        NsProp("x", &Vector2::x);
        NsProp("y", &Vector2::y);
    }
}

Note how NsProp can be used to directly expose member variables, getters and setters or just getters (for read-only properties). For example:

class Game final: public NotifyPropertyChangedBase
{
public:
    void SetSelectedTeam(int selectedTeam)
    {
        if (_selectedTeam != selectedTeam)
        {
            _selectedTeam = selectedTeam;
            OnPropertyChanged("SelectedTeam");
        }
    }

    int GetSelectedTeam() const
    {
        return _selectedTeam;
    }

    Collection* GetVisibleTeams() const
    {
        return _visibleTeams;
    }

private:
    int _selectedTeam;
    Ptr<Collection> _visibleTeams;

    NS_IMPLEMENT_INLINE_REFLECTION(Game, NotifyPropertyChangedBase)
    {
        NsProp("SelectedTeam", &Game::GetSelectedTeam, &Game::SetSelectedTeam);
        NsProp("VisibleTeams", &Game::GetVisibleTeams);
    }
};

Enumerations

Enumerations require a different set of macros. NS_DECLARE_REFLECTION_ENUM, for the header, must be used in the global namespace.

namespace Scoreboard
{

enum class Team
{
    Alliance,
    Horde,
};

enum class Class
{
    Fighter,
    Rogue,
    Hunter,
    Mage,
    Cleric,
};

}

NS_DECLARE_REFLECTION_ENUM(Scoreboard::Team)
NS_DECLARE_REFLECTION_ENUM(Scoreboard::Class)

And NS_IMPLEMENT_REFLECTION_ENUM for the implementation file.

NS_IMPLEMENT_REFLECTION_ENUM(Scoreboard::Team)
{
    NsMeta<TypeId>("Team");

    NsVal("Alliance", Team::Alliance);
    NsVal("Horde", Team::Horde);
}

NS_IMPLEMENT_REFLECTION_ENUM(Scoreboard::Class)
{
    NsMeta<TypeId>("Class");

    NsVal("Fighter", Scoreboard::Class::Fighter);
    NsVal("Rogue", Scoreboard::Class::Rogue);
    NsVal("Hunter", Scoreboard::Class::Hunter);
    NsVal("Mage", Scoreboard::Class::Mage);
    NsVal("Cleric", Scoreboard::Class::Cleric);
}

NOTE

Similar to classes, you can use just a single macro, NS_IMPLEMENT_INLINE_REFLECTION_ENUM

TypeId

In case you need to instantiate a class from a XAML, for example a converter or user control, it must be registered in the component factory. That is achieved by adding a TypeId metadata to the reflection.

NS_IMPLEMENT_REFLECTION(Scoreboard::ThousandConverter)
{
    NsMeta<TypeId>("Scoreboard.ThousandConverter");
}

You also need to register the class in the component factory before it can be used in XAML. Our application framework expose a virtual function for that purpose, RegisterComponents. Find more information about in the Extending NoesisGUI tutorial.

class AppLauncher final: public ApplicationLauncher
{
private:
    void RegisterComponents() const override
    {
        NsRegisterComponent<Scoreboard::MainWindow>();
        NsRegisterComponent<Scoreboard::App>();
        NsRegisterComponent<Scoreboard::ThousandConverter>();
        NsRegisterComponent<EnumConverter<Scoreboard::Team>>();
        NsRegisterComponent<EnumConverter<Scoreboard::Class>>();
    }
};

NOTE

Enums are not directly registered. EnumConverter must be used for that purpose.

Once a class is registered in the factory it can be used in XAML. For example:

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Scoreboard"
    x:Class="Scoreboard.MainWindow"
    FontFamily="Fonts/#Cheboygan">

  <UserControl.Resources>
    <ResourceDictionary>
        <local:ThousandConverter x:Key="ThousandConverter"/>
    </ResourceDictionary>
  </UserControl.Resources>
</UserControl>

Interfaces

In the rare case you need to implement interfaces, NsImpl helper must be used in the corresponding reflection section.

NOTE

NS_IMPLEMENT_INTERFACE_FIXUP must also be used to automatically implement a few internal functions needed by Noesis::Interface

class NotifyPropertyChangedBase: public BaseComponent, public INotifyPropertyChanged
{
public:
    /// From INotifyPropertyChanged
    //@{
    PropertyChangedEventHandler& PropertyChanged() override final;
    //@}

    NS_IMPLEMENT_INTERFACE_FIXUP

protected:
    void OnPropertyChanged(const char* name);

private:
    PropertyChangedEventHandler _propertyChanged;

    NS_DECLARE_REFLECTION(NotifyPropertyChangedBase, BaseComponent)
};
NS_IMPLEMENT_REFLECTION(NotifyPropertyChangedBase)
{
    NsImpl<INotifyPropertyChanged>();
}

RTTI

Having reflection macros in a class also enables safe casts at run-time. This is very similar to the standard dynamic_cast but using NsDynamicCast instead. For example:

Freezable* IsFreezableValue(BaseComponent* value)
{
    Freezable* freezable = NsDynamicCast<Freezable*>(value);
    if (freezable != 0 && !freezable->IsFrozen())
    {
        return freezable;
    }
    else
    {
        return nullptr;
    }
}

Note that Ptr<> can only be casted to Ptr<> (GetPtr() must be used if you want to avoid this)

Ptr<BaseComponent> value = GetLocalValue();
Ptr<BaseBindingExpression> expr = NsDynamicCast<Ptr<BaseBindingExpression>>(value);
BaseBindingExpression* expr_ = NsDynamicCast<BaseBindingExpression*>(value.GetPtr());

NOTE

NsStaticCast performs a NsDynamicCast in debug configurations and just a static_cast in release

© 2017 Noesis Technologies