WPF绑定自定义控件Dependency Property未触发属性变更问题求助
自定义DependencyProperty绑定不更新ViewModel属性的问题解决
我开发了一个仅允许数字输入的自定义NumberBox控件,暴露了可绑定的Value依赖属性,但输入内容时,控件内部的InputValidation和ValueChanged方法会触发,不过绑定的ViewModel的SomeBoundValue属性setter从未触发。相关代码如下:
NumberBox.xaml
<UserControl x:Class="CustomControls.NumberBox" 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" mc:Ignorable="d" Name="root"> <TextBox Text="{Binding ValueAsString, ElementName=root, UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Right"/> </UserControl>
NumberBox.xaml.cs
using System.Windows; using System.Windows.Controls; namespace CustomControls { public partial class NumberBox : UserControl { public string ValueAsString { get { return (string)GetValue(ValueAsStringProperty); } set { SetValue(ValueAsStringProperty, value); } } public static readonly DependencyProperty ValueAsStringProperty = DependencyProperty.Register("ValueAsString", typeof(string), typeof(NumberBox), new PropertyMetadata("0", InputValidation)); public double Value { get => (double)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(NumberBox), new PropertyMetadata(0.0, ValueChanged)); public NumberBox() { InitializeComponent(); } private static void ValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is NumberBox box) { var input = box.Value.ToString(); input = input.Replace(',', '.'); input = RemoveDuplicateDecimalSymbols(input); input = RemoveLeadingZeros(input); box.ValueAsString = input; } } private static void InputValidation(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is NumberBox box) { var input = box.ValueAsString; input = input.Replace(',', '.'); input = RemoveDuplicateDecimalSymbols(input); input = RemoveLeadingZeros(input); if (double.TryParse(input, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out double parsed)) { box.Value = parsed; } box.ValueAsString = input; } } private static string RemoveDuplicateDecimalSymbols(string input) { var split = input.Split('.'); if (split.Length == 1) return input; var retval = string.Empty; for (int i = 0; i < split.Length; i++) { var part = split[i]; retval += part; if (i == 0) retval += "."; } return retval; } private static string RemoveLeadingZeros(string input) { string returnValue = string.Empty; bool allLeadingZerosRemoved = false; for (int i = 0; i < input.Length; i++) { char c = input[i]; if (allLeadingZerosRemoved || c != '0') { returnValue += c; if (char.IsNumber(c) || (i < input.Length - 1 && input[input.Length - 1] == '.')) allLeadingZerosRemoved = true; continue; } if (c != '0') returnValue += c; } return returnValue; } } }
NumberBoxViewModel.cs
using System.ComponentModel; using System.Runtime.CompilerServices; using System.Collections.Generic; namespace CustomControls { public class NumberBoxViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "") { if (EqualityComparer<T>.Default.Equals(field, value)) return false; field = value; OnPropertyChanged(propertyName); return true; } private double someBoundValue; public double SomeBoundValue { get => someBoundValue; set { if (SetField(ref someBoundValue, value)) { int i = 0; // 用于设置断点 } } } } }
MainWindow.xaml
<Window x:Class="CustomControls.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:CustomControls" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid x:Name="MainGrid"> <local:NumberBox x:Name="Box" Value="{Binding SomeBoundValue}"/> </Grid> </Window>
MainWindow.xaml.cs
using System.Windows; namespace CustomControls { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); NumberBoxViewModel viewModel = new NumberBoxViewModel(); Box.DataContext = viewModel; //MainGrid.DataContext = viewModel; 同样无效 } } }
问题原因与解决方法
1. 核心遗漏:绑定的Mode和UpdateSourceTrigger配置
WPF中,自定义依赖属性的默认绑定模式是**OneWay(单向)**——仅当ViewModel属性更新时同步到控件,控件属性变化不会反向同步回ViewModel。而你需要双向绑定来实现控件与ViewModel的双向同步。
修改MainWindow.xaml中的绑定:
<local:NumberBox x:Name="Box" Value="{Binding SomeBoundValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
2. 属性与DependencyProperty的绑定机制解释
- DependencyProperty(DP):WPF核心属性系统,支持绑定、样式、动画等特性。绑定的本质是建立控件DP与数据源属性的同步通道。
- 绑定方向:
OneWay:数据源→控件,仅数据源变化时更新控件TwoWay:双向同步,控件与数据源的变化互相触发更新OneWayToSource:控件→数据源,仅控件变化时更新数据源
- 更新触发时机:
UpdateSourceTrigger决定控件属性变化后通知数据源的时机:PropertyChanged:属性值变化立即更新LostFocus:控件失去焦点时更新Explicit:需手动调用UpdateSource()
当你在NumberBox内部设置box.Value = parsed;时,若绑定为TwoWay,WPF会自动将新值同步到ViewModel的SomeBoundValue属性。
3. 自定义DP和内置DP的行为差异
内置DP(如TextBox.Text)的默认绑定模式是TwoWay,因为这类控件用于用户输入,天然需要双向同步。而自定义DP的默认模式是OneWay,这是WPF的默认规则——除非注册DP时显式指定FrameworkPropertyMetadataOptions.BindsTwoWayByDefault。
你可以在注册Value属性时设置默认双向绑定,这样使用时无需每次指定Mode=TwoWay:
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( "Value", typeof(double), typeof(NumberBox), new FrameworkPropertyMetadata( 0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, // 默认双向绑定 ValueChanged));
修改后,MainWindow中的绑定可简化为Value="{Binding SomeBoundValue}",依然会自动双向同步。
内容的提问来源于stack exchange,提问作者Elias Verstappe
相关产品推荐
相关产品推荐

