最近发现一个WPF里RadioButton的奇怪现象,由于在网上也没搜到合适的答案,遂记录一下:
现象:当一个对象集合里,对象的某些属性需要以RadioButton或者CheckBox的形式展现出来,并且会随着选择集合中不同的对象而变化时(如listbox,combobox),RadioButton绑定不会正确更新,而CheckBox则没问题;
这里以一个小例子来说明:
一个学生集合,每个学生其中包含一个性别属性Sex , 这里假如true表示男性,false表示女性:
public class Student { public int Age { get; set; } public string Name { get; set; } public bool Sex { get; set; } }
使用一个StudentViewModel作为前端的datacontext ,默认先来3个学生:
public class StudentViewModel { public ObservableCollection<Student> Students { get; set; } public Student SelectedStudent { get; set; } public StudentViewModel() { Students = new ObservableCollection<Student>(); Students.Add(new Student() { Name="Tom", Age=20,Sex=true}); Students.Add(new Student() { Name = "Jack", Age =21,Sex=false}); Students.Add(new Student() { Name = "Ailien", Age =22,Sex=true}); } }
前端用一个ListBox和两个radiobutton来展示,选择不同学生的时候, radiobutton的值不会根据我的选择来正确变化:
<Window x:Class="WpfApplication4.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApplication4" xmlns:con="clr-namespace:WpfApplication4.Converters" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <con:InverseBooleanConverter x:Key="inverseBooleanConverter" /> </Window.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <ListBox Grid.Column="0" Width="200" ItemsSource="{Binding Students}" Height="400" SelectedItem="{Binding SelectedStudent}" ScrollViewer.VerticalScrollBarVisibility="Visible"> <ListBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TextBlock Text="{Binding Name}" Grid.Column="0" HorizontalAlignment="Left" /> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Grid Grid.Column="1"> <Grid.RowDefinitions> <RowDefinition Height="20"></RowDefinition> <RowDefinition Height="20"></RowDefinition> <RowDefinition Height="40"></RowDefinition> <RowDefinition Height="20"></RowDefinition> <RowDefinition Height="20"></RowDefinition> </Grid.RowDefinitions> <RadioButton x:Name="r1" Grid.Column="1" Grid.Row="0" Margin="4,4,4,0" Content="Boy" IsChecked="{Binding SelectedStudent.Sex}" GroupName="StudentSex" /> <RadioButton x:Name="r2" Grid.Column="1" Grid.Row="1" Margin="4,4,4,0" Content="Girl" IsChecked="{Binding SelectedStudent.Sex,Converter={StaticResource inverseBooleanConverter}}" GroupName="StudentSex" /> <Button x:Name="button" Content="Button" HorizontalAlignment="Left" Height="60" Margin="72,105,0,-144" Grid.Row="4" VerticalAlignment="Top" Width="120" RenderTransformOrigin="0.5,0.5" Click="button_Click"> <Button.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform Angle="11.937"/> <TranslateTransform/> </TransformGroup> </Button.RenderTransform> </Button> </Grid> </Grid> </Window>
其中Converter代码为:
public class InverseBooleanConverter : MarkupExtension, IValueConverter { #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { bool boolVal = value == null ? false : (bool)value; return !boolVal; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (targetType != typeof(bool) && targetType != typeof(Visibility) && targetType != typeof(bool?)) { throw new NotSupportedException(); } return !(bool)value; } public override object ProvideValue(IServiceProvider serviceProvider) { return this; } public InverseBooleanConverter() : base() { } #endregion }
但是如果把两个radiobutton换成CheckBox就没问题,每次选择学生都能够根据选择的学生正确显示性别:
<CheckBox Grid.Column="1" Grid.Row="3" Content="Boy" IsChecked="{Binding SelectedStudent.Sex}"></CheckBox> <CheckBox Grid.Column="1" Grid.Row="4" Content="Girl" IsChecked="{Binding SelectedStudent.Sex, Converter={StaticResource inverseBooleanConverter}}" />
猜想可能是RadioButton和CheckBox内部原理不太一样,因为每次选择不同学生时,Radiobutton会时不时进入到ConvertBack函数中去,导致属性值错乱,而CheckBox则不会;
于是试着给每个RadioButton加上Mode=OneWay,因为RadioButton默认是TwoWay, 改完发现上面的问题没有了,能够正确更新状态,但是手动选择却不能使后台数据源属性生效了,最后发现只要给第二个RadioButton加上UpdateSourceTrigger=Explicit所有问题都解决了,后台也能根据前端界面值更改而更改:
<RadioButton x:Name="r2" Grid.Column="1" Grid.Row="1" Margin="4,4,4,0" Content="Girl" IsChecked="{Binding SelectedStudent.Sex,Converter={StaticResource inverseBooleanConverter},UpdateSourceTrigger=Explicit}" GroupName="StudentSex" />
原来UpdateSourceTrigger=Explicit是强制手动去call UpdateSource代码才使得后台数据生效,加上就相当于不要去更新第二个RadioButton的后台值,仅针对两个控件绑定一个属性的情况。
If you set the UpdateSourceTrigger value to Explicit, you must call the UpdateSource method or the changes will not propagate back to the source.
这么一想,还有一种同样的办法,就是给第二个RadioButton加上Mode=OneWay:
<RadioButton x:Name="r2" Grid.Column="1" Grid.Row="1" Margin="4,4,4,0" Content="Girl" IsChecked="{Binding SelectedStudent.Sex,Converter={StaticResource inverseBooleanConverter},Mode=OneWay}" GroupName="StudentSex" />
都能够解决问题。