You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 08:24:54