#c# #wpf #binding
#c# #wpf #привязка
Вопрос:
Я создал пользовательский элемент управления в WPF, который имеет звездный рейтинг. Когда есть 5 звезд, они должны быть золотыми.
<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;
}
}
Если я запускаю это, я получаю это исключение:
Исключение InvalidOperationException: должно иметь ненулевое значение для «привязки».
Я что-то неправильно связываю в своем состоянии? Потому что, если я удалю Rating
-Binding , это сработает, но я не вижу здесь своей ошибки.
Комментарии:
1. Имейте в виду, что установщик свойства Rating не будет вызываться, когда вы устанавливаете свойство в XAML, например, с помощью привязки like
<local:RatingControl Rating="{Binding SomeProperty}"/>
. WPF обходит установщик и вызываетSetValue
напрямую. Поэтому у вас не должно быть ничего другого в установщике, кромеSetValue(RatingProperty, value);
2. Да, вы правы, я должен написать этот код внутри dependencyproperty
3. Нет, код из средства установки свойств должен находиться внутри принудительного обратного вызова. Здесь вы можете изменить текущее значение, прежде чем оно будет окончательно передано свойству.
Ответ №1:
В ваших привязках есть несколько проблем Condition
:
- Первая привязка в
MultiDataTrigger
использует aCondition
, который устанавливаетProperty
, что неверно.
Для MultiDataTrigger каждое условие в коллекции должно устанавливать как свойства привязки, так и значения. Для получения дополнительной информации см. раздел Привязка.
Вместо этого установите
Binding
свойство, используяRelativeSource
привязкуSelf
к.<Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="True"/>
- Вторая привязка к
Rating
ошибочна, поскольку она только когда-либо устанавливала бы любую звезду золотой и непрозрачной, если рейтинг равен ровно пяти звездам. Таким образом, для любого другого рейтинга не выбираются звезды.
Чтобы решить эти проблемы, вы можете использовать простое Trigger
IsChecked
свойство on.
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="star" Property="Fill" Value="Gold" />
<Setter TargetName="star" Property="Opacity" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
Если это действительно ваше требование, чтобы звезды были золотыми и непрозрачными только в том случае, если рейтинг 5
точен, тогда вы могли бы исправить MultiDataTrigger
, как указано выше:
<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>
Еще одно замечание о принуждении к значению. Как указано в комментариях @Clemens, установка свойства в XAML, например, как показано ниже, обойдет ваш установщик и
вызовет SetValue
напрямую. Ваш установщик должен вызывать только SetValue
так, как в противном случае поведение будет отличаться при настройке свойств в XAML или через свойство.
<local:RatingControl Rating="{Binding SomeProperty}"/>
Вместо проверок в установщике вы можете указать обратный вызов принуждения к значению в объявлении свойства зависимости.
public static readonly DependencyProperty RatingProperty = DependencyProperty.Register(
nameof(Rating), typeof(int), typeof(RatingControl), new FrameworkPropertyMetadata(
0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, RatingChanged, CoerceRating));
Затем создайте CoerceRating
метод, который содержит ваши проверки и возвращает соответствующие значения.
private static object CoerceRating(DependencyObject d, object baseValue)
{
var value = (int)baseValue;
if (value < 0)
{
return 0;
}
if (value > Max)
{
return Max;
}
return value;
}
Наконец, удалите все проверки из установщика Rating
.
public int Rating
{
get => (int)GetValue(RatingProperty);
set => SetValue(RatingProperty, value);
}
Обратный вызов с принудительным значением будет вызван автоматически, когда свойство будет установлено через SetValue
и, следовательно, убедитесь, что Rating
значение находится в пределах допустимого интервала.
Комментарии:
1. Хорошо, теперь я понимаю, большое вам спасибо! Один из лучших ответов, когда-либо встречавшихся на SO. Также спасибо за то, что показали, как использовать CoerceValueCallback .