2008-07-19

Validating NumericUpDown on compact framework

A customer requested that instead of my NumericUpDown controls silently capping the input value within the Minimum..Maximum range it instead showed an error message telling the user their input is incorrect and that they need to alter it.

I was a bit annoyed to see that NumericUpDown.Validating is never called on the compact framework, in addition there was no way to get the input value and either accept or reject it before it is applied to its data bindings.

There's an article here which shows how to implement auto-select text when the NumericUpDown receives focus and I have been using it since Feb 2006. I decided to extend upon the techniques within it to implement the Validating event. My goal was to fire the Validating event before the value is applied to all data-bindings, but also to allow the programmer to read NumericUpDown.Value in order to determine the new value. To do this I had to replace the WndProc of the control so that I could handle the
WM_UPDOWN_NOTIFYVALUECHANGED message, parse the value, validate it, and then either accept it (call the original WndProc) or restore the value to the current value.

Rather than teach how this is done I thought I would just include the source code here. One point to note though is that I had to have a "bool IsInternalCall" wrapped around my handler otherwise I would have re-entrant problems and experience a stack overflow. Here is the source, it includes the auto-select code by Mark Arteaga.

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Threading;
using System.ComponentModel;

namespace Mycompany.Windows.Forms
{
  public class NumericUpDownWithSelect : NumericUpDown, ISupportInitialize
  {
    #region API
    private const int GWL_WNDPROC = -4;
    private const int WM_UPDOWN_NOTIFYVALUECHANGED = 13;
    public const int WM_GETTEXTLENGTH = 0x000E;
    public const int WM_GETTEXT = 0x000D;
    private const int WM_GETSELECTION = 0x00B0;
    private const int WM_SETSELECTION = 0x00B1;

    private WndProcHandler NewWndProc = null;
    private IntPtr OldWndProc = IntPtr.Zero;

    public delegate IntPtr WndProcHandler(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
    [DllImport("coredll.dll", SetLastError = true)]
    private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);
    [DllImport("coredll.dll", SetLastError = true)]
    private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, StringBuilder buffer);
    [DllImport("coredll.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, WndProcHandler wndproc);
    [DllImport("coredll.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
    [DllImport("coredll.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
    [DllImport("coredll.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr CallWindowProc(IntPtr wndProc, IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
    #endregion

    private bool ControlDisposed = false;
    private bool IsValidating = false;
    private decimal ValueToValidate;

    public NumericUpDownWithSelect()
    {
    }

    public new event CancelEventHandler Validating;
    protected virtual void OnValidating(out bool cancel, decimal newValue)
    {
      cancel = false;
      CancelEventHandler validating = Validating;
      if (validating == null)
        return;

      cancel = false;
      CancelEventArgs args = new CancelEventArgs(false);
      IsValidating = true;
      try
      {
        ValueToValidate = newValue;
        Validating(this, args);
      }
      finally
      {
        IsValidating = false;
      }
      cancel = args.Cancel;
    }

    private decimal currentValue = 0;
    public new decimal Value
    {
      get
      {
        if (IsValidating)
          return ValueToValidate;
        return base.Value;
      }
      set
      {
        bool cancel;
        OnValidating(out cancel, value);
        if (!cancel)
        {
          base.Value = value;
          currentValue = value;
        }
      }
    }

    #region Validation
    protected override void OnHandleCreated(EventArgs e)
    {
      base.OnHandleCreated(e);
      if (this.Site == null)
      {
        NewWndProc = new WndProcHandler(ReplacementWndProcImpl);
        OldWndProc = SetWindowLong(this.Handle, GWL_WNDPROC, NewWndProc);
      }
    }

    private static bool IsInternalCall = false;
    private IntPtr ReplacementWndProcImpl(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam)
    {
      bool cancelled = false;
      if (msg == WM_UPDOWN_NOTIFYVALUECHANGED && !IsInternalCall)
      {
        IsInternalCall = true;
        try
        {
          int length = CallWindowProc(OldWndProc, this.Handle, WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero).ToInt32();
          StringBuilder buffer = new StringBuilder(length + 1);
          SendMessage(this.Handle, WM_GETTEXT, length + 1, buffer);
          try
          {
            decimal newValue = decimal.Parse(buffer.ToString());
            OnValidating(out cancelled, newValue);
            if (cancelled)
            {
              Value = currentValue;
            }
          }
          catch (FormatException)
          {
            cancelled = true;
          }
        }
        finally
        {
          IsInternalCall = false;
          if (cancelled)
          {
            Focus();
            SelectAll();
          }
        }
      }
      return CallWindowProc(OldWndProc, hWnd, msg, wParam, lParam);
    }
    #endregion

    #region AutoSelect

    private delegate void SelectAllInvoke();

    private bool suppressOnGotFocus = false;

    protected override void OnGotFocus(EventArgs e)
    {
      base.OnGotFocus(e);
      if (!this.suppressOnGotFocus)
        SelectAll();
    }

    public void SelectAll()
    {
      this.SelectInternal(0, this.Value.ToString().Length);
    }

    public void Select(int start, int length)
    {
      this.SelectInternal(start, length);
    }

    private void SelectInternal(int start, int length)
    {
      if (!ControlDisposed)
      {
        this.suppressOnGotFocus = true;
        if (!this.Focused)
          this.Focus();
        IntPtr ret = SendMessage(this.Handle, WM_SETSELECTION, start, length);
        this.suppressOnGotFocus = false;
      }
    }
    #endregion

    protected override void Dispose(bool disposing)
    {
      ControlDisposed = true;
      base.Dispose(disposing);
    }

    #region ISupportInitialize Members
    //This region is here simply because the WinForm designer insists on casting this control
    //to ISupportInitialize
    void ISupportInitialize.BeginInit()
    {
    }

    void ISupportInitialize.EndInit()
    {
    }

    #endregion
  }

}


And how might you use it?

private void numericUpDownWithSelect1_Validating_1(object sender, CancelEventArgs e)
{
  if (numericUpDownWithSelect1.Value < numericUpDownWithSelect1.Minimum
    || numericUpDownWithSelect1.Value > numericUpDownWithSelect1.Maximum)
  {
    //No need to cancel, the new value will be rejected
    MessageBox.Show("Warning, value is about to be capped");
  }
  if (numericUpDownWithSelect1.Value > 5)
  {
    MessageBoxIcon icon = new MessageBoxIcon();
    e.Cancel =
      MessageBox.Show(
    "Is it really greater than 5?",
        "Are you sure?",
        MessageBoxButtons.YesNo,
        icon,
        MessageBoxDefaultButton.Button1) != DialogResult.Yes;
  }
}

No comments: