Tutorial: Packages. Part 3. Properties and handlers


In this final tutorial in the series we will show how to remove default properties and events from lists, how to add new ones to them, how to create simple and multi-state properties for your widgets, how to create completely custom properties and their handlers. You will learn more about events and properties and their usage in the Form Builder.



Introduction

In previous tutorials we created Registration widget and showed how to make it appear in the Form Builder. In this tutorial we are going to show how to adjust widget property and event lists. We start from changing event list, then we change property list and after it we will show how to create completely custom property for a widget as well as completely custom property handler for it.

Events

As you know from previous tutorial events for a widget are stored in event list. Event list is provided by package description of a widget. For our Registration widget we created such a list in form of CRegistrationEventList class which is derived from CControlEventList class and contains all events the CControl class has. CControlEventList class is derived from CComponentEventList class and thus it has all events that CComponent has. If you look at the widget events in the Form Builder, you will see all those events on the Properties & Events section on the Events tab.

The events in the list are managed in the event list class constructor (if you recall we left it empty in the previous tutorial). Some of default events have no sense or never used in our widget. So lets first delete events we don't need. Deleting events from a list is pretty simple. All you need is to call list's Delete() method with event name as argument. So, which events do we need to delete from Registration widget event list? To answer this question we need to know which events are in the list. To see which events were added to a list we simply open CComponentEventList and CControlEventList classes and look in theirs constructors. In the component event list almost all events have sense and are called at different events except for one. That event is OnHotkey. The hotkey event can be called only if widget registers hotkeys when it is being added on a form. We don't register any hotkeys in our widget, so that event will never be called and we should delete it from our widget event list.

By looking into CControlEventList class constructor we can see that events there are grouped (by comments). We need parent control events as our control can be placed onto another control and thus can get those events. Our control cannot have child controls so we need to remove all child control related events. In the state change events we need to remove only some of them. Our widget can be shown and hidden, so we need to leave OnShow and OnHide events. We made our widget always enabled, so we need to remove OnEnable and OnDisable events. Our widget has fixed size, so OnResize should be removed as well. We need to leave OnTransform event as the widget has ability to be placed in different positions on a form but we need to remove OnTransformControls event as our control can't have child controls and thus there is no sense in child control transformation event. Style and font of the widget can be changed, so OnStyleChange and OnFontChange events remain in the list. The widget cannot be modal. We specified this directly in widgets constructor when called parent class constructor. So OnSetModal and OnKillModal events need to be removed. Our widget can get keyboard focus, so OnSetFocus and OnKillFocus events remain. The widget doesn't capture keyboard so the OnSetCaptureKeyboard and OnKillCaptureKeyboard events should be removed but it does capture mouse when user press left mouse button, so OnSetCaptureMouse and OnKillCaptureMouse events remain in the list. The control doesn't use active state, so its OnActivate and OnDeactivate events are useless and should be removed as well.

Registration widget can be drawn on a form so all paint events remain in the list. As it may get keyboard focus it can receive keyboard events, so all of them remain as well. For any visible control mouse events exist, so we leave them in the list. OnDropFiles is one of the events generated by mouse input, so we leave it in the event list. We also leave OnPasteString as our widget support clipboard operations.

We know now all the events we need to delete and how to do that. As you might recall we added our own event called OnChange to the Registration widget. Lets see how we can add it to the widget's event list.

Adding event to event list is pretty simple. Event list has method called Add() for that. We should give this method an event description which should be derived from IEvent interface. There is helper class called CEvent which implements that interface. So, we need to create instance of the CEvent class with needed event information and add that instance using event list method Add(). Information about event is passed to CEvent class during its creation via constructor arguments. The constructor has five arguments: pointer to event list instance which contains the event, pointer to entity instance (widget, form, etc) which event it is, name of the event, event signature and event arguments. So, we can add our event description to the list by following line of code in the event list constructor.

Add(new CEvent(this, control, L"OnChange", L"IControl *sender", L"sender"));

Lets get a close look on the last three arguments. To understand why they are needed you need to know how Form Builder uses them. As you already know when you save your form in the Form Builder it generates form prototype class. If you look at that class, you may find there event usage (if you add events) as shown below. There 1 is the event name and it is the member of the widget to assign a callback function (which you can see after =). The 2 is the event signature which is the signature of the event (we declared it in the widget class as void(*OnChange)(IControl *sender);). And finally 3 is the argument to pass to the method (they are like the signature but without type declarations).

Event usage in form prototype class

If you look at the other events you will see they work they same way regardless of event argument count. For example, OnDropFiles event is declared as void(*OnDropFiles)(IControl *sender, const MessageDropFiles &m, bool &processed, bool &accepted); in the IControl interface and below you can see how Form Builder generate code for it in a form prototype class.

OnDropFiles event usage in form prototype class

To correctly generate form prototype class Form Builder also need to know all the forward declarations that might be needed as well as all header files it must add as include in the form prototype file. To do that we need to specify them by using CEvent class methods AddForwardDeclaration() and AddHeaderFile(). The Add() method of the event list returns instance of the event class and those two methods of event class also return instance of the event so we can use all three of them in one line, like this: Add(...)->AddForwardDeclaration(...)->AddHeaderFile(...);. Lets see what forward declaration we need for our event. Event uses IControl interface pointer as argument, so it should be added as forward declaration to the event. Our widget event has no more arguments so that is all forward declaration we need. Header files are needed to be added for all event arguments which are declared not as pointers, so include of the header file where argument type is declared is mandatory. Our event has no such argument, so we don't need to add any header files to our event.

Getting this all together change the CRegistrationEventList class constructor to the following.

CRegistrationEventList::CRegistrationEventList(IPackage *package, IControl *control, IPackageEntity *entity) :
    CControlEventList(package, control, entity)
{
    // Other input events 
    Delete(L"OnHotkey");

    // Events from child controls 
    Delete(L"OnChildStyleChange");
    Delete(L"OnChildFontChange");
    Delete(L"OnChildTransform");
    Delete(L"OnChildResize");
    Delete(L"OnChildShow");
    Delete(L"OnChildHide");
    Delete(L"OnChildAttach");
    Delete(L"OnChildDetach");
    Delete(L"OnChildEnable");
    Delete(L"OnChildDisable");

    // State change events 
    Delete(L"OnEnable");
    Delete(L"OnDisable");
    Delete(L"OnResize");
    Delete(L"OnTransformControls");
    Delete(L"OnSetModal");
    Delete(L"OnKillModal");
    Delete(L"OnSetFocus");
    Delete(L"OnKillFocus");
    Delete(L"OnSetCaptureMouse");
    Delete(L"OnKillCaptureMouse");
    Delete(L"OnActivate");
    Delete(L"OnDeactivate");

    // Add widget event 
    Add(new CEvent(this, control, L"OnChange", L"IControl *sender", L"sender"))
        ->AddForwardDeclaration(L"class IControl;", L"Nitisa/Interfaces/IControl.h", L"");
}

AddForwardDeclaration() method has three arguments: forward declaration as it should be in source code, header file where the class/interface/structure is really declared, and namespace where the forward declared object is located. Namespace is without nitisa global one.

Assume more complicated example. Lets say we declared our widget OnClick event in this way: void(*OnChange)(CRegistration *sender, const RegistrationData &data);. First argument is the widget class pointer and the second one is the widget data. How should we add event in the list in this way? Everything declared directly in global nitisa namespace is available from everywhere by design but in this case CRegistration class is declared in the nitisa::coolwidgets namespace, so coolwidgets namespace should be taken into account when adding the event to the list. The same is true for the RegistrationData and additionally this structure is declared inside IRegistration interface. So, we could change adding the event in the following way.

Add(new CEvent(this, control, L"OnChange", L"coolwidgets::CRegistration *sender, const coolwidgets::IRegistration::RegistrationData &data", L"sender, data"))
	->AddForwardDeclaration(L"class CRegistration;", L"CoolWidgets/Controls/Registration/Registration.h", L"coolwidgets")
	->AddHeaderFile(L"CoolWidgets/Controls/IRegistration.h");

As you can see, we added sender and data argument types declaration relative to the nitisa namespace in the signature. We added forward declaration for the widget class where we specified how widget class may be forward declared, where its actual declaration is and in which namespace (relative to the global one) it is. Also in this case we need to add header file as the data is not a pointer and thus the file where its declaration is should be included in the form prototype header file.

That is almost all about events except for one small thing. The Form Builder has one useful feature called Overwrite namespaces. To this feature work we shouldn't specify namespace in the signature. We need rather use {namespace} placeholder. So, taking this into account, the final code of adding is following.

Add(new CEvent(this, control, L"OnChange", L"{namespace}CRegistration *sender, const {namespace}IRegistration::RegistrationData &data", L"sender, data"))
	->AddForwardDeclaration(L"class CRegistration;", L"CoolWidgets/Controls/Registration/Registration.h", L"coolwidgets")
	->AddHeaderFile(L"CoolWidgets/Controls/IRegistration.h");

Here we only replaced coolwidgets:: with {namespace} in the signature. All other remains the same. You might wonder how Form Builder knows what it should place instead of {namespace}. The answer is simple: it takes proper namespace from package widget class. As you might recall we put it as the second argument in the CPackageControl constructor during calling it in the CPackageRegistration class constructor.

Properties

We work in the similar way with a property list. First we decide which properties we need to remove from a list and then we add properties we need to add. Removing is also done with Delete() method. So, what properties should we remove? CComponentPropertyList adds only one property Name and it should remain in the list. We need to leave most of them in the list except for some. As you might recall we made our widget always enabled and having fixed size, so we need to remove all properties related to size changes: Constraints, Size and Align as well as the property for changing enabled status: Enabled. And one more property to remove is the property controlling transformation of child controls: TransformControls. We can remove it as our widget cannot have child controls.

Adding properties is also performed with Add() method but it's a little bit more complicated as properties themselves are more complicated than events. First of all properties can have different kind of values. It can be just a string, integer or float number, list of items, color, matrix, widget pointer and so on. Secondly, property can be simple or can be multi-state one. If property is multi-state one it stores different values for different states. For example, we defined InputBorderColor property of our widget as property depending on state, or multi-state property, so it can define colors separately for text input borders for normal and focused states. There are a lot of properties for standard objects of the framework defined in the Nitisa code. Almost all classes called CProperty* are properties. All of them have both simple and multi-stated implementations. The last ones are the classes called CProperty*State (with the State at the end). And finally there can be custom properties which should be implemented from scratch. Some packages provide additional properties. For example, you can find some additional properties in the Standard package classes reference page.

So, adding properties is done with the method Add() but we should put in this method instance of class implementing particular property. That is the difference from adding events where we use one class called CEvent for all events.

Lets start by adding a simple property BackgroundColor in the property list. The property type is Color as you may see from getBackgroundColor() and setBackgroundColor() methods of the widget class. If you look at the classes reference page, you will find there two classes CPropertyColor and CPropertyColorState. The first one if for simple property and the second one is for multi-state property (as indicated by State at the end of the class name). So we need the first one. We add it to the property list in its constructor by the following code.

Add(new CPropertyColor(
    this,
    control,
    L"BackgroundColor",
    false,
    nullptr,
    [](IClass *parent) { return cast<CRegistration*>(parent)->getBackgroundColor(); },
    [](IClass *parent, const Color value) { return cast<CRegistration*>(parent)->setBackgroundColor(value); }));

As you can see it's similar to the adding of event in event list but we use particular property implementation class and property implementation classes have much more arguments. Most argument have the same meaning for all property classes, so lets see what they mean. The first argument is the property list to which property is being added. Second argument is the particular component/control/form/etc which property is being managed by the property class. The third argument is the property name. The fourth argument is a boolean value indicating whether the property is read only or not (can or cannot be changed). The last three arguments are functions. First function has type of FSkip which is defined in the CProperty base class. It should return boolean value indicating whether property should be exported in a form prototype class or not. If it returns true, the property won't be exported. It can be useful for conditional exporting of a property. You can use nullptr instead of a function. This means a property will be exported unconditionally. The second and third functions are getter and setter functions which are used to get and set property value. Their type is defined in particular property class and may be slightly different. All these functions have parent argument, which is the object which property is managed. You can use it to convert to a particular widget class, like we did, to access its methods. Getter and setter functions are mandatory.

Our second property is InputBorderColor. It is a multi-state property of type Color so we use CPropertyColorState property class for it. The code to add a property looks like following in our case.

Add(new CPropertyColorState(
    this,
    control,
    L"InputBorderColor",
    false,
    StateItems(),
    ExportPrefixType::NamespaceParent,
    L"",
    nullptr,
    [](IClass *parent, const String &state) { return cast<CRegistration*>(parent)->getInputBorderColor(StringToState(state)); },
    [](IClass *parent, const String &state, const Color value) { return cast<CRegistration*>(parent)->setInputBorderColor(StringToState(state), value); }));

It looks similar to the simple property but its constructor has more arguments. From the beginning arguments are: property list to which the property belongs, entity which property is being handled, property name, whether the property is read only, list of states, state export prefix type, state export prefix, skip function, getter function, setter function.

First difference is that the constructor require list of all possible states. That list is just a string array in form of StringArray (which is just an alias to std::vector<String>). In our example we are going to use StateItems() method to return that list (we will show this method later). If we change multi-state property in the Form Builder and save a form it should be exported in a form prototype class as shown on the image below.

Exported multi-state property

As you can see the name of the state should be specified with path relative to the global namespace nitisa. Only in this case it will work correctly. The state export prefix type and the state export prefix are being used to generate such names. The first parameter specifies what should be added to the state name. In our case it is namespace and widget class name (parent). Full list of possible variants can be found in the ExportPrefixType reference page. The second parameter allows to add additional prefix to a state name. For example, if state is called Disabled and we specify the prefix as st, we will get stDisabled in the form prototype class.

Getter and setter functions receive state in a string representation while in the widget class they are being used as enumeration. That is why we put a call to StringToState() method in those function. We use it to convert string to enumeration item and we will show the method implementation shortly.

Again, skip function is optional while getter and setter are mandatory.

The StateItems() and StringToState() method implementations are trivial and looks like following.

StringArray CRegistrationPropertyList::StateItems()
{
    return StringArray{
        L"State::Normal",
        L"State::Active"
    };
}

CRegistration::State CRegistrationPropertyList::StringToState(const String &state)
{
    if (state == L"State::Active")
        return CRegistration::State::Active;
    return CRegistration::State::Normal;
}

Now lets put this all together. Change the RegistrationPropertyList.h file to the following.

#pragma once

#include "Nitisa/Core/Strings.h"
#include "Nitisa/Package/PropertyLists/ControlPropertyList.h"
#include "../../../Controls/Registration/Registration.h"

namespace nitisa
{
	class IControl;
	class IPackage;
	class IPackageEntity;

	namespace coolwidgets
	{
		class CRegistrationPropertyList :public CControlPropertyList
		{
		private:
			static StringArray StateItems();
			static CRegistration::State StringToState(const String &state);
		public:
			CRegistrationPropertyList(IPackage *package, IControl *control, IPackageEntity *entity);
		};
	}
}

Here we just added two includes: the header file where StringArray and String types are defined and the header file with the Registration widget class definition. We need them for the next two methods we added to the private section. The first method is StateItems() and the second one is StringToState(). You already know what are they for. They are declared as a static ones so that they could be used in a lambda functions in the property constructor.

Change RegistrationPropertyList.cpp file to the following.

#include "stdafx.h"

namespace nitisa
{
	namespace coolwidgets
	{
		CRegistrationPropertyList::CRegistrationPropertyList(IPackage *package, IControl *control, IPackageEntity *entity) :
			CControlPropertyList(package, control, entity)
		{
			// Delete unused properties 
			Delete(L"TransformControls");
			Delete(L"Constraints");
			Delete(L"Size");
			Delete(L"Align");
			Delete(L"Enabled");

			// Add simple property called "BackgroundColor" 
			Add(new CPropertyColor(
				this,
				control,
				L"BackgroundColor",
				false,
				nullptr,
				[](IClass *parent) { return cast<CRegistration*>(parent)->getBackgroundColor(); },
				[](IClass *parent, const Color value) { return cast<CRegistration*>(parent)->setBackgroundColor(value); }));

			// Add multi-state property called "InputBorderColor" 
			Add(new CPropertyColorState(
				this,
				control,
				L"InputBorderColor",
				false,
				StateItems(),
				ExportPrefixType::NamespaceParent,
				L"",
				nullptr,
				[](IClass *parent, const String &state) { return cast<CRegistration*>(parent)->getInputBorderColor(StringToState(state)); },
				[](IClass *parent, const String &state, const Color value) { return cast<CRegistration*>(parent)->setInputBorderColor(StringToState(state), value); }));
		}

		StringArray CRegistrationPropertyList::StateItems()
		{
			return StringArray{
				L"State::Normal",
				L"State::Active"
			};
		}

		CRegistration::State CRegistrationPropertyList::StringToState(const String &state)
		{
			if (state == L"State::Active")
				return CRegistration::State::Active;
			return CRegistration::State::Normal;
		}
	}
}

We already explained what all that means so we won't repeat ourselves. If you build and run Form Builder, add Registration widget from CoolWidgets on a form and look at its events and properties, you will see the result of our work. There will be no unused events and properties anymore, on the Events tab you will see a new event OnChange appears and on the Properties tab there will be our just added two properties. Everything will be managed by the Form Builder just fine.

Widget with just added properties

Custom Properties and Handlers

The last thing remains to be done is to add a custom property. This is the most complicated part of the tutorial. Our widget has property called RegistrationData which is defined by getter method getRegistrationData() and setter method setRegistrationData(). Its type is RegistrationData which is just a structure we defined in the IRegistration widget interface in the IRegistration.h header file. You won't find any property class which can handle such a structure. No wonder, we invented it and framework knows nothing about it, so it has no property class for it. The one way to make this structure manageable in the Form Builder is to add properties for each member of the structure. We could add RegistrationData.FirstName property of type String and described by CPropertyString property class for a FirstName member of the structure, then we could add RegistrationData.LastName property of the same type and property class, and so on. There would be five properties for the structure in the Form Builder. What if the structure would have been much more complex, like having 20-30 members? What if a widget has several such a properties? It would be not very good to have tens of properties in the Form Builder.

We are going to create a custom property and a custom property handler so we could have only one property in the Form Builder instead of five of them.

Each property class should implement IProperty interface. If property is a multi-state property it should implement IPropertyState interface (which is derived from previous one). As for the most objects we create (widgets, forms, properties, etc) we define interfaces and classes. Our RegistrationData is a simple property but if you create packages for distributing to another developers, its a good idea to have both simple and multi-state properties in case another developer would want to use it in multi-state property of his widgets. So we will create both and thus we will have two interfaces IPropertyRegistrationData and IPropertyRegistrationDataState and two classes CPropertyRegistrationData and CPropertyRegistrationDataState implementing our properties. The naming is pretty simple. Its [I|C]Property[Name] for simple properties and [I|C]Property[Name]State for multiple state properties. Here [I|C] is either I (for interfaces) or C (for classes) and [Name] is a property type name.

Create new filter with name Interfaces in the Package filter of the CoolWidgets project. Add there IPropertyRegistrationData.h header file and save it in the Package\Interfaces directory of the project. Add following simple project property interface declaration in it.

#pragma once

#include "Nitisa/Package/Interfaces/IProperty.h"
#include "../../Controls/IRegistration.h"

namespace nitisa
{
	namespace coolwidgets
	{
		class IPropertyRegistrationData :public virtual IProperty
		{
		public:
			virtual IRegistration::RegistrationData getValue() = 0;

			virtual bool setValue(const IRegistration::RegistrationData value) = 0;
		};
	}
}

It's pretty simple. We just declare interface IPropertyRegistrationData derived from IProperty interface with two methods: for getting and for setting value in form of RegistrationData structure declared inside IRegistration widget interface.

Create new header file called IPropertyRegistrationDataState.h under the same project filter and save it in the same directory. Put following multi-state property interface declaration in it.

#pragma once

#include "Nitisa/Core/Strings.h"
#include "Nitisa/Package/Interfaces/IPropertyState.h"
#include "../../Controls/IRegistration.h"

namespace nitisa
{
	namespace coolwidgets
	{
		class IPropertyRegistrationDataState :public virtual IPropertyState
		{
		public:
			virtual IRegistration::RegistrationData getValue(const String &state) = 0;

			virtual bool setValue(const String &state, const IRegistration::RegistrationData value) = 0;
		};
	}
}

This one is also simple. We just declare interface IPropertyRegistrationDataState derived from IPropertyState interface with two methods: for getting and for setting value in form of RegistrationData structure declared inside IRegistration widget interface. Getter and setter methods also have state parameter which is a string representation of state declared as CRegistration::State enumeration.

Add include of this two files to CoolWidgets.h header file of the project.

Create new filter Properties inside the Package filter of the project. Create new header file called PropertyRegistrationData.h there and save it in the Package\Properties directory of the project. Put following declaration of simple property class into it.

#pragma once

#include "Nitisa/Core/Strings.h"
#include "Nitisa/Core/Variant.h"
#include "Nitisa/Package/Core/Property.h"
#include "../../Controls/IRegistration.h"
#include "../Interfaces/IPropertyRegistrationData.h"
#include <iostream>

namespace nitisa
{
	class IClass;
	class IProperty;
	class IPropertyList;

	namespace coolwidgets
	{
		class CPropertyRegistrationData :public virtual IPropertyRegistrationData, public CProperty
		{
		public:
			using FGetter = IRegistration::RegistrationData(*)(IClass *parent);
			using FSetter = bool(*)(IClass *parent, IRegistration::RegistrationData value);
		private:
			IRegistration::RegistrationData m_tOld;
			IRegistration::RegistrationData m_tValue;
			FGetter m_fGetter;
			FSetter m_fSetter;
			bool m_bChanged;
		public:
			// IProperty getters 
			String getPreview() override;
			String getPreview(const String &state) override;
			bool isChanged() override;

			// IProperty setters 
			IProperty *setChanged(const bool value) override;

			// IProperty methods 
			bool Copy(IProperty *dest) override;
			void Save(Variant &dest) override;
			void Load(const Variant &src) override;
			void Export(std::wofstream &f, const String &shift, const String &control) override;

			// IPropertyRegistrationData getters 
			IRegistration::RegistrationData getValue() override;

			// IPropertyRegistrationData setters 
			bool setValue(const IRegistration::RegistrationData value) override;

			CPropertyRegistrationData(IPropertyList *list, IClass *parent, const String &name, const bool read_only, FSkip skip, FGetter getter, FSetter setter);
		};
	}
}

In the beginning we, as always, include heeded header files and add needed forward declarations. The property class is, as expected, implements property interface, so we derive it from the interface. We also derive property class from helper class CProperty which has some default implementation of IProperty features.

In the beginning of the class we declared getter and setter functions. They are very simple for most of the property classes. The both have parent argument which is the entity (component, control, form, ...) which property is being managed by the property class. Getter function returns property value in needed form. Setter function has second argument which is a new property value in needed form. That value will be set into parent when setter function is called. Also setter function returns boolean value indicating whether change was made or not (it can return false, for example, when property of the widget already has the value equal to value argument or when value has invalid value).

Private section of the class has members for storing initial value of the widget property called m_tOld, current value of the property called m_tValue, pointer to getter and setter functions m_fGetter and m_fSetter, and flag m_bChanged indicating whether property was changed.

The next public section contains method of parent interfaces we need to implement and constructor. Constructor should be similar to the constructors of the most property classes you find in the framework. Its first argument is property list to which property class belongs. The second argument is an entity (widget, form, etc), which property the class manages. The third one is a property name, The fourth argument indicates whether property is read only or not. And the last three arguments are function pointers: skip function, getter function, setter function. You might recall all this arguments of constructor from adding BackgroundColor property to the property list earlier. Meaning of other methods from this section we will describe soon, when we show their implementation.

Add new header file PropertyRegistrationDataState.h in the same filter and save it in the same directory. Put following multi-state property class declaration into it.

#pragma once

#include "Nitisa/Core/Strings.h"
#include "Nitisa/Core/Variant.h"
#include "Nitisa/Package/Core/PropertyState.h"
#include "../../Controls/IRegistration.h"
#include "../Interfaces/IPropertyRegistrationDataState.h"
#include <iostream>
#include <map>

namespace nitisa
{
	class IClass;
	class IProperty;
	class IPropertyList;

	namespace coolwidgets
	{
		class CPropertyRegistrationDataState :public virtual IPropertyRegistrationDataState, public CPropertyState
		{
		public:
			using FGetter = IRegistration::RegistrationData(*)(IClass *parent, const String &state);
			using FSetter = bool(*)(IClass *parent, const String &state, IRegistration::RegistrationData value);
		private:
			std::map<String, IRegistration::RegistrationData> m_aOld;
			std::map<String, IRegistration::RegistrationData> m_aValue;
			FGetter m_fGetter;
			FSetter m_fSetter;
			bool m_bChanged;
		public:
			// IProperty getters 
			String getPreview() override;
			String getPreview(const String &state) override;
			bool isChanged() override;

			// IProperty setters 
			IProperty *setChanged(const bool value) override;

			// IProperty methods 
			bool Copy(IProperty *dest) override;
			void Save(Variant &dest) override;
			void Load(const Variant &src) override;
			void Export(std::wofstream &f, const String &shift, const String &control) override;

			// IPropertyRegistrationDataState getters 
			IRegistration::RegistrationData getValue(const String &state) override;

			// IPropertyRegistrationDataState setters 
			bool setValue(const String &state, const IRegistration::RegistrationData value) override;

			CPropertyRegistrationDataState(
				IPropertyList *list,
				IClass *parent,
				const String &name,
				const bool read_only,
				const StringArray &states,
				const ExportPrefixType state_prefix_type,
				const String &state_prefix,
				FSkip skip,
				FGetter getter,
				FSetter setter);
		};
	}
}

It's the same as for previous one except for this time we use state version of interfaces and classes. For the m_aOld and m_aValue we use map as we need to store values for each state.

Also add include of this two header files to CoolWidgets.h.

Before we continue with implementing property classes, we need to create some helper functions. Add new filter Core inside Package filter of the CoolWidgets project. Add new header file called Utils.h there and save it in the Package\Core directory of the project. Add include of this file to CoolWidgets.h.

So, what possible helper function we might need? In order to check whether property is changed or not we need comparison operator for the property data type (structure IRegistration::RegistrationData). To display property value we need ability to make its string representation, so we need a function to convert property value to a string. Form Builder saves form projects in JSON format. To do that it first collect all data in form of Variant class. You may see we have Save() and Load() methods for that in property classes. Thus we need functions to convert property value to and from Variant. Also, as you know, Form Builder generates form prototype which is a header file with form prototype class inside. Each property class have Export() method which is responsible for writing property change source code in that file. To facilitate that source code output we need helper function to convert IRegistration::RegistrationData to source code string. And finally we will define PropertyHandlerRegistrationData constant meaning of which we will explain later.

So, put following code into Utils.h header file.

#pragma once

#include "Nitisa/Core/Strings.h"
#include "Nitisa/Core/Variant.h"
#include "../../Controls/IRegistration.h"

namespace nitisa
{
	namespace coolwidgets
	{
		extern const String PropertyHandlerRegistrationData;

		bool operator!=(const IRegistration::RegistrationData &a, const IRegistration::RegistrationData &b);
		String ToString(const IRegistration::RegistrationData &value);
		String AsSourceCode(const IRegistration::RegistrationData &value);
		Variant ToVariant(const IRegistration::RegistrationData &value);
		void FromVariant(const Variant &src, IRegistration::RegistrationData &value);
	}
}

Create new source code file called Utils.cpp inside the Package\Utils filter in the Package\Utils directory of the project and put following implementation of the functions above into this file.

#include "stdafx.h"

namespace nitisa
{
	namespace coolwidgets
	{
		const String PropertyHandlerRegistrationData{ L"RegistrationData" };

		bool operator!=(const IRegistration::RegistrationData &a, const IRegistration::RegistrationData &b)
		{
			return a.FirstName != b.FirstName || a.FirstName != b.LastName || a.Year != b.Year || a.Month != b.Month || a.Day != b.Day;
		}

		String ToString(const IRegistration::RegistrationData &value)
		{
			return value.FirstName + L" " + value.LastName + L", " + nitisa::ToString(value.Month) + L"/" + nitisa::ToString(value.Day) + L"/" + nitisa::ToString(value.Year);
		}

		String AsSourceCode(const IRegistration::RegistrationData &value)
		{
			return
				L"coolwidgets::IRegistration::RegistrationData{ L\"" +
				value.FirstName + L"\", L\"" +
				value.LastName + L"\", " +
				nitisa::ToString(value.Year) + L", " +
				nitisa::ToString(value.Month) + L"," +
				nitisa::ToString(value.Day) +
				L"}";
		}

		Variant ToVariant(const IRegistration::RegistrationData &value)
		{
			Variant result;
			result[L"FirstName"] = value.FirstName;
			result[L"LastName"] = value.LastName;
			result[L"Year"] = value.Year;
			result[L"Month"] = value.Month;
			result[L"Day"] = value.Day;
			return result;
		}

		void FromVariant(const Variant &src, IRegistration::RegistrationData &value)
		{
			if (src.isSet(L"FirstName"))
				value.FirstName = (String)src.get(L"FirstName");
			if (src.isSet(L"LastName"))
				value.LastName = (String)src.get(L"LastName");
			if (src.isSet(L"Year"))
				value.Year = (int)src.get(L"Year");
			if (src.isSet(L"Month"))
				value.Month = (int)src.get(L"Month");
			if (src.isSet(L"Day"))
				value.Day = (int)src.get(L"Day");
		}
	}
}

The comparison operator bool operator!=() is pretty simple. It returns true if either members of the comparing arguments aren't equal. If all members of the arguments are equal it returns false. We will need only this comparison operator, so we didn't add == one.

ToString() function converts RegistrationData to a string representation which is later displayed in the Property Editor of the Form Builder. It just puts together first and last name and adds date of birth after comma in m/d/Y format.

To change widget RegistrationData property in source code, you need to write something like this: m_pRegistration1->setRegistrationData(coolwidgets::IRegistration::Registration{ L"John", L"Doe", 1990, 10, 15 });. The argument of the setRegistrationData() method is what the AsSourceCode() function return. It just takes values from value argument and convert them into the representation like we have shown just now.

The ToVariant() function converts RegistrationData to Variant. It just adds corresponding members with proper values to it. The FromVariant() function does the opposite. It takes values from Variant if they exist there and put them into RegistrationData structure.

Lets continue with implementing of property classes. Add new source code file called PropertyRegistrationData.cpp inside the Package\Properties filter and save it in the Package\Properties directory of the project. Put following code into it.

#include "stdafx.h"

namespace nitisa
{
	namespace coolwidgets
	{
	#pragma region Constructor & destructor
		CPropertyRegistrationData::CPropertyRegistrationData(IPropertyList *list, IClass *parent, const String &name, const bool read_only, FSkip skip, FGetter getter, FSetter setter) :
			CProperty(list, parent, name, PropertyHandlerRegistrationData, skip, read_only),
			m_tOld{ getter(parent) },
			m_tValue{ getter(parent) },
			m_fGetter{ getter },
			m_fSetter{ setter },
			m_bChanged{ false }
		{
			AddHeaderFile(L"CoolWidgets/Controls/IRegistration.h");
		}
	#pragma endregion

	#pragma region IProperty getters
		String CPropertyRegistrationData::getPreview()
		{
			return ToString(getValue());
		}

		String CPropertyRegistrationData::getPreview(const String &state)
		{
			return L"";
		}

		bool CPropertyRegistrationData::isChanged()
		{
			return m_bChanged || getValue() != m_tOld;
		}
	#pragma endregion

	#pragma region IProperty setters
		IProperty *CPropertyRegistrationData::setChanged(const bool value)
		{
			m_bChanged = value;
			return this;
		}
	#pragma endregion

	#pragma region IProperty methods
		bool CPropertyRegistrationData::Copy(IProperty *dest)
		{
			IPropertyRegistrationData *d{ cast<IPropertyRegistrationData*>(dest) };
			if (d)
			{
				d->setValue(getValue());
				return true;
			}
			return false;
		}

		void CPropertyRegistrationData::Save(Variant &dest)
		{
			dest.Reset();
			dest = ToVariant(getValue());
		}

		void CPropertyRegistrationData::Load(const Variant &src)
		{
			IRegistration::RegistrationData v;
			FromVariant(src, v);
			setValue(v);
		}

		void CPropertyRegistrationData::Export(std::wofstream &f, const String &shift, const String &control)
		{
			if (control.empty())
				f << shift << L"set" << m_sName << L"(" << AsSourceCode(getValue()) << L");" << std::endl;
			else
				f << shift << control << L"->set" << m_sName << L"(" << AsSourceCode(getValue()) << L");" << std::endl;
		}
	#pragma endregion

	#pragma region IPropertyRegistrationData getters
		IRegistration::RegistrationData CPropertyRegistrationData::getValue()
		{
			if (!isReadOnly() || !Application->Editor)
				return m_fGetter(m_pParent);
			return m_tValue;
		}
	#pragma endregion

	#pragma region IPropertyRegistrationData setters
		bool CPropertyRegistrationData::setValue(const IRegistration::RegistrationData value)
		{
			if (!isReadOnly() || !Application->Editor)
				return m_fSetter(m_pParent, value);
			if (value != m_tValue)
			{
				m_tValue = value;
				return true;
			}
			return false;
		}
	#pragma endregion
	}
}

The constructor call parent class constructor as usually. The parent class CProperty constructor arguments are already all familiar to you except one. That argument is a property handler name. We use constant PropertyHandlerRegistrationData for it and we will talk later about it. We put current property value of the widget into m_tOld and m_tValue using getter function. We store getter and setter function pointers in m_fGetter and m_fSetter members for future use. Also we set m_bChanged to false which means the property was not changed yet. Property class constructor is the place where we need to add all needed forward declarations and header files to be included in form prototype header file during its generation. The rules are simple. Setter method of the Registration widget has argument of type IRegistration::RegistrationData. That argument is not a pointer, so header file with its declaration should be included. That it what we did in the constructor body. If it was a pointer, we would use forward declaration instead.

getPreview() methods are used to display property preview value in Property Editor of Form Builder. They return string representation of the property value. The method without argument is being used for simple property and the method with state argument is being used for multi-state property. We implement simple property in this class, so the second method just returns empty string as it will never be used. The preview is not necessary to be an exact string representation of the property value.

Method isChanged() return true if property was changed and false otherwise. In this method we return true if flag m_bChanged is set to true or current value doesn't equal to initial one (m_tOld). Method setChanged() is used to change m_bChanged flag. It allows to mark property as changed and thus be exported in the form prototype file even if its value is the same as it was in the widget when property was created.

The Copy() method is needed to copy value from one property to another. As its argument is IProperty pointer and our class works with IPropertyRegistrationData property we need to convert method argument to it using cast() function and check whether conversion was successful. If it was, just get value of this property and put it using setValue() to the property we got as method argument.

Save() and Load() methods are used to store property value when Form Builder saves form file and restore property value data when it loads form file (*.nfr) from disk. In the first method we use Variant's reset() method to ensure there is no any trash data in it before saving our property data into it using ToVariant() function we defined earlier. In the Load() method we do the opposite. We load value using FromVariant() method and put it as the property value.

The Export() method is being used to generate property setting value in the form prototype class. The first argument is a stream to which we should output source code. The second argument is a shift from the line start (spaces) and is being used for nice code formatting in a form prototype file. And the last argument is a control name to which the property belongs. If it's empty, the property belongs to a form. Depending on it we produce line of code to set either a control or a form property.

The getValue() method is being used to get value of the property. If property is not read only or application is not a Form Builder type application, we use getter function to get current value of the property. Otherwise we return stored in the m_tValue member value.

In the setValue() method, which is being used to change property value, we either use stored setter function pointer to change value (if property is not a read only one or application is not a Form Builder like one) or store new value in the m_tValue member.

That's all what is usually needed from a simple property class. Lets now implement a multi-state property class. Add new source code file called PropertyRegistrationDataState.cpp to Package\Properties filter and save it in the Package\Properties directory of the project. Put following implementation code into it.

#include "stdafx.h"

namespace nitisa
{
	namespace coolwidgets
	{
	#pragma region Constructor & destructor
		CPropertyRegistrationDataState::CPropertyRegistrationDataState(
			IPropertyList *list,
			IClass *parent,
			const String &name,
			const bool read_only,
			const StringArray &states,
			const ExportPrefixType state_prefix_type,
			const String &state_prefix,
			FSkip skip,
			FGetter getter,
			FSetter setter) : CPropertyState(list, parent, name, PropertyHandlerRegistrationData, skip, read_only, states, state_prefix_type, state_prefix),
			m_fGetter{ getter },
			m_fSetter{ setter },
			m_bChanged{ false }
		{
			AddHeaderFile(L"CoolWidgets/Controls/IRegistration.h");
			for (int i = 0; i < getStateCount(); i++)
			{
				m_aOld[getState(i)] = m_fGetter(m_pParent, getState(i));
				m_aValue[getState(i)] = m_fGetter(m_pParent, getState(i));
			}
		}
	#pragma endregion

	#pragma region IProperty getters
		String CPropertyRegistrationDataState::getPreview()
		{
			return L"";
		}

		String CPropertyRegistrationDataState::getPreview(const String &state)
		{
			return ToString(getValue(state));
		}

		bool CPropertyRegistrationDataState::isChanged()
		{
			if (m_bChanged)
				return true;
			for (int i = 0; i < getStateCount(); i++)
				if (getValue(getState(i)) != m_aOld[getState(i)])
					return true;
			return false;
		}
	#pragma endregion

	#pragma region IProperty setters
		IProperty *CPropertyRegistrationDataState::setChanged(const bool value)
		{
			m_bChanged = value;
			return this;
		}
	#pragma endregion

	#pragma region IProperty methods
		bool CPropertyRegistrationDataState::Copy(IProperty *dest)
		{
			IPropertyRegistrationDataState *d{ cast<IPropertyRegistrationDataState*>(dest) };
			if (d && d->getStateCount() == getStateCount())
			{
				for (int i = 0; i < getStateCount(); i++)
					d->setValue(getState(i), getValue(getState(i)));
				return true;
			}
			return false;
		}

		void CPropertyRegistrationDataState::Save(Variant &dest)
		{
			dest.Reset();
			for (int i = 0; i < getStateCount(); i++)
				dest[getState(i).c_str()] = ToVariant(getValue(getState(i)));
		}

		void CPropertyRegistrationDataState::Load(const Variant &src)
		{
			for (int i = 0; i < getStateCount(); i++)
			{
				IRegistration::RegistrationData v;
				FromVariant(src.get(getState(i).c_str()), v);
				setValue(getState(i), v);
			}
		}

		void CPropertyRegistrationDataState::Export(std::wofstream &f, const String &shift, const String &control)
		{
			String state_prefix{ Prefix(getStatePrefixType(), getStatePrefix()) };
			for (int i = 0; i < getStateCount(); i++)
				if (control.empty())
					f << shift << L"set" << m_sName << L"(" << state_prefix << getState(i) << L", " << AsSourceCode(getValue(getState(i))) << L");" << std::endl;
				else
					f << shift << control << L"->set" << m_sName << L"(" << state_prefix << getState(i) << L", " << AsSourceCode(getValue(getState(i))) << L");" << std::endl;
		}
	#pragma endregion

	#pragma region IPropertyRegistrationDataState getters
		IRegistration::RegistrationData CPropertyRegistrationDataState::getValue(const String &state)
		{
			if (!isReadOnly() || !Application->Editor)
				return m_fGetter(m_pParent, state);
			return m_aValue[state];
		}
	#pragma endregion

	#pragma region IPropertyRegistrationDataState setters
		bool CPropertyRegistrationDataState::setValue(const String &state, const IRegistration::RegistrationData value)
		{
			if (!isReadOnly() || !Application->Editor)
				return m_fSetter(m_pParent, state, value);
			if (value != m_aValue[state])
			{
				m_aValue[state] = value;
				return true;
			}
			return false;
		}
	#pragma endregion
	}
}

The meaning of all the methods here is the same as we wrote earlier. The difference here is that we deal with a property having several states here rather than one as in previous property class. So, the constructor has three more arguments and store all values of all states in the m_aOld and m_aValue members. We also check changes for all states in the isChanged() method, copy, save, load and export values for all states in the Copy(), Save(), Load() and Export() method respectfully. And this is all.

We have property classes describing a new custom property with type RegistrationData. To add it to the property list of our widget open RegistrationPropertyList.cpp file and put following code at the end of the constructor body.

Add(new CPropertyRegistrationData(
    this,
    control,
    L"RegistrationData",
    false,
    nullptr,
    [](IClass *parent) { return cast<CRegistration*>(parent)->getRegistrationData(); },
    [](IClass *parent, const IRegistration::RegistrationData value) { return cast<CRegistration*>(parent)->setRegistrationData(value); }));

If you build everything and run Form Builder, you may find there our new custom property as shown below. The first and last names are initially empty and thus only a comma and initial date of birth is displayed in the preview in the Property Editor.

Custom property in Form Builder

If you click on the property in attempt to edit it, you will see No handler found! message. No wonder we cannot edit property yet. If you look carefully on everything we've done so far, you will see we have nothing for actual property editing yet. For the actual property editing a Property Handler is responsible. Property Handler is built-in control which Property Editor of the Form Builder actually builds in itself when you attempt to edit property and it's an actual object responsible for the property value changing. It also means each property or property pair (simple one plus multi-state one) requires property handler.

Form Builder selects needed property handler by its name. That is what for we defined PropertyHandlerRegistrationData constant and used it as an argument in property classes constructors. This constant contains name of property handler which Form Builder will try to use for actual editing of the property of our type. We have no property handler with the name stored in the constant and that is why you see error message in Property Editor. So, lets make one.

As you know there are properties which are edited directly inside the Property Editor of the Form Builder and there are properties which are edited in separate form or popup window. Properties having simple value, like string or number, can easily be edited directly. But if property is a complex value, like border color, which is a set of four color values, it's hard to edit it in one text input. For that we would need to use some predefined format, parser for it and, for different property types we would need different formats. It would be nightmare to remember them all for a user. Much more user friendly is to make a separate visual editor for it. Which is, in mose cases, just a separate form where user can edit property value.

Property Handler should implement IPropertyHandler interface. There is a helper class CPropertyHandler which implements some of its features. Additionally Standard package has three more specific implementations of three property handler types: CPropertyHandlerTextInput implements inline property handler (for simple value properties like string, integer and float numbers), CPropertyHandlerDropDown implements inline drop down property editor where each property can have only several predefined values (for enumeration type properties, boolean properties, and so on), and CPropertyHandlerModal implements property handler in which property is being edited in separate form. All these three implementations are not final, they just implement common features for different kinds of property editing and of course need some more development to get final version of needed property handler.

Our custom property of type IRegistration::RegistrationData is not simple one and thus we won't use CPropertyHandlerTextInput class as a base one. Our property also cannot use CPropertyHandlerDropDown class as it cannot be represented as a list of predefined values. The best choice to start our property handler is to start it from CPropertyHandlerModal class.

As our property will be edited in separate form we, as you might already guessed, need a form for it. Lets create it. Create a filter called Forms in the root of the CoolWidgets project. Create a filter called RegistrationData inside Forms one. In that filter create a form shown on the picture below using Form Builder (either standalone or the one from the Nitisa extension for Visual Studio).

RegistrationData form

You will need Label, Edit and Button widgets from Standard tab of Components and Controls section of the Form Builder and MonthCalendar widget from Additional tab. Change following.

  1. Set form's Name property to FormRegistrationDataProto.
  2. Set form's Caption property to Registration Data.
  3. Set form's HasMaximizeBox property to false.
  4. Set form's HasMinimizeBox property to false.
  5. Set form's HasSizeBox property to false.
  6. Set form's State property to Hidden.
  7. Set form's WindowPosition property to MainFormCenter.
  8. Use EditFirstName and EditLastName as the names of inputs for first and last name. Also clear their Text properties.
  9. Use MonthCalendar as the name of calendar widget.
  10. Use ButtonOk and ButtonCancel as the names of the buttons.
  11. Generate OnClick events for both buttons.
  12. Open Form Builder configuration, change Namespace in Export section to coolwidgets and apply changes to form only (Apply to form only button).

Save the form in the FormRegistrationDataProto.nfr file in the Forms\RegistrationData directory of the project. As you know form prototype header file with name IFormRegistrationDataProto.h will be generated in that folder as well. Add it to the project (under the Forms\RegistrationData filter) and open it. If you have IntelliSense enabled you will see many errors in it (if your IntelliSense is disabled you will get the same errors later during compilation if do no changes). If you take close look on that errors, you will see they are all because of Standard package. IntelliSense can not find anything related to that package. That is because when we configured CoolWidgets project we didn't add an include path to the packages directory in the framework. We didn't do that because we didn't need it then. Now we do, so lets add it. Open CoolWidgets project configuration, find Include Directories property in the Configuration Properties -> VC++ Directories and add $(ProjectDir)../../../Packages path to it, so the custom part of that property is $(ProjectDir);$(ProjectDir)../../..;$(ProjectDir)../../../Packages;. You will see the errors disappear from the prototype file.

We have created the form prototype. Lets now create a final form. Create new header file called IFormRegistrationData.h under the Forms filter, save it in the Forms directory of the project and put following form interface declaration code into it.

#pragma once

#include "../Controls/IRegistration.h"

namespace nitisa
{
	namespace coolwidgets
	{
		class IFormRegistrationData
		{
		public:
			virtual IRegistration::RegistrationData getValue() = 0;

			virtual bool setValue(const IRegistration::RegistrationData &value) = 0;

			virtual void ActivateFirstEnabledInput() = 0;
		};
	}
}

We just declared a form interface following common rules as we also did when we created the Registration widget. Here we declared getter and setter method for our custom data type and additional method called ActivateFirstEnabledInput(). It's more user friendly to activate first field to enter a data in the form and this method will do that. Also add include of this file into CoolWidgets.h.

Create new header file called FormRegistrationData.h inside the Forms\RegistrationData filter, save it in the Forms\RegistrationData directory of the project, add its include to the CoolWidgets.h header file and put following form class declaration code into it.

#pragma once

#include "Nitisa/Core/FormService.h"
#include "Nitisa/Core/Messages.h"
#include "Nitisa/Core/Strings.h"
#include "../../Controls/IRegistration.h"
#include "../IFormRegistrationData.h"
#include "IFormRegistrationDataProto.h"

namespace nitisa
{
	class IControl;
	class IRenderer;
	class IWindow;

	namespace coolwidgets
	{
		class CFormRegistrationData :public virtual IFormRegistrationData, public IFormRegistrationDataProto
		{
		private:
			class CFormRegistrationDataService :public CFormService
			{
			private:
				CFormRegistrationData *m_pForm;
			public:
				void NotifyOnTranslateChange() override;

				CFormRegistrationDataService(CFormRegistrationData *form);
			};

			void LoadTranslatableTexts();
		protected:
			void ButtonOk_OnClick(IControl *sender) override;
			void ButtonCancel_OnClick(IControl *sender) override;
		public:
			static const String ClassName;

			IRegistration::RegistrationData getValue() override;

			bool setValue(const IRegistration::RegistrationData &value) override;

			void ActivateFirstEnabledInput() override;

			CFormRegistrationData(IWindow *window, IRenderer *renderer);
		};
	}
}

The form class, as expected, derives from form interface and form prototype. In the private section we declared form service with only one notification. Although there will be no internationalization in this tutorial, we add it to show how it works. Also there we declared helper method to load translatable texts. Protected section contains event methods as usually. In the public section we declared static constant where we will store form class name and override of form interface methods to implement them. Constructor has two arguments: window and renderer.

Create new source code file under the Forms\RegistrationData filter, call it FormRegistrationData.cpp, save in the Forms\RegistrationData directory of the project and put following form class implementation code into it.

#include "stdafx.h"

namespace nitisa
{
	namespace coolwidgets
	{
		const String CFormRegistrationData::ClassName{ L"FormRegistrationData" };

	#pragma region Service
		CFormRegistrationData::CFormRegistrationDataService::CFormRegistrationDataService(CFormRegistrationData *form) :
			CFormService(form),
			m_pForm{ form }
		{

		}

		void CFormRegistrationData::CFormRegistrationDataService::NotifyOnTranslateChange()
		{
			m_pForm->LoadTranslatableTexts();
		}
	#pragma endregion

	#pragma region Constructor & destructor
		CFormRegistrationData::CFormRegistrationData(IWindow *window, IRenderer *renderer) :
			IFormRegistrationDataProto(window, renderer, ClassName)
		{
			setService(new CFormRegistrationDataService(this), true);
			LoadTranslatableTexts();
		}

		void CFormRegistrationData::LoadTranslatableTexts()
		{
			CLockRepaint lock{ this };
			setCaption(Application->Translate->t(ClassName, L"Registration Data"));
			m_pLabel1->setCaption(Application->Translate->t(ClassName, L"First name:"));
			m_pLabel2->setCaption(Application->Translate->t(ClassName, L"Last name:"));
			m_pLabel3->setCaption(Application->Translate->t(ClassName, L"Date of birth:"));
			m_pButtonOk->setCaption(Application->Translate->t(ClassName, L"Ok"));
			m_pButtonCancel->setCaption(Application->Translate->t(ClassName, L"Cancel"));
		}
	#pragma endregion

	#pragma region Events
		void CFormRegistrationData::ButtonOk_OnClick(IControl *sender)
		{
			setModalResult(ModalResult::Ok);
		}

		void CFormRegistrationData::ButtonCancel_OnClick(IControl *sender)
		{
			setModalResult(ModalResult::Cancel);
		}
	#pragma endregion

	#pragma region Interface getters
		IRegistration::RegistrationData CFormRegistrationData::getValue()
		{
			return IRegistration::RegistrationData{
				m_pEditFirstName->getText(),
				m_pEditLastName->getText(),
				m_pMonthCalendar->getYear(),
				m_pMonthCalendar->getMonth(),
				m_pMonthCalendar->getDay()
			};
		}
	#pragma endregion

	#pragma region Interface setters
		bool CFormRegistrationData::setValue(const IRegistration::RegistrationData &value)
		{
			return
				m_pEditFirstName->setText(value.FirstName) ||
				m_pEditLastName->setText(value.LastName) ||
				m_pMonthCalendar->setYear(value.Year) ||
				m_pMonthCalendar->setMonth(value.Month) ||
				m_pMonthCalendar->setDay(value.Day);
		}
	#pragma endregion

	#pragma region Interface methods
		void CFormRegistrationData::ActivateFirstEnabledInput()
		{
			m_pEditFirstName->setFocus();
		}
	#pragma endregion
	}
}

The implementation is pretty simple. Service constructor just calls parent constructor and store form instance pointer for future use. In the NotifyOnTranslateChange() notification, called when application language is changed, we just use forms helper method LoadTranslatableTexts() to load new, translated, texts for the texts on the form.

In the form constructor we call form's prototype class constructor as usually, change form's service to ours and load texts using helper method LoadTranslatableTexts() which just takes texts from translation matrix for all texts you can find on our form and update them. Later you may add some texts into different languages to translation matrix and use our TestApp project to check how it works (you will need to add something to change TestApp application language; for example, it can be drop down with languages you added to translation matrix).

Our form will be shown as a modal form. So when user clicks on Ok button, the ButtonOk_OnClick() event method is called and we close the form with ModalResult::Ok result. When user clicks on Cancel button, we close the form with ModalResult::Cancel result.

Methods getValue() and setValue() get and set value directly from widgets on the form and ActivateFirstEnabledInput() method just sets keyboard input focus to the first name input.

The next step is to add form description to the package. This is pretty simple to the way we added description of the widget. Create new filter called Forms in the Package filter of the project. Inside that filter create a new one called RegistrationData. Create new header file in it with name FormRegistrationDataPropertyList.h and save it in the Package\Forms\RegistrationData directory of the project. Add include of this header file to the CoolWidgets.h as usual. Put following form property list class declaration into it.

#pragma once

#include "Nitisa/Package/PropertyLists/FormPropertyList.h"

namespace nitisa
{
	class IForm;
	class IPackage;
	class IPackageEntity;

	namespace coolwidgets
	{
		class CFormRegistrationDataPropertyList :public CFormPropertyList
		{
		public:
			CFormRegistrationDataPropertyList(IPackage *package, IForm *form, IPackageEntity *entity);
		};
	}
}

This is very similar to the widget property list we created earlier. In this case a form is used in constructor and the class is derived from default form property list.

Create new source code file called FormRegistrationDataPropertyList.cpp in the same filter and directory. Put following form property list implementation in it.

#include "stdafx.h"

namespace nitisa
{
	namespace coolwidgets
	{
		CFormRegistrationDataPropertyList::CFormRegistrationDataPropertyList(IPackage *package, IForm *form, IPackageEntity *entity) :
			CFormPropertyList(package, form, entity)
		{
			Add(new CPropertyRegistrationData(
				this,
				form,
				L"Value",
				false,
				nullptr,
				[](IClass *parent) { return cast<CFormRegistrationData*>(parent)->getValue(); },
				[](IClass *parent, const IRegistration::RegistrationData value) { return cast<CFormRegistrationData*>(parent)->setValue(value); }));
		}
	}
}

We do not need to delete any default properties. We just need to add the Value property our form has. As you can see we added property of IRegistration::RegistrationData type, which is being handled by the property handler, which uses our form, which has property of that type. This might be a little bit confusing but don't worry it works.

Create new header file called FormRegistrationDataEventList.h in the same place, add its include to the CoolWidgets.h and put following form event class declaration into it.

#pragma once

#include "Nitisa/Package/EventLists/FormEventList.h"

namespace nitisa
{
	class IForm;
	class IPackage;
	class IPackageEntity;

	namespace coolwidgets
	{
		class CFormRegistrationDataEventList :public CFormEventList
		{
		public:
			CFormRegistrationDataEventList(IPackage *package, IForm *form, IPackageEntity *entity);
		};
	}
}

Create new source code file called FormRegistrationDataEventList.cpp in the same place and put following form event list class implementation into it.

#include "stdafx.h"

namespace nitisa
{
	namespace coolwidgets
	{
		CFormRegistrationDataEventList::CFormRegistrationDataEventList(IPackage *package, IForm *form, IPackageEntity *entity) :
			CFormEventList(package, form, entity)
		{

		}
	}
}

Form event list class is basically the same as default form event list. We delete and add nothing to it. We could have also used default form event list class directly but its better to have a separate list implementation as we might add something here in the future.

Create new header file called PackageFormRegistrationData.h in the same place, add its include to the CoolWidgets.h and put following form package description class declaration into it.

#pragma once

#include "Nitisa/Core/Strings.h"
#include "Nitisa/Package/Core/PackageForm.h"

namespace nitisa
{
	class IForm;
	class IEventList;
	class IPackage;
	class IPropertyList;
	class IRenderer;
	class IWindow;

	class Image;

	namespace coolwidgets
	{
		class CPackageFormRegistrationData :public CPackageForm
		{
		private:
			IForm *m_pForm;
		public:
			String getCategory() override;
			String getClassName() override;
			String getTitle() override;
			String getDescription() override;
			String getReferenceUrl() override;
			const Image *getIcon() override;
			IForm *getInstance() override;

			IForm *Create(IWindow *window, IRenderer *renderer) override;
			IPropertyList *CreatePropertyList(IForm *form) override;
			IEventList *CreateEventList(IForm *form) override;

			CPackageFormRegistrationData(IPackage *package);
		};
	}
}

Its methods are similar to the package widget class methods except for the Create() which creates form without property and event lists and has window and renderer arguments.

Create new source code file called PackageFormRegistrationData.cpp in the same place and put following form package description class implementation into it.

#include "stdafx.h"

namespace nitisa
{
	namespace coolwidgets
	{
		CPackageFormRegistrationData::CPackageFormRegistrationData(IPackage *package) :
			CPackageForm(package, L"coolwidgets"),
			m_pForm{ nullptr }
		{
			// Supported platforms 

			// Header files 
			AddHeaderFile(L"CoolWidgets/Forms/RegistrationData/FormRegistrationData.h");
		}

		String CPackageFormRegistrationData::getCategory()
		{
			return L"CoolWidgets";
		}

		String CPackageFormRegistrationData::getClassName()
		{
			return CFormRegistrationData::ClassName;
		}

		String CPackageFormRegistrationData::getTitle()
		{
			return L"FormRegistrationData";
		}

		String CPackageFormRegistrationData::getDescription()
		{
			return L"RegistrationData";
		}

		String CPackageFormRegistrationData::getReferenceUrl()
		{
			return L"";
		}

		const Image *CPackageFormRegistrationData::getIcon()
		{
			return nullptr;
		}

		IForm *CPackageFormRegistrationData::getInstance()
		{
			return m_pForm;
		}

		IForm *CPackageFormRegistrationData::Create(IWindow *window, IRenderer *renderer)
		{
			IForm *result{ new CFormRegistrationData(window, renderer) };
			if (!m_pForm)
				m_pForm = result;
			return result;
		}

		IPropertyList *CPackageFormRegistrationData::CreatePropertyList(IForm *form)
		{
			CFormRegistrationData *f{ cast<CFormRegistrationData*>(form) };
			if (f)
				return new CFormRegistrationDataPropertyList(getPackage(), form, this);
			return nullptr;
		}

		IEventList *CPackageFormRegistrationData::CreateEventList(IForm *form)
		{
			CFormRegistrationData *f{ cast<CFormRegistrationData*>(form) };
			if (f)
				return new CFormRegistrationDataEventList(getPackage(), form, this);
			return nullptr;
		}
	}
}

This, again, is very similar to the widget package class implementation except for the Create() method, as noted above and the new getInstance() method. In Create() method we store pointer to the first instance of a form when we create it. In the getInstance() we just return that pointer. It is needed to avoid creating multiple copies of the form while the only one is enough. Also we don't have an icon for the form and return nullptr in the getIcon() method.

To add form description to package description, adjust CPackageCoolWidgets class in the following way.

  1. In the private section of the class declaration add std::vector<IPackageForm*> m_aForms; line right after m_aControls member declaration. This array will contain package form descriptions (it will have actually only one item).
  2. Add Application->Editor->Register(Create<CPackageFormRegistrationData>(m_aForms)); line into the package constructor. By this line we create form package description class, add it to the m_aForms list and register it in the Form Builder.
  3. Add following two lines to the destructor of the class. This is just a list of forms clearing. The same we have there for controls.
    for (auto pos = m_aForms.begin(); pos != m_aForms.end(); pos++)
        (*pos)->Release();
  4. Change following three methods of the class implementation in the way shown below. The logic here is absolutely the same we have for controls in the corresponding methods.
    int CPackageCoolWidgets::getFormCount()
    {
        return (int)m_aForms.size();
    }
    
    IPackageForm *CPackageCoolWidgets::getForm(const int index)
    {
        if (index >= 0 && index < (int)m_aForms.size())
            return m_aForms[index];
        return nullptr;
    }
    
    IPackageForm *CPackageCoolWidgets::getForm(const String &class_name)
    {
        for (auto pos : m_aForms)
            if (pos->getClassName() == class_name)
                return pos;
        return nullptr;
    }

We finally came to the Property Handler implementation. Create new filter called PropertyHandlers inside Package filter. Create filter called RegistrationData inside this new filter. Create new header file called PropertyHandlerRegistrationData.h inside the just created filter and save it in the Package\PropertyHandlers\RegistrationData directory of the project. Add include of this file into CoolWidgets.h and put following property handler class declaration into the PropertyHandlerRegistrationData.h.

#pragma once

#include "Nitisa/Core/Strings.h"
#include "Standard/Package/Core/PropertyHandlerModal.h"
#include "../../../Controls/IRegistration.h"

namespace nitisa
{
	class IPackage;
	class IProperty;

	namespace coolwidgets
	{
		class IFormRegistrationData;

		class CPropertyHandlerRegistrationData :public standard::CPropertyHandlerModal
		{
		private:
			IProperty *m_pProperty;
			String m_sState;

			IFormRegistrationData *getForm();
			void setFormData(const IRegistration::RegistrationData &value);
		protected:
			void NotifyOnActivateModal() override;
		public:
			String getPropertyState() override;
			IProperty *getProperty() override;
			bool isEditable(IProperty *property) override;

			bool setProperty(IProperty *value) override;
			bool setPropertyState(const String &value) override;

			CPropertyHandlerRegistrationData(IPackage *package);
		};
	}
}

As was written earlier we derive our property handler from CPropertyHandlerModal helper class. This class provides method NotifyOnActivateModal() which is called when user attempts to edit property in the Form Builder. This method is a place where we need to show a form for editing property value. Property handlers usually handle both simple and multi-state properties and thus we defined m_pProperty and m_sState members to store property being edited and its state if the property is multi-state one. In the private section we also defined two helper methods used for getting form instance and setting value to be edited in the form. Meaning of other methods will be explained shortly.

Create new source code file called PropertyHandlerRegistrationData.cpp in the same location and put following property handler class implementation code into it.

#include "stdafx.h"

namespace nitisa
{
	namespace coolwidgets
	{
		CPropertyHandlerRegistrationData::CPropertyHandlerRegistrationData(IPackage *package) :
			CPropertyHandlerModal(package, PropertyHandlerRegistrationData),
			m_pProperty{ nullptr }
		{

		}

		bool CPropertyHandlerRegistrationData::isEditable(IProperty *property)
		{
			return cast<IPropertyRegistrationData*>(property) || cast<IPropertyRegistrationDataState*>(property);
		}

		IProperty *CPropertyHandlerRegistrationData::getProperty()
		{
			return m_pProperty;
		}

		String CPropertyHandlerRegistrationData::getPropertyState()
		{
			return m_sState;
		}

		bool CPropertyHandlerRegistrationData::setProperty(IProperty *value)
		{
			if (!value || isEditable(value))
			{
				m_pProperty = value;
				if (!m_sState.empty())
				{
					IPropertyRegistrationDataState *p{ cast<IPropertyRegistrationDataState*>(m_pProperty) };
					bool found{ false };
					if (p)
					{
						for (int i = 0; i < p->getStateCount(); i++)
							if (p->getState(i) == m_sState)
							{
								found = true;
								break;
							}
					}
					if (!found)
						m_sState.clear();
				}
				if (m_pProperty)
				{
					IPropertyRegistrationData *p{ cast<IPropertyRegistrationData*>(m_pProperty) };
					IPropertyRegistrationDataState *ps{ cast<IPropertyRegistrationDataState*>(m_pProperty) };
					if (ps)
						setFormData(ps->getValue(m_sState));
					else
						setFormData(p->getValue());
				}
				return true;
			}
			return false;
		}

		bool CPropertyHandlerRegistrationData::setPropertyState(const String &value)
		{
			IPropertyRegistrationDataState *p{ cast<IPropertyRegistrationDataState*>(m_pProperty) };
			if (p)
			{
				bool found{ false };
				for (int i = 0; i < p->getStateCount(); i++)
					if (p->getState(i) == value)
					{
						found = true;
						break;
					}
				if (found && value != m_sState)
				{
					m_sState = value;
					setFormData(p->getValue(m_sState));
					return true;
				}
			}
			return false;
		}

		IFormRegistrationData *CPropertyHandlerRegistrationData::getForm()
		{
			if (getPackage() && getControl() && getControl()->getForm() && getControl()->getForm()->getRenderer())
			{
				IPackageForm *pf{ getPackage()->getForm(CFormRegistrationData::ClassName) };
				if (pf)
				{
					IForm *f{ pf->getInstance() };
					if (!f)
						f = pf->Create(getControl()->getForm()->QueryService()->getWindow()->CreateInstance(), getControl()->getForm()->getRenderer()->CreateInstance());
					return cast<IFormRegistrationData*>(f);
				}
			}
			return nullptr;
		}

		void CPropertyHandlerRegistrationData::setFormData(const IRegistration::RegistrationData &value)
		{
			IFormRegistrationData *form{ getForm() };
			if (form)
				form->setValue(value);
		}

		void CPropertyHandlerRegistrationData::NotifyOnActivateModal()
		{
			
			IFormRegistrationData *form{ getForm() };
			if (form)
			{
				IPropertyRegistrationDataState *ps{ cast<IPropertyRegistrationDataState*>(m_pProperty) };
				if (ps)
					setFormData(ps->getValue(m_sState));
				else
					setFormData(cast<IPropertyRegistrationData*>(m_pProperty)->getValue());
				cast<IForm*>(form)->OnSetFocus = [](IForm *sender) {
					sender->OnSetFocus = nullptr;
					cast<IFormRegistrationData*>(sender)->ActivateFirstEnabledInput();
				};
				if (cast<IForm*>(form)->ShowModal() == ModalResult::Ok)
				{
					if (ps)
					{
						ps->setValue(m_sState, form->getValue());
						if (getListener())
							cast<IPropertyHandlerListener*>(getListener())->NotifyOnPropertyChange(this, m_pProperty);
					}
					else
					{
						IPropertyRegistrationData *p{ cast<IPropertyRegistrationData*>(m_pProperty) };
						if (p)
						{
							p->setValue(form->getValue());
							if (getListener())
								cast<IPropertyHandlerListener*>(getListener())->NotifyOnPropertyChange(this, m_pProperty);
						}
					}
				}
			}
		}
	}
}

Constructor of the class is pretty simple. We only call parent class constructor with package instance and property handler name arguments and assign empty value to m_pProperty member which means there is no property to edit assigned to the property handler.

Method isEditable() is used to check whether specified property can be edited by this property handler. The check is pretty simple. If property can be successfully converted into simple or multi-state property of needed interface, then the property can be edited.

getProperty() and getPropertyState() methods are used to get property and its state assigned to the property handler at the moment. The setProperty() and setPropertyState() are used to assign property and its state to the property handler.

In the setProperty() method we first check whether property being assigned is either empty or can be edited (it has our property type). If so, we proceed with assigning value to m_pProperty member. Then, if any state is stored in the m_sState member we validate this member value. It should either store valid state value of the currently assigned multi-state property or be empty. That is what we do in the if block. After validating state we check whether property or empty value was assigned and, if it is property, we update form's value using setFormData() and value from either simple or multi-state property (depending on what the assigned property is). In case of successful property assignment the method returns true. It returns false if property was not assigned.

The setPropertyState() method assigns new property state only if assigned property is a multi-state one and if specified state is correct (it's one of the multi-state property allowed states). We check that by trying to convert assigned property to multi-state one and then by searching the state being assigned in the list of states of multi-state property p. If everything is Okay and the state being assigned is not the same state we already have in the m_sState member, we update the member value, the value of the form using setFormData and return true which means assignment was successful. If something goes wrong or state is the same we already have, we return false.

We use getForm() method to get instance of the form where property value is actually being edited. We created that form earlier in this tutorial. Method doesn't create new form instance each time it's called. It checks whether the form was created and if so returns it rather than creating a new instance. As you know any form requires window and renderer. Our form does too. But there could be many platform window and renderer implementations. Which one should we select? We could have hardcoded some selected ones but the best way is to use the same implementations the Form Builder uses. How can we know which implementations are used by the Form Builder? As you know property handler is a sort of built-in control. When built-in control is assigned to some control we can get that control by built-in control method getControl() (you may recall it from the first tutorial of the series). Once we have control we can find out the form it is located on using its method getForm(). And once we have the form we can get window and renderer it uses. Both window and renderer have method to create instances of the same implementation. The method is called CreateInstance(). So, at the beginning of the getForm() method we check whether a package is assigned to the property handler (we will use package later and thus we need to ensue we have it), check whether property handler is assigned to a control, check that control is on a form, and check that the form has a renderer assigned. We don't need to check whether a form has window as it cannot have renderer without a window. Instead of using CFormRegistrationData form class directly, we use its package description. So, first we get package form description from our package by form class name and store it in the pf variable. If form description is found, we get its instance and store it in the f variable. If form was not created yet, the package form getInstance() method returned nullptr, we need to create the form. We do it by package form description method Create() supplying it with the window and renderer instances we create using CreateInstance() methods of Form Builder form window and renderer. Then we just return form instance pointer in needed form.

The setFormData() method is used to update form with value of our property type. First we try to get form using previous method and then, if form getting was successful, we update form with specified value.

Method NotifyOnActivateModal() looks rather difficult but it's actually not. At the beginning we get form instance and store it in the form variable. If we get form, we continue. We update form with value stored in the assigned property. Depending on the actual property type we either use simple one or multi-state one. After updating form we assign event callback function to OnSetFocus event of the form. This event is called after showing a form when it gets focus. In the callback function we clear OnSetFocus event so that the event function is called only once and after it we activate first text input on the form using ActivateFirstEnabledInput() method of the form which we added and implemented earlier when we were creating the form. Next, we show the form as a modal one using its ShowModal() method and, if form is closed by clicking on Ok button (recall we set its ModalResult to ModalResult::Ok), we update assigned property value with data stored in the form. As you can see we, again, do it depending on what property type we have: simple or multi-state one. Also, if property handler has listener assigned, we call its NotifyOnPropertyChange() method to notify the listener owner about property change. That's all.

In order the Form Builder be able to find our property handler we need to add it to the package in the same way we already added widget and form. To do that do the following.

  1. Add #include "Nitisa/Package/Core/PackagePropertyHandler.h" line to the includes in the PackageCoolWidgets.h file.
  2. Add std::vector<IPackagePropertyHandler*> m_aPropertyHandlers; line to the private section of the CPackageCoolWidgets declaration class right after declaration of the m_aForms member. This is where a list of package property handlers will be stored (it will actually have only one property handler).
  3. Add following template method right after Create() template method in the same private section of the class declaration.
    template<class PropertyHandlerClass> CPackagePropertyHandler<PropertyHandlerClass> *CreatePropertyHandler(const String &name, std::vector<IPackagePropertyHandler*> &list)
    {
        CPackagePropertyHandler<PropertyHandlerClass> *result{ new CPackagePropertyHandler<PropertyHandlerClass>(this, name) };
        list.push_back(result);
        return result;
    }

    This method does the same as a previous one. It creates property handler and adds it to the specified list.
  4. Add Application->Editor->Register(CreatePropertyHandler<CPropertyHandlerRegistrationData>(PropertyHandlerRegistrationData, m_aPropertyHandlers)); line to the end of the constructor. This will add property handler to the list and register it in the Form Builder.
  5. Add following two lines to the destructor of the class. This is just a list of property handlers clearing. The same we have there for controls and forms.
    for (auto pos = m_aPropertyHandlers.begin(); pos != m_aPropertyHandlers.end(); pos++)
        (*pos)->Release();
  6. Change implementation of the following three methods. Again, it's absolutely the same we have for controls and forms. Methods return property handler count the package has and property handler either by index or by name.
    int CPackageCoolWidgets::getPropertyHandlerCount()
    {
        return (int)m_aPropertyHandlers.size();
    }
    
    IPackagePropertyHandler *CPackageCoolWidgets::getPropertyHandler(const int index)
    {
        if (index >= 0 && index < (int)m_aPropertyHandlers.size())
            return m_aPropertyHandlers[index];
        return nullptr;
    }
    
    IPackagePropertyHandler *CPackageCoolWidgets::getPropertyHandler(const String &name)
    {
        for (auto pos : m_aPropertyHandlers)
            if (pos->Name == name)
                return pos;
        return nullptr;
    }

That is all we need to add custom property to a widget so that Form Builder could manage custom property in common way.

Conclusion

If you build and run FormBuilder application you will now be able to edit Registration widget custom property RegistrationData like shown on the screenshot below.

Custom property handler in Form Builder

Summarizing all about packages we can tell following.

  • In order the Form Builder could load and use a package it should be in form of dll with GetPackage() exported function, which creates package description class.
  • Package description class is just a storage of descriptions of all entities the package has. Having such a storage is not enough though. Package description class should register all entities it wants to be available to Form Builder. We usually do it in the package description class constructor.
  • Description of almost each entity in the package consists of three parts: entity description, entity property list and entity event list.
  • Event and property lists are the events and properties you see in the Properties & Events section of the Form Builder.
  • If you need custom property, you should create its property classes (simple property class and multi-state property class; also having only simple or only multi-state one is also allowed) and property handler class, which do the actual editing. In most cases you will need a form to edit custom property visually.

It might be difficult at the beginning to remember everything about needed classes and their interactions. Fortunately everything is pretty much the same for all widgets, forms, properties and property handler so you can just find the most similar one to your object in the framework, copy its implementation, rename classes and data types and do other minor changes and everything will work just fine.

We hope this tutorial series bring much more in understanding how Form Builder and widgets are actually work in the Nitisa and you will start create and distribute your own cool widgets after reading it.

You can find projects of this series with lots of comments in the latest release of the Nitisa framework in Tutorials\Packages folder of the Nitisa solution. To build it you have to select DebugRT configuration and x86 platform. The project Package.CoolWidgets binary file (dll) will be placed in the DebugRT directory of the solution to avoid conflict with your code you created following this tutorial.


If you like Nitisa project consider support it with a small donation or chose one of the paid plan to support further development.