Reputation: 2844
I have made a custom Control in WPF which is a star rating. When there are 5 stars, these should be golden.
<UserControl x:Class="Lama.Wpf.Controls.RatingControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:controls="clr-namespace:Lama.Wpf.Controls"
mc:Ignorable="d">
<Grid>
<Grid.Resources>
<ControlTemplate x:Key="RatingTemplate" TargetType="{x:Type ToggleButton}">
<Viewbox>
<Path Name="star" Fill="White" Opacity="0.2"
Data="F1 M 145.637,174.227L 127.619,110.39L 180.809,70.7577L 114.528,68.1664L 93.2725,5.33333L 70.3262,67.569L 4,68.3681L 56.0988,109.423L 36.3629,172.75L 91.508,135.888L 145.637,174.227 Z" />
</Viewbox>
<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Property="IsChecked" Value="True" />
<Condition
Binding="{Binding Rating, RelativeSource={RelativeSource FindAncestor, AncestorType=controls:RatingControl}}"
Value="5" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter TargetName="star" Property="Fill" Value="Gold" />
<Setter TargetName="star" Property="Opacity" Value="1" />
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ToggleButton Grid.Column="0" Tag="1" Padding="2" Template="{StaticResource RatingTemplate}"
Click="ClickEventHandler" />
<ToggleButton Grid.Column="1" Tag="2" Padding="2" Template="{StaticResource RatingTemplate}"
Click="ClickEventHandler" />
<ToggleButton Grid.Column="2" Tag="3" Padding="2" Template="{StaticResource RatingTemplate}"
Click="ClickEventHandler" />
<ToggleButton Grid.Column="3" Tag="4" Padding="2" Template="{StaticResource RatingTemplate}"
Click="ClickEventHandler" />
<ToggleButton Grid.Column="4" Tag="5" Padding="2" Template="{StaticResource RatingTemplate}"
Click="ClickEventHandler" />
</Grid>
</UserControl>
public partial class RatingControl
{
private const int Max = 5;
public static readonly DependencyProperty RatingProperty = DependencyProperty.Register(nameof(Rating),
typeof(int),
typeof(RatingControl),
new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
RatingChanged));
public int Rating
{
get => (int)GetValue(RatingProperty);
set
{
if (value < 0)
{
SetValue(RatingProperty, 0);
}
else if (value > Max)
{
SetValue(RatingProperty, Max);
}
else
{
SetValue(RatingProperty, value);
}
}
}
public RatingControl()
{
InitializeComponent();
}
private static void RatingChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var item = sender as RatingControl;
var newVal = (int)e.NewValue;
var children = ((Grid)(item.Content)).Children;
ToggleButton button;
for (var i = 0; i < newVal; i++)
{
button = children[i] as ToggleButton;
if (button != null)
button.IsChecked = true;
}
for (var i = newVal; i < children.Count; i++)
{
button = children[i] as ToggleButton;
if (button != null)
button.IsChecked = false;
}
}
private void ClickEventHandler(object sender, RoutedEventArgs args)
{
var button = sender as ToggleButton;
if (button == null)
{
return;
}
var newValue = int.Parse(button.Tag.ToString());
Rating = newValue;
}
}
If I run this, I get this exception:
InvalidOperationException: Must have non-null value for 'Binding'.
Do I bind something wrong inside my condition? Because if I remove the Rating
-Binding, it works, but I don't see my mistake here.
Upvotes: 0
Views: 526
Reputation: 22119
There are multiple issues in your Condition
bindings:
The first binding in the MultiDataTrigger
uses a Condition
that sets Property
, which is wrong.
For a MultiDataTrigger, each condition in the collection must set both the Binding and Value properties. For more information, see Binding.
Set the Binding
property using a RelativeSource
binding to Self
instead.
<Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="True"/>
The second binding to Rating
is faulty, since it would only ever set any star to be golden and opaque if the rating is exactly five stars. So no stars are selected for any other rating.
In order resolve these issues, you can use a simple Trigger
on the IsChecked
property.
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="star" Property="Fill" Value="Gold" />
<Setter TargetName="star" Property="Opacity" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
If it is really your requirement to only make the stars golden and opaque if the rating is exactly 5
, then you could correct the MultiDataTrigger
as stated above:
<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="True"/>
<Condition Binding="{Binding Rating, RelativeSource={RelativeSource FindAncestor, AncestorType=controls:RatingControl}}" Value="5"/>
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter TargetName="star" Property="Fill" Value="Gold" />
<Setter TargetName="star" Property="Opacity" Value="1" />
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</ControlTemplate.Triggers>
Another note on value coercion. As @Clemens stated in the comments, setting a property in XAML e.g. like below will bypass your setter and
call SetValue
directly. Your setter should only call SetValue
as the behavior will differ otherwise when setting properties in XAML or through the property.
<local:RatingControl Rating="{Binding SomeProperty}"/>
Instead of the checks in the setter, you can specify a value coercion callback in the dependency property declaration.
public static readonly DependencyProperty RatingProperty = DependencyProperty.Register(
nameof(Rating), typeof(int), typeof(RatingControl), new FrameworkPropertyMetadata(
0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, RatingChanged, CoerceRating));
Then create a CoerceRating
method that contains your checks and returns the corresponding values.
private static object CoerceRating(DependencyObject d, object baseValue)
{
var value = (int)baseValue;
if (value < 0)
{
return 0;
}
if (value > Max)
{
return Max;
}
return value;
}
Finally, remove all checks from the setter of Rating
.
public int Rating
{
get => (int)GetValue(RatingProperty);
set => SetValue(RatingProperty, value);
}
The value coercion callback will automatically be called, when the property is set through SetValue
and therefore ensure that the Rating
value is within the valid interval.
Upvotes: 1