Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Rethinking the Help Builder HelpProvider: An excercise in Component, Parent and Windows Forms Design time


:P
On this page:
Windows Forms has a HelpProvider class that is used to provide context sensitive Help. You can basically specify a help namespace - usually a CHM file or a Web site on the provider and specific keywords that expose the actual context sensitive topic in the help file. The Help Provider traps the F1 key and brings up your help.

For Help Builder operation, I wanted to extend the HelpProvider to provide an additional hotkey (Alt-F1 by default) to bring up Help Builder in context. Unfortunately the Windows HelpProvider class is pretty lame implementation that doesn't have hooks of any sort to extend its functionality, so all of this work ended up being manual labor and as you'll see in a minute, somewhat non-trivial.

The first issue deals with how to capture the custom key combination. I decided to add a couple of static properties to the control that determine this custom key combination:

public static Keys HotkeyModifier { get { return _HotkeyModifier; } set { _HotkeyModifier = value; } } static Keys _HotkeyModifier = Keys.Alt; public static Keys HotkeyKey { get { return _HotkeyKey; } set { _HotkeyKey = value; } } static Keys _HotkeyKey = Keys.F1;

These two properties allow customization of the hotkey via standard Windows.Forms.Keys. To capture these keys I need to capture the form's key events by hooking the KeyDown event of the form. To do this of course I first need a reference to the form, which has been a major headache. To start out I have a method called HookFormEvents() which receives a form reference as a parameter:

protected void HookFormKeyEvents(Form parent) { this._ParentForm = parent; if (HelpBuilderHelpProvider.EnableHelpBuilderHelpProvider) { // *** Hook up the form keyboard event hooks // *** and route into internal method if (this._ParentForm != null) { this._ParentForm.KeyPreview = true; this._ParentForm.KeyDown += new KeyEventHandler(this.Form_KeyDown); } } }

It receives a reference to the form and then proceeds to hook the KeyDown event. Note that you also need to enable KeyPreview on the form to be able to fire these KeyDown events first. The handler for this event points back to the current class and looks like this:

private void Form_KeyDown(object sender, KeyEventArgs e) { if (e.Modifiers == _HotkeyModifier) { this.AltPressedAt = DateTime.Now; } if (e.KeyCode == HotkeyKey) { TimeSpan ts = DateTime.Now.Subtract( this.AltPressedAt ); if (ts.TotalMilliseconds < 3000) { if (this.AlternateHelpHotKeyPressed != null) this.AlternateHelpHotKeyPressed(this,EventArgs.Empty); else // *** Default: just bring up Help Builder this.ShowHelpBuilder(); e.Handled = true; return; } } e.Handled = false; }

KeyDown works by reporting any key down operation individually, so if you press any keyboard 'chords' each of the keys actually fires individually. To trap for the key combinations I set a timed flag for the hotkey first (Alt for example), then check for the key (F1 for example). The code then goes off to display Help Builder by default, or if the AlternateHelpHotKeyPressed event is set it fires that event handler instead.

So far so good - all of this works great! But there's a catch: HookFormEvents() requires manual intervention. You have to manually write this line of code to cause the form reference to be passed down to HelpProvider, which is workable but kind of lame.

What I want to have happen is that when the custom Help Provider is dropped on the form, I want it to automatically attach itself to the form when it loads.

Finding the Control's Container


It turns out that this is not a trivial task at all. The problem is that HelpProvider inherits from Component and Component does not have any initialization events that fire when a container adds the component to itself. Unlike Control, which has a ControlAdded Event and other initialization events, Component has none.

As it turns out I was unable to get a reference to the running form at runtime. And I tried a variety of different mechanisms:

Strike 1: Using HelpProvider methods as override hooks:

public override void SetHelpKeyword(Control ctl, string keyword) { base.SetHelpKeyword (ctl, keyword); while (ctl != null) { ctl = ctl.Parent; if (ctl == null) break; if ( ctl.Parent is Form) { this.ParentForm = ctl.Parent as Form; break; } ctl = ctl.Parent; } }

The problem here is that although you can get a reference to the Control here, the Control has not been added to the Form's Controls collection yet, and thus doesn't have a Parent at this point.

Strike 2: Try hooking up Events to the form.

public override void SetHelpKeyword(Control ctl, string keyword) { base.SetHelpKeyword (ctl, keyword); if (!this.HookFormInitialization) { //ctl.Paint += new PaintEventHandler(ctl_Paint); ctl.ControlAdded +=new ControlEventHandler(ctl_ControlAdded); this.HookFormInitialization = true; } } public void ctl_ControlAdded(object sender, ControlEventArgs e) { MessageBox.Show("Ctrl Added"); Control frm = sender as Control; frm.ControlAdded -= new ControlEventHandler(ctl_ControlAdded); Control ctl = frm; while (ctl != null) { ctl = ctl.Parent; if (ctl == null) break; if ( ctl.Parent is Form) { this.ParentForm = ctl.Parent as Form; break; } ctl = ctl.Parent; } }

This seemed like a good idea - I would grab the first control that calls into SetHelpKeyWord() and register an event to fire when the control gets added to the Controls collection of the parent. At that time we would be guaranteed that the form reference is available.

I'm not sure why this failed, actually. The event never fired. In fact I couldn't get any event to fire back to me - even a known good event like Paint() that must fire to display the control did not respect this hook. I suspect this is a timing issue of some sort. If anybody sees the error of my ways please let me know. <g>


The Solution - using DesignTime and the Site property


Finally, after asking Kevin McNeish for some thoughts he mentioned that there are ways to manipulate the designtime environment to generate code for specific constructors or initialization code. The idea is that I can create a property for ParentForm and have the designer automatically fill this property and thus assign it as part of the initialization code.

The property then automatically fires the HookFormEvents() method:

public Form ParentForm { get { return this._ParentForm; } set { this._ParentForm = value; if (this._ParentForm != null && !this.HookFormInitialization) { this.HookFormInitialization = true; this.HookFormKeyEvents(this._ParentForm); } } } Form _ParentForm = null;

Now to get the form to automatically fill the property. The trick to do this goes like this: When the form loads into the VS.NET designer the Site property of the Container gets assigned. We can intercept this event and from the Site property retrieve a reference to the form.

public override ISite Site { get { return base.Site; } set { base.Site = value; if (value == null || this._ParentForm != null) return; IDesignerHost host = value.GetService( typeof(IDesignerHost)) as IDesignerHost; IComponent componentHost = host.RootComponent; this._ParentForm = componentHost as Form; } }

This innocuous code fires only at design time. It captures the current site and then retrieves the host container from it which in this case is a Windows Form. We store that reference to the _ParentForm field which initializes the property for the designer. So now when the designer starts up the value is already set!

Now that the designer holds a reference to our form, we've assigned it to our ParentForm property and the designer generates this code:

< <CODE lang="C#">this.oHelp.HelpNamespace = "webmonitor.chm"; this.oHelp.ParentForm = this;

This of course fires the Set for the HelpProvider and successfully hooks up the form events so we can monitor for our custom keystroke.


This works well, and the process in Help Builder is now very smooth. You can now simply drop the custom Help Provider onto a form and it will response to the custom hotkey.

There are a couple of static properties that are global settings that control whether the custom Help Provider is active and which keys it should use to pop up Help Builder. You can set these at application startup:

HelpBuilderHelpProvider.EnableHelpBuilderHelpProvider = true;
HelpBuilderHelpProvider.HotkeyKey = Keys.F1;
HelpBuilderHelpProvider.HotkeyModifier = Keys.Alt;

More specifically it makes sense to add a configuration switch for the Enable flag:

HelpBuilderHelpProvider.EnableHelpBuilderHelpProvider = App.Configuration.EnableHelpBuilder;

so this can be set without having to recompile.


The Voices of Reason


 

Primadi Setiawan
June 21, 2006

# re: Rethinking the Help Builder HelpProvider: An excercise in Component, Parent and Windows Forms Design time

Can you help me how to know ParentForm from Component at runtime ?

Regards,
Primadi

Rick Strahl
June 21, 2006

# re: Rethinking the Help Builder HelpProvider: An excercise in Component, Parent and Windows Forms Design time

You have to walk the control hierarchy up to the top. The article above shows how to do that in SetHelpKeyWord.


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024