'Binding' 问题必须有非空值

Must have non-null value for 'Binding' problem

我在 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: Must have non-null value for 'Binding'.

我的condition里面是不是绑定了什么错误的东西?因为如果我删除 Rating-Binding,它会起作用,但我在这里看不到我的错误。

您的 Condition 绑定中存在多个问题:

  • MultiDataTrigger 中的第一个绑定使用设置 Propertywhich is wrong.

    Condition

    For a MultiDataTrigger, each condition in the collection must set both the Binding and Value properties. For more information, see Binding.

    使用 RelativeSource 绑定将 Binding 属性 设置为 Self

    <Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="True"/>
    
  • Rating 的第二个绑定是错误的,因为如果评级正好是五颗星,它只会将任何星星设置为金色和不透明。因此没有为任何其他评级选择星星。

为了解决这些问题,您可以在 IsChecked 属性.

上使用简单的 Trigger
<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 中设置 属性 例如如下所示将绕过您的 setter 和 直接调用 SetValue。您的 setter 应该只调用 SetValue,因为在 XAML 或通过 属性.

设置属性时行为会有所不同
<local:RatingControl Rating="{Binding SomeProperty}"/>

您可以在依赖项 属性 声明中指定值强制回调,而不是 setter 中的检查。

public static readonly DependencyProperty RatingProperty = DependencyProperty.Register(
   nameof(Rating), typeof(int), typeof(RatingControl), new FrameworkPropertyMetadata(
      0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, RatingChanged, CoerceRating));

然后创建一个 CoerceRating 方法,其中包含您的检查和 returns 相应的值。

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 的 setter 中的所有检查。

public int Rating
{
   get => (int)GetValue(RatingProperty);
   set => SetValue(RatingProperty, value);
}

当通过SetValue设置属性时,会自动调用值强制回调,因此确保Rating值在有效区间内。