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?
box.Value = parsed;inInputValidationdestroys the binding initially made in XAML (Value="{Binding SomeBoundValue}") Also the code that changesSomeBoundValueand then should trigger the Value-change is at least not shown (so I wonder where you expect Value to change)?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.