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 box 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:

#pragma warning(disable: 4275)
#pragma warning(disable: 4251)


#include <Noesis.h>
#include <NsGui/UserControl.h>
#include <NsGui/UIElementData.h>
#include <NsGui/FrameworkPropertyMetaData.h>
#include <NsCore/ReflectionImplement.h>
#include <NsCore/TypeId.h>
#include <NsCore/Package.h>


using namespace Noesis;
using namespace Noesis::Core;
using namespace Noesis::Gui;


////////////////////////////////////////////////////////////////////////////////////////////////////
class NumericUpDown: public UserControl
{
    NS_IMPLEMENT_INLINE_REFLECTION(NumericUpDown, UserControl)
    {
        NsMeta<TypeId>("NumericUpDown");

        NsString source = "Gui/Tutorials/UserControl/NumericUpDown.xaml";

        Ptr<UIElementData> data = NsMeta<UIElementData>(TypeOf<SelfClass>());
        data->OverrideMetadata<NsString>(UserControl::SourceProperty, "Source",
            FrameworkPropertyMetadata::Create(source, FrameworkOptions_None));
    }
};

////////////////////////////////////////////////////////////////////////////////////////////////////
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 property source that is declated in the reflection of the class. This property is assigned the XAML file that will be loaded when this user control is initialized.

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:

#pragma warning(disable: 4275)
#pragma warning(disable: 4251)


#include <Noesis.h>
#include <NsGui/UserControl.h>
#include <NsGui/UIElementData.h>
#include <NsGui/FrameworkPropertyMetaData.h>
#include <NsCore/ReflectionImplement.h>
#include <NsCore/TypeId.h>
#include <NsCore/Package.h>


using namespace Noesis;
using namespace Noesis::Core;
using namespace Noesis::Gui;


////////////////////////////////////////////////////////////////////////////////////////////////////
class NumericUpDown: public UserControl
{
 public:
     /// 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 Gui::DependencyProperty* ValueProperty;

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

        NsProp("Value", &NumericUpDown::GetValue, &NumericUpDown::SetValue);

        NsString source = "Gui/Tutorials/UserControl/NumericUpDown.xaml";

        Ptr<UIElementData> data = NsMeta<UIElementData>(TypeOf<SelfClass>());
        data->RegisterProperty<NsInt32>(ValueProperty, "Value",
            FrameworkPropertyMetadata::Create(NsInt32(0), FrameworkOptions_None));
        data->OverrideMetadata<NsString>(UserControl::SourceProperty, "Source",
            FrameworkPropertyMetadata::Create(source, 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>
#pragma warning(disable: 4275)
#pragma warning(disable: 4251)


#include <Noesis.h>
#include <NsGui/UserControl.h>
#include <NsGui/UIElementData.h>
#include <NsGui/FrameworkPropertyMetaData.h>
#include <NsGui/RoutedEvent.h>
#include <NsCore/ReflectionImplement.h>
#include <NsCore/TypeId.h>
#include <NsCore/Package.h>


using namespace Noesis;
using namespace Noesis::Core;
using namespace Noesis::Gui;


////////////////////////////////////////////////////////////////////////////////////////////////////
class NumericUpDown: public UserControl
{
public:
    /// 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 Gui::DependencyProperty* ValueProperty;

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

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

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

        NsProp("Value", &NumericUpDown::GetValue, &NumericUpDown::SetValue);

        NsFunc("UpButton_Click", &NumericUpDown::UpButton_Click);
        NsFunc("DownButton_Click", &NumericUpDown::DownButton_Click);

        NsString source = "Gui/Tutorials/UserControl/NumericUpDown.xaml";

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

const DependencyProperty* NumericUpDown::ValueProperty;

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

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:

#pragma warning(disable: 4275)
#pragma warning(disable: 4251)


#include <Noesis.h>
#include <NsGui/UserControl.h>
#include <NsGui/UIElementData.h>
#include <NsGui/FrameworkPropertyMetaData.h>
#include <NsGui/RoutedEvent.h>
#include <NsCore/ReflectionImplement.h>
#include <NsCore/TypeId.h>
#include <NsCore/Package.h>


using namespace Noesis;
using namespace Noesis::Core;
using namespace Noesis::Gui;


////////////////////////////////////////////////////////////////////////////////////////////////////
class NumericUpDown: public UserControl
{
public:
    /// 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;
    static const RoutedEvent* ValueChangedEvent;

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

    /// From DependencyObject
    //@{
    NsBool OnPropertyChanged(const Gui::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 UpButton_Click(BaseComponent* sender, const Gui::RoutedEventArgs& e)
    {
        SetValue(GetValue() + 1);
    }

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

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

        NsProp("Value", &NumericUpDown::GetValue, &NumericUpDown::SetValue);

        NsFunc("UpButton_Click", &NumericUpDown::UpButton_Click);
        NsFunc("DownButton_Click", &NumericUpDown::DownButton_Click);

        NsString source = "Gui/Tutorials/UserControl/NumericUpDown.xaml";

        Ptr<UIElementData> data = NsMeta<UIElementData>(TypeOf<SelfClass>());
        data->RegisterProperty<NsInt32>(ValueProperty, "Value",
            FrameworkPropertyMetadata::Create(NsInt32(0), FrameworkOptions_None));
        data->OverrideMetadata<NsString>(UserControl::SourceProperty, "Source",
            FrameworkPropertyMetadata::Create(source, 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 extend 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