Reputation: 52
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?
Upvotes: 0
Views: 61
Reputation: 52
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));
Upvotes: 0