NoesisGUI

UserControl Tutorial

zip Tutorial Data

No modern presentation framework would be complete without the ability to create your own reusable controls. If no existing control has a programmatic interface that naturally represents your concept, go ahead and create a user control or custom control.

User controls can be seen as a composition of existing controls. They contain a logical tree defining its look and tend to have logic that directly interacts with these child elements.

In contrast Custom controls are needed when you want to create a totally new control or extend the functionality of an existing control. A custom control tends to get its look from a visual tree defined in a separate control template and generally has logic that works even if the user changes its visual tree completely.

In this tutorial we will focus on the development of a simple user control that implements the typical numeric spinner. The next tutorial will be dedicated to custom controls.

Interface Creation

Let's create a very simple user control, a NumericUpDown control composed of two buttons to increment and decrement and a text to show the current value.

UserControlTutorialImg1.jpg
<UserControl
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  x:Class="NumericUpDown"
  UseLayoutRounding="True">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <Border Grid.RowSpan="2" Grid.ColumnSpan="2" BorderThickness="1"
          BorderBrush="Gray"/>

        <TextBlock Grid.RowSpan="2" VerticalAlignment="Center" Margin="5,3,4,3"/>

        <RepeatButton Name="UpButton" Grid.Column="1" Grid.Row="0" Padding="4,1"
          Margin="0,2,2,0">
            <Path Data="M1,1L4,4 7,1" Stroke="Black" StrokeThickness="2"
              StrokeStartLineCap="Round" StrokeEndLineCap="Round"
              RenderTransformOrigin="0.5,0.5">
                <Path.RenderTransform>
                    <ScaleTransform ScaleY="-1"/>
                </Path.RenderTransform>
            </Path>
        </RepeatButton>

        <RepeatButton Name="DownButton" Grid.Column="1" Grid.Row="1" Padding="4,1"
          Margin="0,0,2,2">
            <Path Data="M1,1L4,4 7,1" Stroke="Black" StrokeThickness="2"
              StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
        </RepeatButton>

    </Grid>

</UserControl>

This interface will correspond to a C++ class that implements the control's code-behind:

#include <Noesis.h>

using namespace Noesis;

class NumericUpDown: public UserControl
{
public:
    NumericUpDown()
    {
        InitializeComponent();
    }

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

    NS_IMPLEMENT_INLINE_REFLECTION(NumericUpDown, UserControl)
    {
        NsMeta<TypeId>("NumericUpDown");
    }
};

extern "C" NS_DLL_EXPORT void NsRegisterReflection(ComponentFactory* factory, NsBool registerComponents)
{
    NS_REGISTER_COMPONENT(NumericUpDown)
}

We are following the steps in the tutorial that describes how to extend NoesisGUI. The unique new detail here, apart from deriving from UserControl, is the GUI::LoadComponent call that indicates the XAML file that will be loaded when this user control is created.

Properties

Now we define the properties that this control will expose. We talked about the numeric spinner value, so this could be our first property: Value. A dependency property must be declared as a public static member with getter and setter accessors to facilitate the use of the control within the code:

#include <Noesis.h>

using namespace Noesis;

class NumericUpDown: public UserControl
{
public:
    NumericUpDown()
    {
        InitializeComponent();
    }

    /// Gets or sets numeric spinner value
    //@{
    NsInt32 GetValue() const
    {
        return DependencyObject::GetValue<NsInt32>(ValueProperty);
    }

    void SetValue(NsInt32 value)
    {
        DependencyObject::SetValue<NsInt32>(ValueProperty, value);
    }
    //@}

public:
    static const DependencyProperty* ValueProperty;

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

    NS_IMPLEMENT_INLINE_REFLECTION(NumericUpDown, UserControl)
    {
        NsMeta<TypeId>("NumericUpDown");

        Ptr<UIElementData> data = NsMeta<UIElementData>(TypeOf<SelfClass>());
        data->RegisterProperty<NsInt32>(ValueProperty, "Value",
            FrameworkPropertyMetadata::Create(NsInt32(0), FrameworkOptions_None));
    }
};

const DependencyProperty* NumericUpDown::ValueProperty;

extern "C" NS_DLL_EXPORT void NsRegisterReflection(ComponentFactory* factory, NsBool registerComponents)
{
    NS_REGISTER_COMPONENT(NumericUpDown)
}

The interface has a text block to show the spinner value. If we want to automatically update the interface we can bind its Text property to the Value property we just created. Besides that, the interface has two buttons that increment or decrement the value of the spinner, so we can add some code-behind that responds to the Click event of these buttons to update the Value property appropriately:

<UserControl
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  x:Class="NumericUpDown"
  x:Name="NumericUpDownControl"
  UseLayoutRounding="True">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <Border Grid.RowSpan="2" Grid.ColumnSpan="2" BorderThickness="1"
          BorderBrush="Gray"/>

        <TextBlock Grid.RowSpan="2" VerticalAlignment="Center" Margin="5,3,4,3"
          Text="{Binding Value, ElementName=NumericUpDownControl}"/>

        <RepeatButton Name="UpButton" Grid.Column="1" Grid.Row="0" Padding="4,1"
          Margin="0,2,2,0" Click="UpButton_Click">
            <Path Data="M1,1L4,4 7,1" Stroke="Black" StrokeThickness="2"
              StrokeStartLineCap="Round" StrokeEndLineCap="Round"
              RenderTransformOrigin="0.5,0.5">
                <Path.RenderTransform>
                    <ScaleTransform ScaleY="-1"/>
                </Path.RenderTransform>
            </Path>
        </RepeatButton>

        <RepeatButton Name="DownButton" Grid.Column="1" Grid.Row="1" Padding="4,1"
          Margin="0,0,2,2" Click="DownButton_Click">
            <Path Data="M1,1L4,4 7,1" Stroke="Black" StrokeThickness="2"
              StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
        </RepeatButton>

    </Grid>

</UserControl>
#include <Noesis.h>

using namespace Noesis;

#define CONNECT_EVENT_HANDLER(type, event, handler) \
    if (String::Compare(eventName, #event) == 0 && String::Compare(handlerName, #handler) == 0) \
    { \
        ((type*)source)->event() += MakeDelegate(this, &SelfClass::handler); \
        return; \
    }

class NumericUpDown: public UserControl
{
public:
    NumericUpDown()
    {
        InitializeComponent();
    }

    /// Gets or sets numeric spinner value
    //@{
    NsInt32 GetValue() const
    {
        return DependencyObject::GetValue<NsInt32>(ValueProperty);
    }

    void SetValue(NsInt32 value)
    {
        DependencyObject::SetValue<NsInt32>(ValueProperty, value);
    }
    //@}

public:
    static const DependencyProperty* ValueProperty;

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

    void Connect(BaseComponent* source, const NsChar* eventName, const NsChar* handlerName)
    {
        CONNECT_EVENT_HANDLER(Button, Click, UpButton_Click);
        CONNECT_EVENT_HANDLER(Button, Click, DownButton_Click);
    }

    void UpButton_Click(BaseComponent* sender, const RoutedEventArgs& e)
    {
        SetValue(GetValue() + 1);
    }

    void DownButton_Click(BaseComponent* sender, const RoutedEventArgs& e)
    {
        SetValue(GetValue() - 1);
    }

    NS_IMPLEMENT_INLINE_REFLECTION(NumericUpDown, UserControl)
    {
        NsMeta<TypeId>("NumericUpDown");

        Ptr<UIElementData> data = NsMeta<UIElementData>(TypeOf<SelfClass>());
        data->RegisterProperty<NsInt32>(ValueProperty, "Value",
            FrameworkPropertyMetadata::Create(NsInt32(0), FrameworkOptions_None));
    }
};

const DependencyProperty* NumericUpDown::ValueProperty;

extern "C" NS_DLL_EXPORT void NsRegisterReflection(ComponentFactory* factory, NsBool registerComponents)
{
    NS_REGISTER_COMPONENT(NumericUpDown)
}

To connect events with code-behind functions we override Connect virtual function. This function is called for each event referenced in the associated xaml, and provides the event source along with the name of function that is expected to be called. User should register the corresponding event handler here (we used a macro to simplify event subscription code).

Events

Next step is exposing an event to notify users when the value of the numeric spinner changes. We call this event ValueChanged and implement it as a routed event. Routed events must be declared with a public static member and with a virtual function that raises the event so inheritors can override the basic implementation:

#include <Noesis.h>

using namespace Noesis;

#define CONNECT_EVENT_HANDLER(type, event, handler) \
    if (String::Compare(eventName, #event) == 0 && String::Compare(handlerName, #handler) == 0) \
    { \
        ((type*)source)->event() += MakeDelegate(this, &SelfClass::handler); \
        return; \
    }

class NumericUpDown: public UserControl
{
public:
    NumericUpDown()
    {
        InitializeComponent();
    }

    /// Gets or sets numeric spinner value
    //@{
    NsInt32 GetValue() const
    {
        return DependencyObject::GetValue<NsInt32>(ValueProperty);
    }

    void SetValue(NsInt32 value)
    {
        DependencyObject::SetValue<NsInt32>(ValueProperty, value);
    }
    //@}

    /// Occurs when numeric value changes
    RoutedEvent_<RoutedPropertyChangedEventHandler<NsInt32>::Handler> ValueChanged()
    {
        return RoutedEvent_<RoutedPropertyChangedEventHandler<NsInt32>::Handler>(this,
            ValueChangedEvent);
    }

public:
    static const DependencyProperty* ValueProperty;
    static const RoutedEvent* ValueChangedEvent;

protected:
    virtual void OnValueChanged(const RoutedPropertyChangedEventArgs<NsInt32>& args)
    {
        RaiseEvent(args);
    }

    // From DependencyObject
    //@{
    NsBool OnPropertyChanged(const DependencyPropertyChangedEventArgs& args)
    {
        NsBool handled = ParentClass::OnPropertyChanged(args);

        if (!handled)
        {
            if (args.prop == ValueProperty)
            {
                NsInt32 oldValue = *static_cast<const NsInt32*>(args.oldValue);
                NsInt32 newValue = *static_cast<const NsInt32*>(args.newValue);

                RoutedPropertyChangedEventArgs<NsInt32> e(this, ValueChangedEvent,
                    oldValue, newValue);
                OnValueChanged(e);

                return true;
            }
        }

        return handled;
    }
    //@}

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

    void Connect(BaseComponent* source, const NsChar* eventName, const NsChar* handlerName)
    {
        CONNECT_EVENT_HANDLER(Button, Click, UpButton_Click);
        CONNECT_EVENT_HANDLER(Button, Click, DownButton_Click);
    }

    void UpButton_Click(BaseComponent* sender, const RoutedEventArgs& e)
    {
        SetValue(GetValue() + 1);
    }

    void DownButton_Click(BaseComponent* sender, const RoutedEventArgs& e)
    {
        SetValue(GetValue() - 1);
    }

    NS_IMPLEMENT_INLINE_REFLECTION(NumericUpDown, UserControl)
    {
        NsMeta<TypeId>("NumericUpDown");

        Ptr<UIElementData> data = NsMeta<UIElementData>(TypeOf<SelfClass>());
        data->RegisterProperty<NsInt32>(ValueProperty, "Value",
            FrameworkPropertyMetadata::Create(NsInt32(0), FrameworkOptions_None));

        data->RegisterEvent(ValueChangedEvent, "ValueChanged", RoutingStrategy_Bubbling);
    }
};

const DependencyProperty* NumericUpDown::ValueProperty;
const RoutedEvent* NumericUpDown::ValueChangedEvent;

extern "C" NS_DLL_EXPORT void NsRegisterReflection(ComponentFactory* factory, NsBool registerComponents)
{
    NS_REGISTER_COMPONENT(NumericUpDown)
}

Improvements

The source code that accompanies this tutorial incorporates several improvements. We have extended the functionality of the spinner by adding new dependency properties to control the maximum and minimum value, and the step factor whenever the up/down buttons are clicked. It is recommended that you read the code carefully.

Usage

Now we can use our control in any other XAML file. For example as an editor for the values of an RGB color:

UserControlTutorialImg2.jpg
<Grid
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  UseLayoutRounding="True">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <GroupBox Header="BACKGROUND: " HorizontalAlignment="Center" Margin="0,20,0,0" Padding="10">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>

                <Rectangle Stretch="Fill" Stroke="Black" Width="80">
                    <Rectangle.Fill>
                        <SolidColorBrush x:Name="BgColor" Color="White"/>
                    </Rectangle.Fill>
                </Rectangle>

                <Grid Grid.Column="1" Margin="10,0,4,0">
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <TextBlock Text="R:" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Row="0"/>
                    <TextBlock Text="G:" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Row="1"/>
                    <TextBlock Text="B:" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Row="2"/>
                </Grid>

                <Grid Grid.Column="2" Width="60">
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <NumericUpDown Grid.Row="0" Margin="0,0,0,0"/>
                    <NumericUpDown Grid.Row="1" Margin="0,2,0,0"/>
                    <NumericUpDown Grid.Row="2" Margin="0,2,0,0"/>
                </Grid>

            </Grid>
        </GroupBox>
        <GroupBox Header="FOREGROUND: " HorizontalAlignment="Center" Margin="20,20,0,0" Padding="10">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>

                <Rectangle Stretch="Fill" Stroke="Black" Width="80">
                    <Rectangle.Fill>
                        <SolidColorBrush x:Name="FgColor" Color="Black"/>
                    </Rectangle.Fill>
                </Rectangle>

                <Grid Grid.Column="1" Margin="10,0,4,0">
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <TextBlock Text="R:" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Row="0"/>
                    <TextBlock Text="G:" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Row="1"/>
                    <TextBlock Text="B:" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Row="2"/>
                </Grid>

                <Grid Grid.Column="2" Width="60">
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <NumericUpDown Grid.Row="0" Margin="0,0,0,0"/>
                    <NumericUpDown Grid.Row="1" Margin="0,2,0,0"/>
                    <NumericUpDown Grid.Row="2" Margin="0,2,0,0"/>
                </Grid>

            </Grid>
        </GroupBox>
        <TextBlock Text="Sample Text" HorizontalAlignment="Center" Margin="20,30,0,0" FontSize="24" Padding="10,5"
             VerticalAlignment="Center" Background="{Binding ElementName=BgColor}"
             Foreground="{Binding ElementName=FgColor}"/>
    </StackPanel>
</Grid>
© 2017 Noesis Technologies