绑定了可见性的动态复选框永远不显示

编程语言 2026-07-09

我正在使用MVVM Community Toolkit创建一个AvaloniaUI应用,在其中需要动态渲染控件,并将它们的数据绑定到一个属性在运行时才会知道的视图模型。事实上,我将从一个JSON文件中读取要绑定的属性及字段。

延续我在上一个问题的探索,我决定把所有控件类型“动态化”,从复选框开始。我做了一个用户控件,其中的 StackPanel 由两个 CheckBox 实例填充,只有在第一个被选中时,第二个才会显示。但点击第一项并不会显示第二项,且还出现其他奇怪的行为。

编辑:我刚刚发现我在上一个问题中接受的答案竟然消失了!在进入 DynamicSettingsGroupVM 之前,这位给出答案的网友让我再多准备一个类:

public class DynamicSettings : INotifyPropertyChanged {
  private readonly Dictionary<string, object?> _values = new();
  public event PropertyChangedEventHandler? PropertyChanged;

  public object? this[string key] {
    get => _values.TryGetValue(key, out var v) ? v : null;
    set {
      _values[key] = value;
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(key));
    }
  }
}

现在,用户控件的视图模型,用于实现动态属性:

public partial class DynamicSettingsGroupVM : ViewModelBase {
  [ObservableProperty]
  private string _name;
  public DynamicSettings DynamicSettingsProperties { get; } = new ();

  public DynamicSettingsGroupVM(string aName) {
    Name = aName;
    ((INotifyPropertyChanged)DynamicSettingsProperties).PropertyChanged +=
          new PropertyChangedEventHandler(HandlePropertyChanges);
  }

  private void HandlePropertyChanges(
      object? sender, PropertyChangedEventArgs e) {
    Trace.WriteLine($"{e.PropertyName} has changed to {DynamicSettingsProperties[e.PropertyName]}.");
  }
}

用户控件的XAML代码主体:

  <DockPanel Margin="10,0" Background="White">
      <TextBlock Margin="0,0,5,0" FontWeight="Bold" Text="{Binding Name}" />
    <StackPanel x:Name="stpDynamicFields" />
  </DockPanel>

用于生成动态 CheckBox 的后台代码,依据绑定到它们的 IsChecked 的属性来生成,这些名称从文件中加载:

  public void CreateDynamicCheckBoxes(string upperBoundProp, string lowerBoundProp) {
    var myDC = DataContext as ViewModels.DynamicSettingsGroupVM;
    var upperCB = new CheckBox {
      [!CheckBox.IsCheckedProperty] = new Binding($"DynamicSettingsProperties[{upperBoundProp}]"),
      Content = "Upper"
    };
    var lowerCB = new CheckBox {
      [!CheckBox.IsCheckedProperty] = new Binding($"DynamicSettingsProperties[{lowerBoundProp}]"),
      [!IsVisibleProperty] = new Binding($"DynamicSettingsProperties[{upperBoundProp}]"),
      Content = "Lower, visible when Upper checked"
    };
    myDC.DynamicSettingsProperties[upperBoundProp] = false;
    myDC.DynamicSettingsProperties[lowerBoundProp] = false;
    stpDynamicFields.Children.Add(upperCB);
    stpDynamicFields.Children.Add(lowerCB);
  }

用于初始化内部用户控件实例及其视图模型的外部用户控件的后台代码:

      var dynaSettings = new DynamicSettingsGroup();
      dynaSettings.DataContext = new DynamicSettingsGroupVM("New CheckBoxes!");
      dynaSettings.CreateDynamicCheckBoxes("IsUpperChecked", "IsLowerChecked");
      stpSettingsGroups.Children.Add(dynaSettings);

执行上述代码时,调试窗口将写入并显示以下内容:

IsUpperChecked has changed to False.
IsLowerChecked has changed to False.

enter image description here

当我点击“Upper”复选框时,出现如下调试输出,但并未出现“Lower...”:

IsUpperChecked has changed to True.

更糟的是,未选中时什么都不打印,这应该把数据绑定的属性设为 false

第一种尝试的修复

我本以为绑定的索引应该是实际的字符串,因为其他代码访问它们的方式就是如此:

    var upperCB = new CheckBox {
      [!CheckBox.IsCheckedProperty] = new Binding($"DynamicSettingsProperties[\"{upperBoundProp}\"]"),
      Content = "Upper"
    };
    var lowerCB = new CheckBox {
      [!CheckBox.IsCheckedProperty] = new Binding($"DynamicSettingsProperties[\"{lowerBoundProp}\"]"),
      [!IsVisibleProperty] = new Binding($"DynamicSettingsProperties[\"{upperBoundProp}\"]"),
      Content = "Lower, visible when Upper checked"
    };

但这现在会让两个复选框都处于可见状态,处于一种奇怪的第三状态,调试输出现在包括:

IsUpperChecked has changed to False.
IsLowerChecked has changed to False.
[Binding]An error occurred binding 'IsVisible' to 'DynamicSettingsProperties["IsUpperChecked"]': 'Could not convert '(null)' (null) to 'System.Boolean'.' (CheckBox #2428637)

enter image description here

第一次点击一个复选框会打印出“changed to False”的消息,而较低的复选框无论如何都保持可见。

第二种尝试的修复

随后我使用一个实际的 Binding 实例,以便用默认值初始化它,避免以 null 开始:

    var lowerCB = new CheckBox {
      [!CheckBox.IsCheckedProperty] = new Binding($"DynamicSettingsProperties[\"{lowerBoundProp}\"]"),
      Content = "Lower, visible when Upper checked"
    };
    var visBinding = new Binding($"DynamicSettingsProperties[\"{upperBoundProp}\"]") {
      TargetNullValue = false
    };
    lowerCB.Bind(IsVisibleProperty, visBinding);

初始调试输出中的错误从中消失,现在包含:

IsUpperChecked has changed to False.
IsLowerChecked has changed to False.
"IsUpperChecked" has changed to False.

但虽然“Lower...”被隐藏,“Upper”仍然以第三种状态启动,在第一次点击时打印“False”,再次点击时永远不会显示“Lower...”。

第三种尝试的修复

我从 Binding 的初始化参数中移除了 \",并让“Upper”以真正未选中的状态初始化,只打印出以下内容:

IsUpperChecked has changed to False.
IsLowerChecked has changed to False.

但再次地,只有在点击时才会显示为选中,并且“Lower...”永远不会出现。

第四种尝试的修复

我也为“Upper”已选状态的绑定添加了空值保护:

    var upperCB = new CheckBox {
      Content = "Upper"
    };
    var upperCheckBinding = new Binding($"DynamicSettingsProperties[{upperBoundProp}]") {
      TargetNullValue = false,
      Mode = BindingMode.TwoWay
    };
    upperCB.Bind(CheckBox.IsCheckedProperty, upperCheckBinding);

但这并没有改变任何东西。

非常感谢帮助使这个 IsVisible 绑定正常工作。

解决方案

你的 DynamicSettings 索引器必须在Avalonia索引器绑定更新时触发 PropertyChanged("Item[]")

public class DynamicSettings : INotifyPropertyChanged
{
    private readonly Dictionary<string, bool> _values = new();

    public event PropertyChangedEventHandler? PropertyChanged;

    public bool this[string key]
    {
        get => _values.TryGetValue(key, out var v) && v;
        set
        {
            if (_values.TryGetValue(key, out var existing) &&
                existing == value)
                return;

            _values[key] = value;

            PropertyChanged?.Invoke(
                this,
                new PropertyChangedEventArgs("Item[]"));
        }
    }
}

然后在绑定之前初始化数值

myDC.DynamicSettingsProperties[upperBoundProp] = false;
myDC.DynamicSettingsProperties[lowerBoundProp] = false;

并像下面这样绑定,同时也请不要在绑定路径中使用转义引号

upperCB.Bind(
    CheckBox.IsCheckedProperty,
    new Binding($"DynamicSettingsProperties[{upperBoundProp}]")
    {
        Mode = BindingMode.TwoWay
    });

lowerCB.Bind(
    IsVisibleProperty,
    new Binding($"DynamicSettingsProperties[{upperBoundProp}]"));

备选方案

你的绑定没有问题,但问题在于Avalonia不会在你使用字典键名触发 PropertyChanged 时重新评估索引器绑定。

这:

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(key));

对你的调试输出有效,但对像下面这样的绑定就无效:

DynamicSettingsProperties[IsUpperChecked]

对于索引器绑定,Avalonia期望的通知名称是:

"Item[]"

因此你的 DynamicSettings 类应该长成这样:

public class DynamicSettings : INotifyPropertyChanged
{
    private readonly Dictionary<string, bool> _values = new();

    public event PropertyChangedEventHandler? PropertyChanged;

    public bool this[string key]
    {
        get => _values.TryGetValue(key, out var v) && v;

        set
        {
            if (_values.TryGetValue(key, out var existing) &&
                existing == value)
                return;

            _values[key] = value;

            PropertyChanged?.Invoke(
                this,
                new PropertyChangedEventArgs("Item[]"));
        }
    }
}

还有一些其他原因也在促成这种奇怪的行为:

  1. object? 允许 null,这也是为什么复选框一开始就处于不确定状态的原因。如果这些是复选框绑定的值,请使用 bool
  2. 在绑定之前初始化数值,这样控件在初始时就不会看到 null
  3. 不要在绑定路径中对索引器键使用引号。这样才是正确的:
new Binding($"DynamicSettingsProperties[{upperBoundProp}]")

而不是

new Binding($"DynamicSettingsProperties[\"{upperBoundProp}\"]")

有了这些更改,这个按预期工作:

upperCB.Bind(
    CheckBox.IsCheckedProperty,
    new Binding($"DynamicSettingsProperties[{upperBoundProp}]")
    {
        Mode = BindingMode.TwoWay
    });

lowerCB.Bind(
    IsVisibleProperty,
    new Binding($"DynamicSettingsProperties[{upperBoundProp}]"));

切换到 "Item[]" 之后,当上方的复选框改变时,IsVisible 绑定会立即更新。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章