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# painless 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, 'NoesisPCH.h', exposing all NoesisGUI API. This is specially useful when using precompiled headers.
#include <NoesisPCH.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 NoesisPCH.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
Brush* color = new SolidColorBrush(Color::Red);
//...
// Release instance and delete it if reference counter reaches zero
color->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.
References to components can be manually handled using AddReference() and Release(). For example:
Brush* color0 = new SolidColorBrush(Color::Red); // Ref '1' - Created
Brush* color1 = color0;
color0->AddReference(); // Ref '1' -> '2'
//...
color0->Release(); // Ref '2' -> '1'
color1->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<Brush> color0 = *new SolidColorBrush(Color::Red);
Ptr<Brush> color1(color0);
//...
// color0 and color1 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 convenient alternative using MakePtr:
Ptr<Brush> color0 = MakePtr<SolidColorBrush>(Color::Red);
Ptr<Brush> color1(color0);
//...
// color0 and color1 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);
NOTE
For performance purposes, refcounted instances created in the stack are also supported. In this case is very important to make sure that no extra references are pending at destruction time. BaseRefCounted will assert if that scenario is detected.
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 needs 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);
snprintf(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<BaseComponent>* GetVisibleTeams() const
{
return _visibleTeams;
}
private:
int _selectedTeam;
Ptr<Collection<BaseComponent>> _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, "Scoreboard.Team")
{
NsVal("Alliance", Team::Alliance);
NsVal("Horde", Team::Horde);
}
NS_IMPLEMENT_REFLECTION_ENUM(Scoreboard::Class, "Scoreboard.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
Factory
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 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
{
RegisterComponent<Scoreboard::MainWindow>();
RegisterComponent<Scoreboard::App>();
RegisterComponent<Scoreboard::ThousandConverter>();
RegisterComponent<EnumConverter<Scoreboard::Team>>();
RegisterComponent<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 DynamicCast instead. For example:
Freezable* IsFreezableValue(BaseComponent* value)
{
Freezable* freezable = DynamicCast<Freezable*>(value);
if (freezable != 0 && !freezable->IsFrozen())
{
return freezable;
}
else
{
return nullptr;
}
}
DynamicPtrCast is also available in case a dynamic cast from Ptr<> to Ptr<> is needed. Note that this is more efficient than manually getting the pointer using GetPtr() and doing the cast with DynamicCast.
Ptr<BaseComponent> value = GetLocalValue();
Ptr<BaseBindingExpression> expr = DynamicPtrCast<BaseBindingExpression>(value);
BaseBindingExpression* expr_ = DynamicCast<BaseBindingExpression*>(value.GetPtr());