WPF中遇到的一个关于RadioButton的数据绑定问题

最近在重构代码的时候遇到一个WPF相关的问题,在使用MVVM pattern时,给WPF RadioButton建立绑定数据源时,理所当然的想到使用boolean类型。但是发生了一个奇怪的现象。废话不多说,直接上sample 代码。

问题代码

完整sample代码在github上。

  • TestClass.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public enum RW
    {
    Read, Write
    }

    public class TestClass
    {

    public RW ReadOrWrite { get; set; }
    }
  • TestViewModel.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class TestViewModel : ViewModelBase
    {
    private readonly TestClass testclass;
    public TestViewModel(TestClass tc)
    {
    testclass = tc;
    }

    public bool IsRead
    {
    get { return testclass.ReadOrWrite == RW.Read; }
    set
    {
    testclass.ReadOrWrite = value ? RW.Read : RW.Write;
    OnPropertyChanged("IsRead");
    }
    }

    public bool IsWrite
    {
    get { return testclass.ReadOrWrite == RW.Write; }
    set
    {
    testclass.ReadOrWrite = value ? RW.Write : RW.Read;
    OnPropertyChanged("IsWrite");
    }
    }
    }
  • TestWindow.xaml

    1
    2
    3
    4
    <StackPanel >
    <RadioButton GroupName="test" Content="read" IsChecked="{Binding IsRead, Mode=TwoWay}"/>
    <RadioButton GroupName="test" Content="write" IsChecked="{Binding IsWrite, Mode=TwoWay}"/>
    </StackPanel>

    代码很简单,有两个window: MainWindowViewModel, TestWindow。TestWindow有两个RadioButton,分别建立双向绑定TestViewModel中的两个属性。在MainWindow中传递TestViewModel给TestWindow,作为其DataContext。同时在MainWindow有一个button,点击打开TestWindow。当点击button,然后关闭TestWindow,重复几遍,发现TestWindow中的RadioButton 来回自动切换状态!

第一次点击:

  • read
  • write

第二次点击:

  • read
  • write

第三次点击:

  • read
  • write

第四次点击:

  • read
  • write

如此反复…

问题分析

为什么在“数据源不变”的情况下,RadioButton的状态会变化呢?这是一个很简单的数据绑定而已! 在TestViewModel中属性的getter和setting中打上断点发现:程序会”自动”调用两个属性的setter。在代码中没有直接给属性赋值,所以setter的调用肯定是WPF搞的鬼了。

会不会是因为两个RadioButton属于同一个Group,所以在选择一个的时候,WPF会自动将Group内的置为Uncheck状态?在xaml中去掉GroupName之后,果然没有出现这个问题了。

查看Reference code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//file RadioButton.cs
[ThreadStatic] private static Hashtable _groupNameToElements;

protected override void OnChecked(RoutedEventArgs e)
{
// If RadioButton is checked we should uncheck the others in the same group
UpdateRadioButtonGroup();
base.OnChecked(e);
}

private void UpdateRadioButtonGroup()
{
string groupName = GroupName;
if (!string.IsNullOrEmpty(groupName))
{
Visual rootScope = KeyboardNavigation.GetVisualRoot(this);
if (_groupNameToElements == null)
_groupNameToElements = new Hashtable(1);
lock (_groupNameToElements)
{
// Get all elements bound to this key and remove this element
ArrayList elements = (ArrayList)_groupNameToElements[groupName];
for (int i = 0; i < elements.Count; )
{
WeakReference weakReference = (WeakReference)elements[i];
RadioButton rb = weakReference.Target as RadioButton;
if (rb == null)
{
// Remove dead instances
elements.RemoveAt(i);
}
else
{
// Uncheck all checked RadioButtons different from the current one
if (rb != this && (rb.IsChecked == true) && rootScope == KeyboardNavigation.GetVisualRoot(rb))
rb.UncheckRadioButton();
i++;
}
}
}
}
else // Logical parent should be the group
{
DependencyObject parent = this.Parent;
if (parent != null)
{
// Traverse logical children
IEnumerable children = LogicalTreeHelper.GetChildren(parent);
IEnumerator itor = children.GetEnumerator();
while (itor.MoveNext())
{
RadioButton rb = itor.Current as RadioButton;
if (rb != null && rb != this && string.IsNullOrEmpty(rb.GroupName) && (rb.IsChecked == true))
rb.UncheckRadioButton();
}
}

}
}

看代码可知,RadioButton内部有一个static变量_groupNameToElements, 用来存储GroupName与RadioButton示例的对应关系。在RadioButton的状态改变时,会调用UpdateRadioButtonGroup函数。

1
2
if (rb != null && rb != this && string.IsNullOrEmpty(rb.GroupName) && (rb.IsChecked == true))
rb.UncheckRadioButton();

料想应该是这段代码设置RadioButton的状态,然后通过binding调用了属性的setter。在TestWindow ShowDialog前后打断点,查看变量_groupNameToElements.

第一次点击:

第二次点击:

第三次点击:

可以看到由于创建的RadioButton并没有马上被垃圾回收,还是残留在同一个Group中。所以在UpdateRadioButtonGroup函数中迭代elements时,执行

1
rb.UncheckRadioButton();

同时由于在这几个RadioButton中TestViewModel是共享的(即DataContext为同一个对象),所以通过setter会改变TestViewModel中的属性。而因为是双向绑定,又反过来作用与RadioButton的IsChecked状态。所以出现了上述的问题。

问题解决

至此应该很明了了,在RadioButton中使用绑定时,应该留个心眼,如果给每个RadioButton分别绑定一个属性的时候,需要注意这种情况。这种情况在xaml中直接给RadioButton去掉GroupName这个属性即可。我觉得更好的方法应该是使用其他的数据绑定方式。如stackoverflow中的一个问题,使用ListBox来模拟RadioButton.

[Simple WPF RadioButton Binding?]

延伸阅读