0

I am having trouble binding to a dependency property on a custom control.

This is my custom control. It's basically a textbox that only allows numeric input and exposes a "Value" dependency property you should be able to bind to to get and set the value.

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;
        }
    }
}

Then I also make a little viewmodel just to simplify my use case. NumberBoxViewModel.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

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;      // For the sake of setting a breakpoint here
                }
            }
        }
    }
}

Then I use it like this in my main window: 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;     // Does not work either
        }
    }
}

When I Type something into the textbox, my breakpoits hit in NumberBox.InputValidation & NumberBox.ValueChanged . The property I bind to "Value" never triggers a value change though (see the set property of NumberBoxViewModel.SomeBoundValue).

Is there something stupid that I'm missing? What is going on here. Can someone explain how bindings between properties and dependency properties work? Do user defined dependency properties have different behaviour than built in properties such as the Text field on a TextBlock?

5
  • The assignment box.Value = parsed; in InputValidation destroys the binding initially made in XAML (Value="{Binding SomeBoundValue}") Also the code that changes SomeBoundValue and then should trigger the Value-change is at least not shown (so I wonder where you expect Value to change)? Commented Dec 1, 2023 at 11:22
  • box.Value = parsed; would be ok with a TwoWay Binding of the Value property. The Binding should default to TwoWay, which you would achieve by appropriate FrameworkPropertyMetadata. Just as the TextBox.Text property binds TwoWay by default. Commented Dec 1, 2023 at 11:32
  • @lidqy : in my system I update SomeBoundValue depending on some other logic and it does get displayed on the screen. Also, how could I go about exposing a bindable property that holds the input data parsed from string to a doudle? How do I do that without breaking the binding? Commented Dec 1, 2023 at 11:57
  • @Clemens: If the binding defaults to TwoWay, what is it you are trying to say? Commented Dec 1, 2023 at 11:58
  • Your Value property does not bind TwoWay by default. You have to set an appropriate FrameworkPropertyMetadataOptions flag. Commented Dec 1, 2023 at 12:00

1 Answer 1

0

Thank you all for your answers. The problem is fixed by adjusting the dependency property definition to use two-way bindings.

private string ValueAsString
{
    get => (string)GetValue(ValueAsStringProperty);
    set => SetValue(ValueAsStringProperty, value);
}

private static readonly DependencyProperty ValueAsStringProperty =
    DependencyProperty.Register(nameof(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(nameof(Value),
                                typeof(double),
                                typeof(NumberBox),
                                new FrameworkPropertyMetadata(0.0,
                                                              FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                              ValueChanged));
Sign up to request clarification or add additional context in comments.

4 Comments

ValueAsString does not need that change. There is not even a Binding that targets that property. It is apparently only used internally by the control as source property of the TextBox.Text Binding. It should perhaps not even be a public property at all.
How will the NumberBox.xaml.cs bind to ValueAsString
It could be a private property, and since it is not the target of a Binding you do not need to set BindsTwoWayByDefault.
Thanks - code has been updated

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.