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 FlowLayoutPanel
s 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:
- Original DTP
OnGotFocus()
andOnLostFocus()
event handlers are overridden to trigger my new controlEnter
andLeave
events. Note that they are triggered almost correctly. - 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
andLeave
events. We don't want those, so I check the time between events, if it's smaller than 10ms I just ignore later event. DesignerToolStripControlHost
is used to fix broken designer behavior. If you usedToolStripControlHost
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.- 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.