Question

To reproduce this bug you need to create custom ToolStripItem, using ToolStripControlHost. In my case, I made ToolStripDateTimePicker control (as seen on many good tutorials). The control however behaves slightly different than a regular DateTimePicker.

Regular one produces default Windows bell sound when ESC is pressed while it's active. The ToolStrip hosted control reacts for ESC pressed in a more sensible manner. The control becomes inactive, no beep.

Here's the bug part: when focusing another control with a click - regular DateTimePicker triggers Leave event. As expected. ToolStrip hosted control DOES NOT trigger any event!

Yes, I've tried KeyDown event - it's not sent when ESC key is pressed, but sent when any other key is pressed.

I believe it's a bug in .NET itself.

The consequence of this is broken focus behavior of the form containing the ToolStrip control. The form cannot be focused again after entering ToolStrip hosted control.

But it's a workaround for this: you can focus an other form (or even maybe another control), and then the target form - it works for me.

However I would like to have it done automatically - the moment user exits the hosted control. The problem is I don't have an event for this. Any ideas?

What's weird, Leave event is eventually triggered when the hosted control is disposed - it's clearly a bug, because the event is totally useless there.

Here: sample application illustrating the problem I've replaced it with sample workaround, to see the problem comment out OnGotFocus() and OnLostFocus() overrides.

It worked nice (and could not reproduce the bug) until I've changed FlowLayoutPanel TabIndex to 0, so the DateTimePicker is not active when the application starts.

Was it helpful?

Solution

Here's what I learned:

The problem was not exactly with ToolStripControlHost class, but with the DateTimePicker control itself, and to be more specific - with it's interaction with FlowLayoutPanels and possibly other similar controls. And I'm not sure if it's a bug or expected behavior, but seems more like a bug.

Here's how it works: If there is another control which can be obviously activated (like TextBox), leaving the DatePickerControl means activating the other control. But if the other control is empty container, or container with no controls which can be activated - Leave event is not triggered despite the DateTimePicker control is not active anymore.

Why would you want to have a container with no active controls active itself? I use FlowLayoutPanel to produce a read-only, non-editable report. It is not editable, but I want it scrollable, and I don't want DateTimePicker control to steal focus from FlowLayoutPanel - so in this case FlowLayoutPanel is an active control.

Of course it doesn't work this way. There are more workarounds needed for such behavior to be achieved, like receiving mouse events from containing form, but correct Focus/Leave behavior for ToolStripDatePickerControl is a good start.

So without further ado, my perfect ToolsStripDateTimePicker control, with focus glitch fixed:

DesignerToolStripControlHost class:

namespace System.Windows.Forms {
    /// <summary>
    /// Fixes ToolStripControlHost broken designer behavior
    /// </summary>
    public class DesignerToolStripControlHost : ToolStripControlHost {
        /// <summary>
        /// Fixes designer bug by creating a constructor allowing to create ToolStripControlHost
        /// without parameter
        /// </summary>
        public DesignerToolStripControlHost() : base(new UserControl()) { }
        /// <summary>
        /// Initializes a new instance of the System.Windows.Forms.DesignerToolStripControlHost
        /// class that hosts the specified control
        /// </summary>
        /// <param name="c"></param>
        public DesignerToolStripControlHost(Control c) : base(c) { }
        /// <summary>
        /// Initializes a new instance of the System.Windows.Forms.DesignerToolStripControlHost
        /// class that hosts the specified control and that has the specified name
        /// </summary>
        /// <param name="c"></param>
        /// <param name="name"></param>
        public DesignerToolStripControlHost(Control c, string name) : base(c, name) { }
    }
}

ToolStripDateTimePicker class:

using System.ComponentModel;
using System.Windows.Forms;
using System.Windows.Forms.Design;

namespace System.Windows.Controls {

    [ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.All)]
    public partial class ToolStripDateTimePicker : DesignerToolStripControlHost, IComponent {

        public ToolStripDateTimePicker() : base(new DateTimePicker() { Margin = new Padding(0, 0, 0, 0), Width = 150, Value = DateTime.Now.Date }) { }

        #region Properties

        [Browsable(true)]
        [Category("Design")]
        [Description("Internal ToolStrip hosted control.")]
        public DateTimePicker DateTimePickerControl { get { return Control as DateTimePicker; } }

        [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)]
        [Category("Behavior")]
        [Description("Gets or sets the tab order of the control within its container.")]
        public int TabIndex { get { return DateTimePickerControl.TabIndex; } set { DateTimePickerControl.TabIndex = value; } }

        [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)]
        [Category("Behavior")]
        [Description("Gets or sets a value indicating whether the user can give the focus to this control using the TAB key.")]
        public bool TabStop { get { return DateTimePickerControl.TabStop; } set { DateTimePickerControl.TabStop = value; } }

        [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
        [Description("This property is ignored.")]
        public override string Text { get { return DateTimePickerControl.Value.ToString(); } set { DateTimePickerControl.Value = DateTime.Parse(value); } }

        [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)]
        [Category("Behavior")]
        [Description("The current date/time value for this control.")]
        public DateTime Value { get { return DateTimePickerControl.Value; } set { DateTimePickerControl.Value = value; } }

        #endregion

        #region Events

        [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)]
        [Category("Focus")]
        [Description("Occurs when the input focus enters the control.")]
        public new EventHandler Enter;

        [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)]
        [Category("Focus")]
        [Description("Occurs when the input focus leaves the control.")]
        public new EventHandler Leave;

        [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)]
        [Category("Behavior")]
        [Description("Occurs when the value of the control changes.")]
        public event EventHandler ValueChanged;

        #endregion

        #region Event Handlers

        protected void OnEnter(object sender, EventArgs e) { EventHandler handler = Enter; if (handler != null) handler(this, e); }

        protected void OnLeave(object sender, EventArgs e) { EventHandler handler = Leave; if (handler != null) handler(this, e); }

        protected override void OnGotFocus(EventArgs e) {
            base.OnGotFocus(e);
            if (Enter != null) {
                if (DateTime.Now.Ticks - _FocusGlitchFix_LastEvent > _FocusGlitchFixTickWindow) Enter.Invoke(this, e);
                _FocusGlitchFix_LastEvent = DateTime.Now.Ticks;
            }
        }

        protected override void OnLostFocus(EventArgs e) {
            base.OnLostFocus(e);
            if (Leave != null) {
                if (DateTime.Now.Ticks - _FocusGlitchFix_LastEvent > _FocusGlitchFixTickWindow) Leave.Invoke(this, e);
                _FocusGlitchFix_LastEvent = DateTime.Now.Ticks;
            }
        }

        protected void OnValueChanged(object sender, EventArgs e) { EventHandler handler = ValueChanged; if (handler != null) handler(this, e); }

        protected override void OnSubscribeControlEvents(Control control) {
            base.OnSubscribeControlEvents(control);
            DateTimePickerControl.ValueChanged += new EventHandler(OnValueChanged);
        }

        protected override void OnUnsubscribeControlEvents(Control control) {
            base.OnUnsubscribeControlEvents(control);
            DateTimePickerControl.ValueChanged -= new EventHandler(OnValueChanged);
        }

        #endregion

        #region Focus Glitch Workaround data

        private long _FocusGlitchFix_LastEvent = 0;
        private readonly long _FocusGlitchFixTickWindow = 100000; // 10ms

        #endregion

    }
}

Workarounds explained:

  1. Original DTP OnGotFocus() and OnLostFocus() event handlers are overridden to trigger my new control Enter and Leave events. Note that they are triggered almost correctly.
  2. When the control is the only one which can be activated, when the control is first left it's activated and deactivated instantly - which means doubled (redundant) Enter and Leave events. We don't want those, so I check the time between events, if it's smaller than 10ms I just ignore later event.
  3. DesignerToolStripControlHost is used to fix broken designer behavior. If you used ToolStripControlHost directly, you would get an exception trying to show the control's designer view, because designer tries to instantiate this class with no argument, and this class does not have a constructor with no argument. So my new class does.
  4. When designer view of a form containing ToolStripDateTimePicker break which you can tell by seeing DTP disappeared, just close designer view and open it again. It will work fine until you compile or debug your application again.

Glitch fix was tested with 1ms time window and worked fine. So I chose 10ms to ensure it works on slower or more loaded machines, but it's still short enough to capture any event from user interaction.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top