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;
  }
}

2008-07-08

Single instance application

An app I am working on needs to be a single instance. It is associated with certain file extensions so that when I select a character or license file it will be imported automatically. When the user buys a character or license (etc) from the website it will be downloaded and opened, and then imported.

Obviously it is a pretty poor user experience if they have to close the app, download, close the app, download... So what I really needed was a way to have the 2nd instance of the application to invoke the first instance and pass the command line parameters. Here is a simple solution I implemented using remoting.

01: An interface

public interface ISingleInstance
{
void Execute(string[] args);
}


02: A class that implements the interface

public class SingleInstance : MarshalByRefObject, ISingleInstance
{
  private static object SyncRoot = new object();
  private static Form1 MainForm;

  static SingleInstance()
  {
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
  }

  public void Execute(string[] args)
  {
    bool isNew = false;
    lock (SyncRoot)
    {
      isNew = (MainForm == null);
      if (isNew)
        MainForm = new Form1();
      MainForm.AcceptCommandLineArguments(args);
    }
    if (isNew)
      Application.Run(MainForm);
  }
}


03: And finally the remoting code in the Program.cs file itself:

static class Program
{
  private static IpcChannel IpcChannel;

  [STAThread]
  static void Main(string[] args)
  {
    bool isNew;
    using (Mutex mutex = new Mutex(true, "TheCatSatOnTheMat", out isNew))
    {
      if (isNew)
        RegisterServer();
      else
      {
        IpcChannel = new IpcChannel("Client");
        ChannelServices.RegisterChannel(IpcChannel, false);
      }
      ISingleInstance app = (ISingleInstance)Activator.GetObject(typeof(ISingleInstance), "ipc://Server/RemotingServer");
      app.Execute(args);
    }
  }

  private static void RegisterServer()
  {
    IpcChannel = new IpcChannel("Server");
    ChannelServices.RegisterChannel(IpcChannel, false);
    RemotingConfiguration.RegisterWellKnownServiceType(typeof(SingleInstance), "RemotingServer", WellKnownObjectMode.Singleton);
  }
}


This is more of a note to myself in case I lose my small test app before I come around to implementing it into my main application :-)

More leak fixes

I have changed the DirtyObjectCatcher so that it initially only hooks

Form.Disposed - Automatically disposes the DirtyObjectCatcher
Form.Closed - Unhooks all additional form events (below)
Form.Shown - To hook additional form events (below)

==Additional form events==
Form.Activated
Form.MdiParent.Activated
Form.MdiChildActivate

The additional events are to ensure that the DirtyObjectCatcher's undo block is moved to the top. The reason that these events are now unhooked is so that there is no strong event references from the application's main form (Form.MdiParent) to this component, keeping it alive. Now we only have long-term event references from the owning form itself. Really though this is just an added precaution against something I may not have thought of :-)

The true memory saver comes from only holding a WeakReference to the owning form. Otherwise in an MDI application we have the following

MainForm.MdiChildActivate->DirtyObjectCatcher->Form

In such a case closing the MDI child form will not be collected it because it is referenced by the DirtyObjectCatcher, which cannot be collected because it is referenced by the application's main form. Unhooking these events and holding only a WeakReference prevents the leakage.

In addition I have hooked Form.Disposed so that this component is disposed along with its owner. I have also hooked DirtyObjectCatcher.Disposed from ObjectValidator so that it may also auto-dispose itself and knows not to place any new subscriptions.

Again the files are available here.

In addition I was able to track down and reproduce a couple of leaks in ECO 4 which are now fixed internally.

  1. Deactivating / Reactivating an EcoSpace would leak an object reference each time. This is not the case if you allow the EcoSpace to be collected.
  2. Using OclVariables with OclPsHandle and calling EcoSpace.Persistence.Refresh would leak a single object reference, also if OclPsHandle.Execute was executed.

I monitored my app with over 4,000 tasks today (normally there are about 200) for a few hours and the memory usage didn't budge!