1

I have a custom control called DoubleNumericBox that validates and accepts user input like 23,00, 0,9, 23.900,01, 34... etc.

The problem starts when I try to binding something to it. The binding is not reliable enough, some times the control won't display the new value, but if I set the DataContext one more time it will set the value, etc.

So, I must be doing something very wrong with my custom properties and events.

Custom Properties/Events

  • Value : Double
  • MinValue : Double
  • MaxValue : Double

ValueChanged : Event

Expected Behaviour

  • Validate typed keys: Numbers, Commas and Points (Decimal separator and digit grouping glyph). My culture uses Comma as decimal separator.
  • Validate the whole text if (return to the latest Value if number not valid):

    • Text pasted.
    • Lost Focus.
  • Validate Min/Max limit.
  • Accept binding from Text or Value, and validate the binding value.

Code

public class DoubleNumericBox : TextBox
{

Variables:

    public readonly static DependencyProperty MinValueProperty;
    public readonly static DependencyProperty ValueProperty;
    public readonly static DependencyProperty MaxValueProperty;

Properties:

    public double MinValue
    {
        get { return (double)GetValue(MinValueProperty); }
        set { SetCurrentValue(MinValueProperty, value); }
    }

    public double Value
    {
        get { return (double)GetValue(ValueProperty); }
        set
        {
            SetCurrentValue(ValueProperty, value);
            RaiseValueChangedEvent();
        }
    }

    public double MaxValue
    {
        get { return (double)GetValue(MaxValueProperty); }
        set { SetCurrentValue(MaxValueProperty, value); }
    }

Event:

    public static readonly RoutedEvent ValueChangedEvent;

    public event RoutedEventHandler ValueChanged
    {
        //Provide CLR accessors for the event 
        add { AddHandler(ValueChangedEvent, value); }              
        remove { RemoveHandler(ValueChangedEvent, value); }
    }

    public void RaiseValueChangedEvent()
    {
        var newEventArgs = new RoutedEventArgs(ValueChangedEvent);
        RaiseEvent(newEventArgs);
    }

Constructor/Override:

    static DoubleNumericBox()
    {
        MinValueProperty = DependencyProperty.Register("MinValue", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(0D));
        ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(0D, ValueCallback));
        MaxValueProperty = DependencyProperty.Register("MaxValue", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(Double.MaxValue));

        ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DoubleNumericBox));
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        PreviewTextInput += DoubleNumericBox_PreviewTextInput;
        ValueChanged += DoubleNumericBox_ValueChanged;
        TextChanged += DoubleNumericBox_TextChanged;
        LostFocus += DoubleNumericBox_LostFocus;

        AddHandler(DataObject.PastingEvent, new DataObjectPastingEventHandler(PastingEvent));
    }

Events:

    private static void ValueCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var textBox = d as DoubleNumericBox;
        if (textBox == null) return;

        //textBox.Text = String.Format("{0:###,###,##0.0###}", textBox.Value);

        textBox.RaiseValueChangedEvent();
    }

    private void DoubleNumericBox_ValueChanged(object sender, RoutedEventArgs e)
    {
        var textBox = sender as DoubleNumericBox;

        if (textBox == null) return;

        ValueChanged -= DoubleNumericBox_ValueChanged;
        TextChanged -= DoubleNumericBox_TextChanged;

        if (Value > MaxValue)
            Value = MaxValue;

        else if (Value < MinValue)
            Value = MinValue;

        textBox.Text = Text = String.Format("{0:###,###,##0.0###}", Value);

        ValueChanged += DoubleNumericBox_ValueChanged;
        TextChanged += DoubleNumericBox_TextChanged;
    }

    private void DoubleNumericBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        var textBox = sender as TextBox;

        if (textBox == null) return;
        if (String.IsNullOrEmpty(textBox.Text)) return;
        if (IsTextDisallowed(textBox.Text)) return;

        ValueChanged -= DoubleNumericBox_ValueChanged;

        var newValue = Convert.ToDouble(textBox.Text);

        if (newValue > MaxValue)
            Value = MaxValue;
        else if (newValue < MinValue)
            Value = MinValue;
        else
        {
            Value = newValue;
        }

        ValueChanged += DoubleNumericBox_ValueChanged;
    }

    private void DoubleNumericBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        if (String.IsNullOrEmpty(e.Text))
        {
            e.Handled = true;
            return;
        }

        //Only Numbers, comma and points.
        if (IsEntryDisallowed(sender, e.Text))
        {
            e.Handled = true;
        }
    }

    private void PastingEvent(object sender, DataObjectPastingEventArgs e)
    {
        if (e.DataObject.GetDataPresent(typeof(String)))
        {
            var text = (String)e.DataObject.GetData(typeof(String));

            if (IsTextDisallowed(text))
            {
                e.CancelCommand();
            }
        }
        else
        {
            e.CancelCommand();
        }
    }

    private void DoubleNumericBox_LostFocus(object sender, RoutedEventArgs e)
    {
        TextChanged -= DoubleNumericBox_TextChanged;

        Text = String.Format("{0:###,###,##0.0###}", Value);

        TextChanged += DoubleNumericBox_TextChanged;
    }

Methods:

    private bool IsEntryDisallowed(object sender, string text)
    {
        var regex = new Regex(@"^[0-9]|\.|\,$");

        if (regex.IsMatch(text))
        {
            return !CheckPontuation(sender, text);
        }

        //Not a number or a Comma/Point.
        return true;
    }

    private bool IsTextDisallowed(string text)
    {
        var regex = new Regex(@"^((\d+)|(\d{1,3}(\.\d{3})+)|(\d{1,3}(\.\d{3})(\,\d{3})+))((\,\d{4})|(\,\d{3})|(\,\d{2})|(\,\d{1})|(\,))?$");
        return !regex.IsMatch(text); //\d+(?:,\d{1,2})?
    }

    private bool CheckPontuation(object sender, string next)
    {
        var textBox = sender as TextBox;

        if (textBox == null) return true;

        if (Char.IsNumber(next.ToCharArray()[0]))
            return true;

        if (next.Equals("."))
        {
            var textAux = textBox.Text;

            if (!String.IsNullOrEmpty(textBox.SelectedText))
                textAux = textAux.Replace(textBox.SelectedText, "");

            //Check if the user can add a point mark here.
            var before = textAux.Substring(0, textBox.SelectionStart);
            var after = textAux.Substring(textBox.SelectionStart);

            //If no text, return true.
            if (String.IsNullOrEmpty(before) && String.IsNullOrEmpty(after)) return true;

            if (!String.IsNullOrEmpty(before))
            {
                if (before.Contains(',')) return false;

                if (after.Contains("."))
                {
                    var split = before.Split('.');

                    if (split.Last().Length != 3) return false;
                }
            }

            if (!String.IsNullOrEmpty(after))
            {
                var split = after.Split('.', ',');

                if (split.First().Length != 3) return false;
            }

            return true;
        }

        //Only one comma.
        if (next.Equals(","))
        {
            return !textBox.Text.Any(x => x.Equals(','));
        }

        return true;
    }  
}

Can you guys help me out to make this custom control work better?

Henka Programmer
  • 680
  • 6
  • 23
Nicke Manarin
  • 2,294
  • 1
  • 24
  • 59
  • 1
    [here is a library that does similar things](https://github.com/JohanLarsson/Gu.Wpf.NumericInput), grab what you need or use the nuget. – Johan Larsson Mar 04 '16 at 21:11

1 Answers1

1

So a couple of gotchas I see in your code: Do not use += / -= to hook up events in WPF controls, it can and will break routed events, use Addhandler / RemoveHandler instead. I removed the unhooking and rehooking of events and used a member level flag instead for change loop issues. Here is the code I came up with, seem to bind fine to Value field.

A side note, you failed to account for multiple "." entry in your textbox so a user could type 345.34.434.23 which would not be prevented. I know to check this because I wrote a WPF FilterTextBox years ago and this came up in my testing.

public class DoubleNumericBox : TextBox
{
    public readonly static DependencyProperty MinValueProperty;
    public readonly static DependencyProperty ValueProperty;
    public readonly static DependencyProperty MaxValueProperty;
    public bool _bIgnoreChange = false;

public double MinValue
{
    get { return (double)GetValue(MinValueProperty); }
    set { SetCurrentValue(MinValueProperty, value); }
}

public double Value
{
    get { return (double)GetValue(ValueProperty); }
    set
    {
        SetCurrentValue(ValueProperty, value);
        RaiseValueChangedEvent();
    }
}

public double MaxValue
{
    get { return (double)GetValue(MaxValueProperty); }
    set { SetCurrentValue(MaxValueProperty, value); }
}

public static readonly RoutedEvent ValueChangedEvent;

public event RoutedEventHandler ValueChanged
{
    //Provide CLR accessors for the event 
    add { AddHandler(ValueChangedEvent, value); }
    remove { RemoveHandler(ValueChangedEvent, value); }
}

public void RaiseValueChangedEvent()
{
    var newEventArgs = new RoutedEventArgs(ValueChangedEvent);
    RaiseEvent(newEventArgs);
}

static DoubleNumericBox()
{
    MinValueProperty = DependencyProperty.Register("MinValue", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(0D));
    ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(0D, ValueCallback));
    MaxValueProperty = DependencyProperty.Register("MaxValue", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(Double.MaxValue));

    ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DoubleNumericBox));
}

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    AddHandler(TextBox.PreviewTextInputEvent, new TextCompositionEventHandler(DoubleNumericBox_PreviewTextInput));
    AddHandler(TextBox.TextChangedEvent, new  TextChangedEventHandler(DoubleNumericBox_TextChanged));
    AddHandler(TextBox.LostFocusEvent, new RoutedEventHandler(DoubleNumericBox_LostFocus));
    AddHandler(DataObject.PastingEvent, new DataObjectPastingEventHandler(PastingEvent));
}


private static void ValueCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var textBox = d as DoubleNumericBox;
    if (textBox == null) return;

    //textBox.Text = String.Format("{0:###,###,##0.0###}", textBox.Value);

    textBox.DoubleNumericBox_ValueChanged();
}

private void DoubleNumericBox_ValueChanged()
{

    if (Value > MaxValue)
        Value = MaxValue;

    else if (Value < MinValue)
        Value = MinValue;

    if (!_bIgnoreChange)
        this.Text = Text = String.Format("{0:###,###,##0.0###}", Value);
}

private void DoubleNumericBox_TextChanged(object sender, TextChangedEventArgs e)
{
    var textBox = sender as TextBox;

    if (textBox == null) return;
    if (String.IsNullOrEmpty(textBox.Text)) return;
    if (IsTextDisallowed(textBox.Text)) return;

    //ValueChanged -= DoubleNumericBox_ValueChanged;
    _bIgnoreChange = true;

    Value = Convert.ToDouble(textBox.Text);

    //if (newValue > MaxValue)
    //    Value = MaxValue;
    //else if (newValue < MinValue)
    //    Value = MinValue;
    //else
    //{
    //    Value = newValue;
    //}
    _bIgnoreChange = false;

    //ValueChanged += DoubleNumericBox_ValueChanged;
}

private void DoubleNumericBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
    if (String.IsNullOrEmpty(e.Text))
    {
        e.Handled = true;
        return;
    }

    //Only Numbers, comma and points.
    if (IsEntryDisallowed(sender, e.Text))
    {
        e.Handled = true;
    }
}

private void PastingEvent(object sender, DataObjectPastingEventArgs e)
{
    if (e.DataObject.GetDataPresent(typeof(String)))
    {
        var text = (String)e.DataObject.GetData(typeof(String));

        if (IsTextDisallowed(text))
        {
            e.CancelCommand();
        }
    }
    else
    {
        e.CancelCommand();
    }
}

private void DoubleNumericBox_LostFocus(object sender, RoutedEventArgs e)
{
    //TextChanged -= DoubleNumericBox_TextChanged;

    Text = String.Format("{0:###,###,##0.0###}", Value);

    //TextChanged += DoubleNumericBox_TextChanged;
}




private bool IsEntryDisallowed(object sender, string text)
{
    var regex = new Regex(@"^[0-9]|\.|\,$");

    if (regex.IsMatch(text))
    {
        return !CheckPontuation(sender, text);
    }

    //Not a number or a Comma/Point.
    return true;
}

private bool IsTextDisallowed(string text)
{
    var regex = new Regex(@"^((\d+)|(\d{1,3}(\.\d{3})+)|(\d{1,3}(\.\d{3})(\,\d{3})+))((\,\d{4})|(\,\d{3})|(\,\d{2})|(\,\d{1})|(\,))?$");
    return !regex.IsMatch(text); //\d+(?:,\d{1,2})?
}

private bool CheckPontuation(object sender, string next)
{
    var textBox = sender as TextBox;

    if (textBox == null) return true;

    if (Char.IsNumber(next.ToCharArray()[0]))
        return true;

    if (next.Equals("."))
    {
        var textAux = textBox.Text;

        if (!String.IsNullOrEmpty(textBox.SelectedText))
            textAux = textAux.Replace(textBox.SelectedText, "");

        //Check if the user can add a point mark here.
        var before = textAux.Substring(0, textBox.SelectionStart);
        var after = textAux.Substring(textBox.SelectionStart);

        //If no text, return true.
        if (String.IsNullOrEmpty(before) && String.IsNullOrEmpty(after)) return true;

        if (!String.IsNullOrEmpty(before))
        {
            if (before.Contains(',')) return false;

            if (after.Contains("."))
            {
                var split = before.Split('.');

                if (split.Last().Length != 3) return false;
            }
        }

        if (!String.IsNullOrEmpty(after))
        {
            var split = after.Split('.', ',');

            if (split.First().Length != 3) return false;
        }

        return true;
    }

    //Only one comma.
    if (next.Equals(","))
    {
        return !textBox.Text.Any(x => x.Equals(','));
    }

    return true;
}

}

Kelly Barnard
  • 1,101
  • 6
  • 8