添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
宽容的小刀  ·  Docker+LNMP+Jenkins+ ...·  6 月前    · 
追风的手链  ·  How to send a ...·  7 月前    · 
满身肌肉的野马  ·  无法在Assets ...·  10 月前    · 

最近在重构代码的时候遇到一个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 来回 自动 切换状态!

    第一次点击:

  • write
  • 第二次点击:

  • write
  • 第三次点击:

  • write
  • 第四次点击:

  • 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? ]

  • [ How is XAML interpreted and executed at runtime? ]
  •