WPF嵌套用户控件:如何避免重复代码并暴露子控件属性
刚接触WPF,已实现仅允许输入数字的NumberOnlyTextBox用户控件,包含Value、ValueStringFormat等属性;将其嵌套到NumericUpDownControl后,不得不重复编写子控件的属性代码,有以下疑问:
- 为何必须在
NumericUpDownControl中重复NumberOnlyTextBox的属性? - 是否有更简便的写法避免代码重复?能否在
NumericUpDownControl的XAML中直接暴露NumberOnlyTextBox的属性?
相关代码
NumberOnlyTextBox XAML
<UserControl x:Class="PracticeWPF.UserControls.TextBoxNumberOnly" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:PracticeWPF.UserControls" mc:Ignorable="d" d:DesignHeight="30" d:DesignWidth="120"> <Grid> <TextBox Text="{Binding ValueString, Mode=OneWay}" PreviewTextInput="TextBox_PreviewTextInput" LostFocus="TextBox_LostFocus" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Background="Transparent" FontSize="{Binding RelativeSource={RelativeSource AncestorType=UserControl},Path=FontSize}"/> </Grid> </UserControl>
NumberOnlyTextBox C#
public partial class TextBoxNumberOnly : UserControl, INotifyPropertyChanged { private static readonly Regex _regex = new Regex("[0-9.-]+"); private double _value = 0; private string _valueString = ""; private string _valueStringFormat = "#"; public event PropertyChangedEventHandler? PropertyChanged; public double Value { get { return _value; } set { _value = value; ValueString = _value.ToString(_valueStringFormat); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); } } public string ValueStringFormat { get { return _valueStringFormat; } set { _valueStringFormat = value; ValueString = _value.ToString(_valueStringFormat); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ValueStringFormat")); } } public string ValueString { get { return _valueString; } private set { _valueString = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ValueString")); } } public TextBoxNumberOnly() { InitializeComponent(); DataContext = this; } private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { e.Handled = !IsTextAllowed(e.Text, _regex); if (((TextBox)sender).Text.Contains(".") && (e.Text == ".")) e.Handled = true; } private static bool IsTextAllowed(string text, Regex regex) { return regex.IsMatch(text); } private void TextBox_LostFocus(object sender, RoutedEventArgs e) { if (string.IsNullOrEmpty(((TextBox)sender).Text)) { ((TextBox)sender).Text = "0"; } } }
NumericUpDownControl XAML
<UserControl x:Class="PracticeWPF.UserControls.IntegerUpDown" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:PracticeWPF.UserControls" mc:Ignorable="d" d:DesignHeight="80" d:DesignWidth="120"> <Grid Background="Wheat"> <Grid.ColumnDefinitions> <ColumnDefinition Width="2*"/> <ColumnDefinition Width="1*"/> </Grid.ColumnDefinitions> <local:TextBoxNumberOnly x:Name="tbnoValueDisplay" Grid.Column="0"/> <Grid Grid.Column="1"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Button Name="UpButton" Grid.Row="0" Background="Green" Click="UpButton_Click"/> <Button Name="DownButton" Grid.Row="1" Background="Red" Click="DownButton_Click"/> </Grid> </Grid> </UserControl>
NumericUpDownControl C#
public partial class IntegerUpDown : UserControl, INotifyPropertyChanged { private double _value; private string _valueStringFormat = "#"; private string _valueString = ""; public double Value { get { return _value; } set { _value = value; ValueString = _value.ToString(_valueStringFormat); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value")); } } public string ValueStringFormat { get { return _valueStringFormat; } set { _valueStringFormat = value; ValueString = _value.ToString(_valueStringFormat); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ValueStringFormat")); } } public string ValueString { get { return _valueString; } private set { _valueString = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ValueString")); } } public IntegerUpDown() { DataContext = this; InitializeComponent(); } public event PropertyChangedEventHandler? PropertyChanged; private void UpButton_Click(object sender, RoutedEventArgs e) { Value++; } private void DownButton_Click(object sender, RoutedEventArgs e) { Value--; } }
问题解答
1. 为何必须重复属性?
WPF的UserControl是封装性控件,内部子控件(比如这里的tbnoValueDisplay)默认对外不可访问,外部代码无法直接通过NumericUpDownControl实例操作子控件的Value、ValueStringFormat等属性。如果要让父控件的使用者能操作这些属性,就必须在父控件上重新定义对应的属性——否则外部无法感知到子控件的属性存在。
另外你当前的代码还存在逻辑脱节问题:父控件的Value变化时并未同步到子控件,子控件的输入也不会回传给父控件,重复属性只是徒增冗余代码,并未实现真正的联动。
2. 如何避免代码重复?
可以通过属性转发+绑定的方式避免重复,无需重新编写子控件的属性逻辑,只需要在父控件中关联子控件的属性即可,具体有两种方案:
方案一:直接包装子控件属性(简单高效)
修改NumericUpDownControl的C#代码,去掉重复的属性逻辑,直接转发子控件的属性:
public partial class IntegerUpDown : UserControl { public double Value { get => tbnoValueDisplay.Value; set => tbnoValueDisplay.Value = value; } public string ValueStringFormat { get => tbnoValueDisplay.ValueStringFormat; set => tbnoValueDisplay.ValueStringFormat = value; } public IntegerUpDown() { InitializeComponent(); // 移除DataContext=this,避免和子控件的DataContext冲突 } private void UpButton_Click(object sender, RoutedEventArgs e) { Value++; } private void DownButton_Click(object sender, RoutedEventArgs e) { Value--; } }
这种方式完全复用子控件的属性逻辑,父控件只做转发,没有冗余代码。
方案二:依赖属性+XAML绑定(支持数据绑定)
如果需要支持WPF的双向数据绑定特性,建议将父控件的属性定义为依赖属性,再绑定到子控件:
public partial class IntegerUpDown : UserControl { public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(IntegerUpDown), new PropertyMetadata(0.0, (d, e) => { var control = (IntegerUpDown)d; control.tbnoValueDisplay.Value = (double)e.NewValue; })); public double Value { get => (double)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } public static readonly DependencyProperty ValueStringFormatProperty = DependencyProperty.Register("ValueStringFormat", typeof(string), typeof(IntegerUpDown), new PropertyMetadata("#", (d, e) => { var control = (IntegerUpDown)d; control.tbnoValueDisplay.ValueStringFormat = (string)e.NewValue; })); public string ValueStringFormat { get => (string)GetValue(ValueStringFormatProperty); set => SetValue(ValueStringFormatProperty, value); } public IntegerUpDown() { InitializeComponent(); } private void UpButton_Click(object sender, RoutedEventArgs e) { Value++; } private void DownButton_Click(object sender, RoutedEventArgs e) { Value--; } }
也可以在XAML中直接绑定父控件和子控件的属性(与依赖属性回调二选一即可):
<local:TextBoxNumberOnly x:Name="tbnoValueDisplay" Grid.Column="0" Value="{Binding Value, RelativeSource={RelativeSource AncestorType=UserControl}}" ValueStringFormat="{Binding ValueStringFormat, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
能否直接在XAML中暴露子控件属性?
不能直接在XAML中“暴露”子控件属性给外部——XAML只是控件的声明,外部只能访问控件类公开的属性。但通过上述绑定/转发方式,可让父控件的属性与子控件属性联动,间接实现外部操作子控件属性的效果,同时彻底避免代码重复。
内容的提问来源于stack exchange,提问作者VariableSquid

