manuel
Topic Author
Posts: 13
Joined: 16 Aug 2022, 10:06

Plain Properties with INotifyPropertyChanged VS Dependency Properties

01 Dec 2022, 17:35

Hey everyone,

currently, I work on styling a Slider with a custom Tickbar. As you may know, the Tickbar isn't directly accessible because of legacy reasons, as far as I understood it. Therefore, I derive a new class from Noesis::TickBar and override its OnRender function. My issue now is that whenever the OnRender function is executed, all member variables of this (my MenuTickBar object) contain only their default values instead of the values I provide them through the ControlTemplate. This issue only appears with my c++ implementation. The c# implementation that uses exactly the same XAML files works as intended.

Details:
It turned out to be a case of "Plain Properties with INotifyPropertyChanged VS Dependency Properties". As you can see in my c# version of my MenuTickBar, the class has a Brush and two double member variables that are reflected with INotifyPropertyChanged implemented. But translating this class into c++ results in the issue I mentioned above. To fix it, I had to transform ALL member variables into DependencyProperties. But I don't understand why this is necessary for c++ when it isn't for c#. Maybe someone can spot out the issue here.

Observations:
My Xaml creates a Slider that loads a Style that sets the ControlTemplate that creates my custom MenuTickBar (code provided below).
By stepping through the code, I can see that two MenuTickBar objects are created in the initialization process of Noesis, before the View in question is actually loaded. One for the vertical and one for the horizontal ControlTemplate. Both objects also get their SetBrush, SetRadiusX, and SetRadiusY functions called once with the values specified in the XAML.
A third MenuTickBar is created when the View is loaded. But this object is only initialized with its default values. This object is also the only one that is used whenever OnRender is executed later.

This is the Xaml code of the Slider:
<Slider x:Name="GameStartsSlider"
     Value="{Binding SelectedIndex, ElementName=GameStartsList, FallbackValue=0}"
     Maximum="{Binding ElementName=EpisodeList, Path=SelectedItem.GameStartList.Count, Converter={StaticResource ConverterSubtract}, ConverterParameter=1, FallbackValue=10}"
     Minimum="0"
     SmallChange="1"
     IsSnapToTickEnabled="True"
     Style="{DynamicResource MenuSliderStyle}"
     Height="50"
     Width="{Binding ElementName=EpisodeList, Path=SelectedItem.GameStartList.Count, Converter={StaticResource ConverterMultiply}, ConverterParameter=25, FallbackValue=400}"
     />
The Slider references the following Style:
    <Style x:Key="MenuSliderStyle" TargetType="{x:Type Slider}">
        <Setter Property="Focusable" Value="False"/>
        <Setter Property="SnapsToDevicePixels" Value="true" />
        <Setter Property="OverridesDefaultStyle" Value="true" />
        <Style.Triggers>
            <Trigger Property="Orientation" Value="Horizontal">
                <Setter Property="Template" Value="{StaticResource MenuSliderHorizontalTemplate}" />
            </Trigger>
            <Trigger Property="Orientation" Value="Vertical">
                <Setter Property="Template" Value="{StaticResource MenuSliderVerticalTemplate}" />
            </Trigger>
        </Style.Triggers>
    </Style>
The Style references the following ControlTemplates, one vertical and one horizontal version:
    <ControlTemplate x:Key="MenuSliderHorizontalTemplate" TargetType="{x:Type Slider}">
        <Grid>
            <controls:MenuTickBar Fill="{StaticResource Custom.SolidColorBrush.BlueLight}" VerticalAlignment="Top" SnapsToDevicePixels="True" Grid.Row="0" Placement="Top" Visibility="Visible" Brush="{StaticResource Custom.SolidColorBrush.BlueLight}" RadiusX="5" RadiusY="5" Orientation="{TemplateBinding Orientation}" Width="{TemplateBinding ActualWidth}" Height="{TemplateBinding ActualHeight}"/>
            <Track Grid.Row="1" x:Name="PART_Track" >
                <Track.DecreaseRepeatButton>
                    <RepeatButton Style="{StaticResource SliderRepeatButton}" Command="Slider.DecreaseLarge" />
                </Track.DecreaseRepeatButton>
                <Track.Thumb>
                    <Thumb Style="{StaticResource SliderThumbHorizontal}" />
                </Track.Thumb>
                <Track.IncreaseRepeatButton>
                    <RepeatButton Style="{StaticResource SliderRepeatButton}" Command="Slider.IncreaseLarge" />
                </Track.IncreaseRepeatButton>
            </Track>
        </Grid>
    </ControlTemplate>

    <ControlTemplate x:Key="MenuSliderVerticalTemplate" TargetType="{x:Type Slider}">
        <Grid>
            <controls:MenuTickBar Fill="{StaticResource Custom.SolidColorBrush.BlueLight}" VerticalAlignment="Top" SnapsToDevicePixels="True" Grid.Row="0" Placement="Top" Visibility="Visible" Brush="{StaticResource Custom.SolidColorBrush.BlueLight}" RadiusX="5" RadiusY="5" Orientation="{TemplateBinding Orientation}" Width="{TemplateBinding ActualWidth}" Height="{TemplateBinding ActualHeight}"/>
            <Track Grid.Row="1" x:Name="PART_Track" >
                <Track.DecreaseRepeatButton>
                    <RepeatButton Style="{StaticResource SliderRepeatButton}" Command="Slider.DecreaseLarge" />
                </Track.DecreaseRepeatButton>
                <Track.Thumb>
                    <Thumb Style="{StaticResource SliderThumbVertical}" />
                </Track.Thumb>
                <Track.IncreaseRepeatButton>
                    <RepeatButton Style="{StaticResource SliderRepeatButton}" Command="Slider.IncreaseLarge" />
                </Track.IncreaseRepeatButton>
            </Track>
        </Grid>
    </ControlTemplate>
Both ControlTemplates create my custom MenuTickBar that looks like the following:
c++ MenuTickBar - Header
namespace UI
{
	class MenuTickBar : public Noesis::TickBar, public Noesis::INotifyPropertyChanged
	{
	public:
		MenuTickBar();
		~MenuTickBar();

		MenuTickBar(const MenuTickBar& other);
		void operator=(const MenuTickBar& other);
		MenuTickBar(const MenuTickBar&& other) = delete;
		void operator=(const MenuTickBar&& other) = delete;

		Noesis::Brush* GetBrush() const { return Brush; }
		void SetBrush(Noesis::Brush* brush);

		float GetRadiusX() const { return RadiusX; }
		void SetRadiusX(const float radiusX);

		float GetRadiusY() const { return RadiusY; }
		void SetRadiusY(const float radiusY);

		static const Noesis::DependencyProperty* OrientationProperty;
		Noesis::Orientation GetOrientation() const;
		void SetOrientation(Noesis::Orientation orientation);

		virtual Noesis::PropertyChangedEventHandler& PropertyChanged() override;

		NS_IMPLEMENT_INTERFACE_FIXUP

		void OnRender(Noesis::DrawingContext* context) override;

	protected:
		void OnPropertyChanged(const char* name);

	private:
		Noesis::Brush* Brush = nullptr;
		Noesis::Pen* Pen = nullptr;
		float RadiusX = 1.0;
		float RadiusY = 1.0;

		Noesis::PropertyChangedEventHandler PropertyChangedEventHandler;

		NS_DECLARE_REFLECTION(UI::MenuTickBar, Noesis::TickBar)
	};
}
c++ MenuTickBar - Implementation:
namespace UI
{
	MenuTickBar::MenuTickBar()
	{
		Pen = new Noesis::Pen();
	}

	MenuTickBar::~MenuTickBar()
	{
		SAFE_DELETE(Pen);
		//SAFE_DELETE(Brush); // TODO: causing crash on shutdown. Has this already been taken care of by NoesisAPI?
	}

	MenuTickBar::MenuTickBar(const MenuTickBar& other)
	{
		Pen = new Noesis::Pen();
		Brush = other.Brush;
		PropertyChangedEventHandler = other.PropertyChangedEventHandler;
		RadiusX = other.RadiusX;
		RadiusY = other.RadiusY;
	}

	void MenuTickBar::operator=(const MenuTickBar& other)
	{
		if (&other != this) {
			SAFE_DELETE(Pen);
			//SAFE_DELETE(Brush); // TODO: causing crash on shutdown. Has this already been taken care of by NoesisAPI?
			Pen = new Noesis::Pen();
			Brush = other.Brush;
			PropertyChangedEventHandler = other.PropertyChangedEventHandler;
			RadiusX = other.RadiusX;
			RadiusY = other.RadiusY;
		}
	}

	void MenuTickBar::SetBrush(Noesis::Brush* brush)
	{
		if (Brush != brush) {
			Brush = brush;
			OnPropertyChanged("Brush");
		}
	}

	void MenuTickBar::SetRadiusX(const float radiusX)
	{
		if (RadiusX != radiusX) {
			RadiusX = radiusX;
			OnPropertyChanged("RadiusX");
		}
	}

	void MenuTickBar::SetRadiusY(const float radiusY)
	{
		if (RadiusY != radiusY) {
			RadiusY = radiusY;
			OnPropertyChanged("RadiusY");
		}
	}

	const Noesis::DependencyProperty* UI::MenuTickBar::OrientationProperty;

	Noesis::Orientation MenuTickBar::GetOrientation() const
	{
		return GetValue<Noesis::Orientation>(OrientationProperty);
	}

	void MenuTickBar::SetOrientation(Noesis::Orientation orientation)
	{
		SetValue<Noesis::Orientation>(OrientationProperty, orientation);
		OnPropertyChanged("Orientation");
	}

	void MenuTickBar::OnRender(Noesis::DrawingContext* context)
	{
		float max = GetMaximum();
		float min = GetMinimum();
		float tickfreq = GetTickFrequency();
		float actualwidth = GetActualWidth();
		float actualheight = GetActualHeight();
		float radiusX = GetRadiusX();
		float radiusY = GetRadiusY();
		Noesis::Orientation orientation = GetOrientation();

		// Calculate the tickcount
		int tickCount = (int)((max - min) / tickfreq);

		double length = orientation == Noesis::Orientation::Orientation_Horizontal ? actualwidth - radiusX : actualheight - radiusY; // Add the radius of a dot, so that the center of the left and right most dots are within the drawing area

		double tickFrequencySize = length * tickfreq / tickCount;
		Noesis::Brush* brush = GetBrush();
		for (int i = 0; i <= tickCount; ++i) {
			Noesis::Point point = orientation == Noesis::Orientation::Orientation_Horizontal ?
				Noesis::Point(static_cast<float>(tickFrequencySize * i) + (radiusX * 0.5f), 25) :
				Noesis::Point(25, static_cast<float>(tickFrequencySize * i) + (radiusY * 0.5f));
				// offset the drawing area (all dots) by half size of a single dot, to align all dots with the slider

			context->DrawEllipse(brush, Pen, point, radiusX, radiusY);
		}
	}

	Noesis::PropertyChangedEventHandler& MenuTickBar::PropertyChanged()
	{
		return PropertyChangedEventHandler;
	}

	void MenuTickBar::OnPropertyChanged(const char* name)
	{
		if (PropertyChangedEventHandler) {
			PropertyChangedEventHandler(this, Noesis::PropertyChangedEventArgs(Noesis::Symbol(name)));
		}
	}
}

NS_BEGIN_COLD_REGION

NS_IMPLEMENT_REFLECTION(UI::MenuTickBar, "SomeCompany.Controls.MenuTickBar")
{
	NsProp("Brush", &MenuTickBar::GetBrush, &MenuTickBar::SetBrush);
	NsProp("RadiusX", &MenuTickBar::GetRadiusX, &MenuTickBar::SetRadiusX);
	NsProp("RadiusY", &MenuTickBar::GetRadiusY, &MenuTickBar::SetRadiusY);

	Noesis::DependencyData* orientationdata = NsMeta<Noesis::DependencyData>(Noesis::TypeOf<SelfClass>());
	orientationdata->RegisterProperty<Noesis::Orientation>(OrientationProperty, "Orientation", Noesis::PropertyMetadata::Create(Noesis::Orientation::Orientation_Vertical));

	NsImpl<INotifyPropertyChanged>();
}

NS_END_COLD_REGION
c# version of MenuTickBar
public class MenuTickBar : TickBar, INotifyPropertyChanged
	{
		private Brush brush;
		public Brush Brush
		{
			get { return brush; }
			set { brush = value; OnPropertyChanged(); }
		}

		private double radiusX = 5.0;
		public double RadiusX
		{
			get { return radiusX; }
			set { radiusX = value; OnPropertyChanged(); }
		}

		private double radiusY = 5.0;
		public double RadiusY
		{
			get { return radiusY; }
			set { radiusY = value; OnPropertyChanged(); }
		}

		public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(MenuTickBar), new PropertyMetadata(null));
		public Orientation Orientation
		{
			get { return (Orientation)GetValue(OrientationProperty); }
			set
			{
				SetValue(OrientationProperty, value);
			}
		}

		public event PropertyChangedEventHandler PropertyChanged;
		private void OnPropertyChanged([CallerMemberName] string caller = "")
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(caller));
			}
		}

		protected override void OnRender(DrawingContext dc)
		{
			// Calculate the tickcount
			int tickCount = (int)((Maximum - Minimum) / TickFrequency);

			double length = Orientation == Orientation.Horizontal ? ActualWidth - RadiusX : ActualHeight - RadiusY; // Add the radius of a dot, so that the center of the left and right most dots are within the drawing area

			double tickFrequencySize = length * TickFrequency / tickCount;

			for (int i = 0; i <= tickCount; ++i)
			{
				Point point = Orientation == Orientation.Horizontal ? new Point(tickFrequencySize * i + (RadiusX * 0.5), 25) : new Point(25, tickFrequencySize * i + (RadiusY * 0.5)); // offset the drawing area (all dots) by half size of a single dot, to align all dots with the slider
				dc.DrawEllipse(Brush, new Pen(), point, RadiusX, RadiusY);
			}
		}
	}
 
User avatar
sfernandez
Site Admin
Posts: 2991
Joined: 22 Dec 2011, 19:20

Re: Plain Properties with INotifyPropertyChanged VS Dependency Properties

02 Dec 2022, 16:33

Hello,

First I want to suggest you use our Ptr smart pointer to keep a reference to the objects owned by the MenuTickBar, that way you don't have to worry about the object references and if it is safe to delete the object or not:
class MenuTickBar ...
{
  ...
  Noesis::Ptr<Noesis::Brush> Brush = nullptr;
  Noesis::Ptr<Noesis::Pen> Pen = nullptr;
};
MenuTickBar::MenuTickBar(): Pen(Noesis::MakePtr<Noesis::Pen>()) { }
MenuTickBar::~MenuTickBar() { // do nothing, Ptr will automatically release its own references }
void MenuTickBar::SetBrush(Noesis::Brush* brush)
{
  if (Brush != brush) {
    Brush.Reset(brush);
    OnPropertyChanged("Brush");
  }
}
...
Regarding your question, Noesis implements control template instantiation a bit different than WPF. In Noesis we use a clone system to generate the visual tree of the template. Dependency properties are automatically cloned, but other members in your class need to be manually copied, this is why FrameworkElement exposes a virtual CloneOverride method. You should implement it something like this:
void MenuTickBar::CloneOverride(Noesis::FrameworkElement* clone, Noesis::FrameworkTemplate* template_) const
{
  MenuTickBar* tickbar = (MenuTickBar*)clone;
  tickbar->Brush = Brush;
  tickbar->Pen = Pen;
  tickbar->RadiusX = RadiusX;
  tickbar->RadiusY = RadiusY;
  ParentClass::CloneOverride(clone, template_);
}
But I recommend you use dependency properties in your controls to benefit from all their advantages (low memory usage, automatic change notification, bindings, animation, styling...).

Hope this helps.
 
manuel
Topic Author
Posts: 13
Joined: 16 Aug 2022, 10:06

Re: Plain Properties with INotifyPropertyChanged VS Dependency Properties

02 Dec 2022, 17:29

Yes, indeed that helps a lot. I was quite confused seeing that the API creates two default objects at startup but their copy constructor has never been used. So they seemed to be worthless at first glance. Now knowing that this is done in favor of a CloneOverride method, makes sense to me.

Thanks

Who is online

Users browsing this forum: No registered users and 30 guests