Plain Properties with INotifyPropertyChanged VS Dependency Properties
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:
The Slider references the following Style:
The Style references the following ControlTemplates, one vertical and one horizontal version:
Both ControlTemplates create my custom MenuTickBar that looks like the following:
c++ MenuTickBar - Header
c++ MenuTickBar - Implementation:
c# version of MenuTickBar
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:
Code: Select all
<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}"
/>
Code: Select all
<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>
Code: Select all
<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>
c++ MenuTickBar - Header
Code: Select all
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)
};
}
Code: Select all
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
Code: Select all
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);
}
}
}
-
sfernandez
Site Admin
- Posts: 2991
- Joined:
Re: Plain Properties with INotifyPropertyChanged VS Dependency Properties
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:
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:
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.
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:
Code: Select all
class MenuTickBar ...
{
...
Noesis::Ptr<Noesis::Brush> Brush = nullptr;
Noesis::Ptr<Noesis::Pen> Pen = nullptr;
};
Code: Select all
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");
}
}
...
Code: Select all
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_);
}
Hope this helps.
Re: Plain Properties with INotifyPropertyChanged VS Dependency Properties
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
Thanks
Who is online
Users browsing this forum: No registered users and 30 guests