XAML 与数据网格的SelectedItem链接时的WPF RadioButton绑定问题

oipij1gg  于 2023-05-11  发布在  其他
关注(0)|答案(1)|浏览(134)

首先,对不起我的英语。我正在做的一个项目遇到了问题,但我无法指出问题出在哪里。
如果有人能帮助我或给予我一个建议来解决这个问题,我将非常感激。
我可以创建一个最小复制案例来更好地解释我的问题:
让我们创建一个名为Foo的类:

public class Foo
    {
        public Foo(byte _id, string _name)
        { 
            ID = _id;
            Name = _name;
        }
        public byte ID { get; set; }
        public string Name { get; set; }
        public bool Option1 { get; set; }
        public bool Option2 { get; set; }
        public bool Option3 { get; set; }
    }

然后在主窗口中,让我们创建一个名为“Foos”的“ObservableCollection”,并使用一些值填充它:

public partial class MainWindow : Window
    {
        public ObservableCollection<Foo> Foos { get; set; } = new ObservableCollection<Foo>();
        public MainWindow()
        {
            InitializeComponent();

            DataContext = this;

            Foos.Add(new Foo(1, "foo 1") { Option1 = true });
            Foos.Add(new Foo(2, "foo 2") { Option2 = true });
            Foos.Add(new Foo(3, "foo 3") { Option3 = true });

        }
    }

在UI中,我现在有一个绑定到Foos的数据网格,显示一些元素(Name & ID)。
在右侧,我有一些RadioButton链接到数据网格的“SelectedItem”,显示选项1,2&3:

<StackPanel Orientation="Horizontal">
    <DataGrid x:Name="DtgSource" ItemsSource="{Binding Foos}"
              AutoGenerateColumns="False"
              CanUserAddRows="False"
              Width="100"
              SelectionChanged="DtgSource_SelectionChanged"
              SelectedValuePath="Name">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding ID}" IsReadOnly="True"/>
            <DataGridTextColumn Binding="{Binding Name}" IsReadOnly="True"/>
        </DataGrid.Columns>
    </DataGrid>
    <StackPanel Orientation="Vertical" >
        <RadioButton GroupName="Options" IsChecked="{Binding ElementName=DtgSource,
            Path=SelectedItem.Option1}" Content="Option 1"/>
        <RadioButton GroupName="Options" IsChecked="{Binding ElementName=DtgSource,
            Path=SelectedItem.Option2}" Content="Option 2"/>
        <RadioButton GroupName="Options" IsChecked="{Binding ElementName=DtgSource,
            Path=SelectedItem.Option3}" Content="Option 3"/>
    </StackPanel>
</StackPanel>

问题是,有时,当我在数据网格中选择一个对象时,即使我从未单击任何单选按钮,选项也会更改:我有这样的行为:

  • 选择Foo 1:单选按钮状态:Op 1:真|Op 2:假|Op 3:假
  • 选择Foo 2:单选按钮状态:Op 1:假|Op 2:真|Op 3:假
  • 选择Foo 3:单选按钮状态:Op 1:假|Op 2:假|Op 3:真
  • 选择Foo 1:单选按钮状态:Op 1:真|Op 2:假|Op 3:假
  • 选择Foo 3:单选按钮状态:Op 1:假|Op 2:假|Op 3:否

我是否错过了什么,或者是否有更好的实现来避免这种行为?在我正在做的项目中,除了radiobutton,我还有其他控件,我只在RadioButton上遇到这个问题。
我尝试了其他控件的选择面板,如列表视图和列表框。相同的行为
我还尝试以不同的方式实现Foo,以获得更多的信息来调试,并捕捉选项被修改的时刻:
SelectionChanged方法:

private void DtgSource_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var d = sender as DataGrid;
        Foo selitem = d.SelectedItem as Foo;
        Console.WriteLine($"SelectionChanged to {d.SelectedValue}| options -> {selitem.Option1}|{selitem.Option2}|{selitem.Option3}");
    }

用于调试的选项实现:

public class Foo
    {
        private bool option1;
        public bool Option1 { get => option1; set { if (option1 != value) { option1 = value; Console.WriteLine($"{Name}|Option1 set to {value}"); } } }
    //...

如前所述,控制台中的输出如下:

SelectionChanged to foo 1| options -> True|False|False 
SelectionChanged to foo 2| options -> False|True|False  
SelectionChanged to foo 3| options -> False|False|True 
foo 3|Option3 set to False
SelectionChanged to foo 1| options -> True|False|False
SelectionChanged to foo 3| options -> False|False|False

通过在任何选项的'set'上插入一个断点并查看调用堆栈,我看到调用已经由'external code' -> UI完成
/!\在问这个问题之前,我发现:wpf RadioButton Binding Issue这似乎是相关的。但公认的答案是避免使用'GroupName'属性。这是我尝试过的事情,可悲的是,行为完全相同。
正如我所说,任何帮助都将非常感激,以了解这里正在发生的事情:)

hec6srdp

hec6srdp1#

    • 说明**

TL; DR-RadioButton组在将IsChecked双向绑定到单个bool属性并且绑定源更改时会出现问题。
这里的根本问题是,当SelectedItem更改时,每个RadioButton的相应IsChecked属性一次只更新一个。然而,在分组模式下,RadioButton被编程为当任何一个按钮变为true时立即将组中的其他按钮更新为false,这当然是有意义的。
不幸的副作用是,当您更改选择时,有一段时间按钮并不都指向同一个对象,但WPF仍然将按钮视为一组,因此错误对象的OptionX属性被设置为false。
以下是发生的步骤:
初始状态-按钮未绑定。在网格中选择Foo1。

  1. Button1.IsChecked = Foo1.Option1 (true)。其他按钮仍然未绑定,没有进一步操作。
  2. Button2.IsChecked = Foo1.Option2 (false)。没有进一步的操作。
  3. Button3.IsChecked = Foo1.Option3 (false)。没有进一步的操作。
    在网格中选择Foo2。
  4. Button1.IsChecked = Foo2.Option1 (false)。没有进一步的操作。
  5. Button2.IsChecked = Foo2.Option2 (true)。然而,这也会导致:Foo2.Option1.IsChecked = Button1.IsChecked = false和**Foo1.Option3.IsChecked = Button3.IsChecked = false**。后者不是错别字;这是一个不匹配,因为Button3仍然绑定到Foo1,尽管它是无害的,因为Foo1.Option3已经是false。
  6. Button3.IsChecked = Foo2.Option3 (false).
    在网格中选择Foo1。
  7. Button1.IsChecked = Foo1.Option1 (true)。这也导致:* * Foo2.Option2.IsChecked = Button2.IsChecked = falseFoo2.Option3.IsChecked = Button3.IsChecked = false另一个不匹配。它对Foo2.Option3是无害的,因为它无论如何都是false,但它错误地强制Foo2.Option2设置为false,因为Button2还没有将其绑定源切换回Foo1,因此它错误地输出Foo2**.Option2而不是Foo1.Option2。
  8. Button2.IsChecked = Foo1.Option2 (false).
  9. Button3.IsChecked = Foo1.Option3 (false).
    在网格中选择Foo2。
  10. Button1.IsChecked = Foo2.Option1 (false).
  11. Button2.IsChecked = Foo2.Option2 (false).
  12. Button3.IsChecked = Foo2.Option3 (false)
    因此,您可以看到问题是,绑定源每次为每个RadioButton更改一个,而不是原子地更改。当任何绑定更改导致RadioButton被选中时,组中的其他按钮会立即受到影响,即使它们自己的绑定源尚未更新。因此,如果绑定源可能更改,您就不能安全地将IsChecked与分组的RadioButton绑定到单个bool支持属性。这是否可以被称为WPF中的bug,我将留给其他人来发表意见,但至少这种行为是可以解释的。
    • 解决方法**

如果你想坚持使用单独的bool支持属性,并且不想处理枚举,一个解决方案是取消RadioButton的分组,并在视图模型端处理独占性,如下所示:

public class Foo : INotifyPropertyChanged
{
    public Foo(byte _id, string _name)
    {
        ID = _id;
        Name = _name;
    }
    public byte ID { get; set; }
    public string Name { get; set; }

    bool _option1;
    public bool Option1
    {
        get => _option1;
        set
        {
            if (value == _option1)
                return;
            _option1 = value;
            if (value)
            {
                this.Option2 = false;
                this.Option3 = false;
            }
            OnPropertyChanged();
        }
    }

    bool _option2;
    public bool Option2
    {
        get => _option2;
        set
        {
            if (value == _option2)
                return;
            _option2 = value;
            if (value)
            {
                this.Option1 = false;
                this.Option3 = false;
            }
            OnPropertyChanged();
        }
    }

    bool _option3;
    public bool Option3 
    {
        get => _option3;
        set
        {
            if (value == _option3)
                return;
            _option3 = value;
            if (value)
            {
                this.Option1 = false;
                this.Option2 = false;
            }
            OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName]string property = null)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
    }
}

(Don不要省略属性更改通知的额外代码,否则当一个按钮更改时,UI不会更新其他按钮)。
在XAML中:

<StackPanel x:Name="_stack"
                    Orientation="Vertical" DataContext="{Binding ElementName=DtgSource, Path=SelectedItem}">
            <RadioButton GroupName="1" 
                         IsChecked="{Binding Option1}" Content="Option 1" />
            <RadioButton GroupName="2"
                         IsChecked="{Binding Option2}" Content="Option 2" />
            <RadioButton GroupName="3"
                         IsChecked="{Binding Option3}" Content="Option 3" />
        </StackPanel>

在这里,您需要指定不同的组名来覆盖默认的分组行为。

    • 其他变通办法**

无枚举解决方法的一个缺点是,它不能很好地扩展,如果您每次都不正确地输出正确的属性,可能会导致细微的错误。
如果你愿意使用枚举,那么下面评论中的Rufo爵士的例子很棒,值得作为自己的答案。另一个很好的选择(实际上是我的首选)是这个问题的公认答案:
How do you work around the binding problems with radio button groups
这两个属性绑定可以工作,而单个bool属性绑定不能工作的原因有点微妙,但最终它们都可以防止在RadioButton由于处于互斥组中而被UI取消选中时更改backing属性。

相关问题