需求
需要做一组单选按钮,只要单选按钮的显示内容与需要匹配的内容一样,则该单选按钮就为选中状态,否则则为不选中状态;且需要将当前选中状态保存,后续再进入此页面时,匹配内容为此次的保存状态。
如下所示,3个单选按钮分别为Test1、Test2、Test3,需要匹配的内容为Test2。那么Test2就为选中状态,其它两个就为非选中状态。
深入分析
通过对需求的了解,可得出下述进一步需求:
单选按钮需要在选中状态下,需要将当前选中内容保存下来,也就是说需要将当前的选中内容作为匹配更新到VM中,在VM中再去实现相应的状态保存。
单选按钮在非选中状态下,可不将当前单选按钮的匹配内容清除(也可以清除),同时要保证这一组单选按钮有一个按钮是选中状态。
根据上述分析,从代码实现上需要考虑以下问题:
匹配内容需要绑定。
首次加载控件时,需要根据匹配内容设置当前控件是否为选中状态。
选中时需要将匹配内容更新,以便VM根据匹配内容的变化以保存它。
取消选中时,可考虑清空匹配内容,若要清空匹配匹配内容,那么在保存匹配内容时需要注意,不要将空值作为匹配内容的一个值保存;或者说空值不需要触发匹配内容的保存功能。
代码实现
按上述深入分析,那么仅需要自定义一个自定义单选按钮即可实现相应功能。详细代码如下:
public class CustomRadio : RadioButton
{
static CustomRadio()
{
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(CustomRadio), new PropertyMetadata(string.Empty, TextChanged));
private static void TextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var radio = (CustomRadio)d;
var ntxt = e.NewValue as string;
// 获取Content属性的BindingExpression
//var bindingExpression = BindingOperations.GetBindingExpression(radio, ContentControl.ContentProperty);
//if (bindingExpression != null)
//{
// // 强制更新绑定
// bindingExpression.UpdateTarget();
//}
// 现在可以获取到更新后的Content值
var content = radio.Content?.ToString();
if (content == ntxt)
{
radio.IsChecked = true;
}
else
{
radio.IsChecked = false;
}
}
protected override void OnChecked(RoutedEventArgs e)
{
base.OnChecked(e);
// 当RadioButton被选中时,更新Text属性
Text = Content == null ? string.Empty : Content.ToString();
}
protected override void OnUnchecked(RoutedEventArgs e)
{
base.OnUnchecked(e);
// 当RadioButton未被选中时,清空Text属性
Text = string.Empty;
}
}
注:上述代码中的Text依赖属性即是用于匹配内容的绑定。
以上为测试时使用的VM:
internal partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
TestMethod testMethod = new TestMethod();
[ObservableProperty]
string testName = "Test2";
[ObservableProperty]
ObservableCollection<Student> students = [
new(){Id=1,Name="test1",Age=12,Grade=1,Y=50},
new(){Id=2,Name="test2",Age=12,Grade=2,Y=100},
new(){Id=3,Name="test3",Age=12,Grade=3,Y=150},
new(){Id=4,Name="test4",Age=12,Grade=4,Y=200},
new(){Id=5,Name="test5",Age=12,Grade=5,Y=250},];
Timer timer = new();
public MainWindowViewModel()
{
timer.Start();
timer.Interval = 2000;
timer.Elapsed += Timer_Elapsed;
}
[RelayCommand]
public void StopTimer()
{
timer.Stop();
}
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
Random random = new();
var index = random.Next(0, Students.Count);
Students[index].Grade = random.Next(1, 101);
var order = Students.OrderBy(e => e.Grade);
//Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() =>
//{}));
int i = 0;
foreach (Student student in order)
{
student.Id = ++i;
student.OldY = student.Y;
student.Y = i * 50;
student.IsUp = student.Y > student.OldY;
//Students[i - 1] = student;
}
}
}
public partial class Student : ObservableObject
{
[ObservableProperty]
private int id;
[ObservableProperty]
private string name = string.Empty;
[ObservableProperty]
private int age;
[ObservableProperty]
private int grade;
[ObservableProperty]
private int y;
[ObservableProperty]
private int oldY;
[ObservableProperty]
private bool isUp;
}
public partial class TestMethod : ObservableObject
{
[ObservableProperty]
string one = "Test1";
[ObservableProperty]
string two = "Test2";
[ObservableProperty]
string three = "Test3";
}
以下为测试时使用的WPF UI:
<Window
x:Class="WpfApp1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:control="clr-namespace:WpfApp1.Control"
xmlns:converter="clr-namespace:WpfApp1.Converter"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewmodels="clr-namespace:WpfApp1.VM"
x:Uid="Window1Title"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.DataContext>
<viewmodels:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel>
<control:CustomRadio
Content="{Binding TestMethod.One}"
GroupName="test1"
Text="{Binding TestName, Mode=TwoWay}" />
<control:CustomRadio
Content="{Binding TestMethod.Two}"
GroupName="test1"
Text="{Binding TestName, Mode=TwoWay}" />
<control:CustomRadio
Content="{Binding TestMethod.Three}"
GroupName="test1"
Text="{Binding TestName, Mode=TwoWay}" />
<Button Command="{Binding StopTimerCommand}" Content="Close" />
</StackPanel>
</Grid>
</Window>
更优实现
然在搞定此自定义的RadioButton后,若只是单一的一行RadioButton,那么其实还可以有其它实现方式,比如用集合控件。
比如使用ListBox,这样还不用多次使用自定义的RadioButton,只需要绑定时将VM中的集合绑定于ListBox即可。
以下为ListBox结合CustomRadio的实现:
<StackPanel>
<ListBox x:Name="list" ItemsSource="{Binding Methods}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<control:CustomRadio
Margin="5"
Content="{Binding}"
GroupName="test1"
Text="{Binding DataContext.TestName, Mode=TwoWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}}}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Command="{Binding StopTimerCommand}" Content="{x:Static loc:Resources.TextBlock1_TextBlock_Text}" />
</StackPanel>
然,上述ListBox的实现并不是最好的,它存在以下问题:
每次ListBox内的单选按钮的选中与取消,会触发CustomRadio中的方法OnChecked(原按钮)与OnUnchecked(现按钮),这有两次调用才能将匹配内容更新;而只要改为ListBox的选中事件,那么只需要调用一次就可以更新匹配内容,也就是说从性能上来说,使用ListBox与CustomRadio的组合不是最优,最好是完全自定义ListBox来实现需求。