Jonas Rembratt
Jonas Rembratt

Reputation: 1752

Can't get DependencyProperty to work

I wrote a small attached property called "IsValid" for the WPF TextBox, like so:

public enum InputTypes
{
    Any,

    Integer,

    Double,

    Float
}

/// <summary>
/// This attached property can be used to validate input for <see cref="TextBox"/>. 
/// </summary>
public class IsValid : DependencyObject
{
    public static readonly DependencyProperty InputProperty = DependencyProperty.Register(
        "Input",
        typeof(InputTypes),
        typeof(IsValid),
        new UIPropertyMetadata(InputTypes.Any, onInput));

    public static InputTypes GetInput(DependencyObject d)
    {
        return (InputTypes)d.GetValue(InputProperty);
    }

    public static void SetInput(DependencyObject d, InputTypes value)
    {
        d.SetValue(InputProperty, value);
    }

    private static void onInput(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var textBox = (TextBox)d;
        var value = (InputTypes)e.NewValue;
        switch (value)
        {
            case InputTypes.Any:
                textBox.PreviewTextInput -= validateInput;
                textBox.PreviewKeyDown -= validateKeyDown;
                break;

            default:
                textBox.PreviewTextInput += validateInput;
                textBox.PreviewKeyDown += validateKeyDown;
                break;
        }
    }

    private static void validateInput(object sender, TextCompositionEventArgs e)
    {
        // enforce numeric input when configured ...
        var textBox = (TextBox) sender;
        var inputTypes = (InputTypes) textBox.GetValue(InputProperty);
        foreach (var c in e.Text)
        {
            switch (inputTypes)
            {
                case InputTypes.Integer:
                    if (!char.IsDigit(c))
                    {
                        e.Handled = true;
                        return;
                    }
                    break;

                case InputTypes.Double:
                case InputTypes.Float:
                    if (!char.IsNumber(c))
                    {
                        e.Handled = true;
                        return;
                    }
                    break;

                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
    }

    private static void validateKeyDown(object sender, KeyEventArgs e)
    {
        // block [SPACE] when numeric input is expected ...
        var textBox = (TextBox)sender;
        var inputTypes = (InputTypes)textBox.GetValue(InputProperty);
        if (inputTypes != InputTypes.Any && e.Key == Key.Space)
            e.Handled = true;
    }
}

End here's how I've used it:

<Window x:Class="Spike.Wpf.Controls.TestApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:values="clr-namespace:Spike.Wpf.Controls.Input;assembly=Spike.Wpf.Controls"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <TextBox values:IsValid.Input="Double" />
</Grid>

After the initialization (of the DependencyProperty) none of the methods in IsValid gets called however. What am I missing?

Upvotes: 0

Views: 2412

Answers (2)

Jonas Rembratt
Jonas Rembratt

Reputation: 1752

Ok, so the core of the problem was trivial (see accepted answer): I needed to call DependencyProperty.RegisterAttached(...) (as opposed to DependencyProperty.Register(...).

Just wanted to share the result. I decided to scrap the use of a simple enum to specify input type and decided to use markup extensions instead.

The attached property implementation now looks like this:

public static class IsValid
{
    public static readonly DependencyProperty InputProperty = DependencyProperty.RegisterAttached(
        "Input",
        typeof(IsValidInputExtension),
        typeof(IsValid),
        new UIPropertyMetadata(onInput));

    public static IsValidInputExtension GetInput(DependencyObject d)
    {
        return (IsValidInputExtension)d.GetValue(InputProperty);
    }

    public static void SetInput(DependencyObject d, IsValidInputExtension value)
    {
        d.SetValue(InputProperty, value);
    }

    private static void onInput(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var textBox = (TextBox)d;
        var value = (IsValidInputExtension)e.NewValue;
        if (value == null)
        {
            textBox.PreviewTextInput -= validateInput;
            textBox.PreviewKeyDown -= validateKeyDown;
            return;
        }
        textBox.PreviewTextInput += validateInput;
        textBox.PreviewKeyDown += validateKeyDown;
    }

    private static void validateInput(object sender, TextCompositionEventArgs e)
    {
        // dispatch validation to specified markup class ...
        var textBox = (TextBox) sender;
        var markup = (IsValidInputExtension)textBox.GetValue(InputProperty);
        markup.ValidateInput(sender, e);
    }

    private static void validateKeyDown(object sender, KeyEventArgs e)
    {
        // dispatch validation to specified markup class ...
        var textBox = (TextBox)sender;
        var markup = (IsValidInputExtension)textBox.GetValue(InputProperty);
        markup.ValidateKeyDown(sender, e);
    }
}

And here's part of the markup extension classes:

public abstract class IsValidInputExtension : MarkupExtension
{
    internal abstract void ValidateInput(object sender, TextCompositionEventArgs e);
    internal abstract void ValidateKeyDown(object sender, KeyEventArgs e);
}

public class NumericExtension : IsValidInputExtension
{
    public double Minimum { get; set; }

    public double Maximum { get; set; }

    public uint Decimals { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }

    internal override void ValidateInput(object sender, TextCompositionEventArgs e)
    {
        var textBox = (TextBox) sender;
        if (isDecimalSeparator(e.Text) && Decimals == 0)
        {
            e.Handled = true;
            return;
        }

        // todo: honor Minimum and Maximum ...
    }

    private static bool isDecimalSeparator(string s)
    {
        return CultureInfo.CurrentUICulture.NumberFormat.CurrencyDecimalSeparator == s;
    }

    internal override void ValidateKeyDown(object sender, KeyEventArgs e)
    {
        // block [SPACE] when numeric input is expected ...
        e.Handled = e.Key == Key.Space;
    }
}

public class StringExtension : IsValidInputExtension
{
    public double MaximumLength { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }

    internal override void ValidateInput(object sender, TextCompositionEventArgs e)
    {
        // (nop)
    }

    internal override void ValidateKeyDown(object sender, KeyEventArgs e)
    {
        // todo: honor MaximumLength here
    }
}

The end result, in XAML, is quite nice and easy to read...

<TextBox v:IsValid.Input="{v:Numeric Minimum=0, Maximum=99, Decimals=0}" />

It all seems to work as I hoped. Thanks for all input

Cheers

/Jonas

Upvotes: 0

brunnerh
brunnerh

Reputation: 185300

Earlier you probably got an error telling you that IsValid needs to derive from DependecyObject, so you added that, you should have asked yourself why that is. The answer is right here:

public static readonly DependencyProperty InputProperty = DependencyProperty.Register(...

You try to register a normal property for objects on type IsValid, change it to RegisterAttached and it should work. (I would also remove the inheritance and make IsValid a static class)

Upvotes: 2

Related Questions