- In This Chapter
- Callbacks in .NET
- Callbacks in COM
- Handling COM Events in Managed Code
- Handling ActiveX Control Events in Managed Code
- Conclusion
Handling ActiveX Control Events in Managed Code
As discussed in the previous two chapters, the ActiveX importer produces its own classes that wrap coclasses representing ActiveX controls as Windows Forms controls. If an ActiveX Assembly didn't also contain some extra transformations for events, then the AxHost-derived wrapper classes that you can host in a Windows Forms control would not appear to have any events. Therefore, the ActiveX importer must clearly do some transformations as well. These transformations, and their use, are covered in this section.
ActiveX Importer Transformations
Just as classes generated by the type library importer contain event members when the coclass lists an interface marked with the IDL [source] attribute, classes generated by the ActiveX importer also contain event members. However, only event members corresponding to the default source interface are created. Any non-default source interfaces are ignored. Name conflicts are handled by appending an Event suffix to applicable event names.
Besides the additions made to the AxHost-derived classes (more of which are shown in the next listing), the ActiveX importer creates some additional types every time it encounters a coclass listing a source interface:
SourceInterfaceName_MethodNameEventHandlerA delegate, one for each method on the default source interface (excluding methods that have no parameters, which use the System.EventHandler delegate). Unlike the delegates created by the type library importer, these signatures do not match the signatures of methods in the source interface. Instead, the delegate signature always has two parameters to (almost) match the convention used by all delegates in Windows Forms. The first parameter is a System.Object type named sender. The second parameter, named e, is a type described next in the list.
SourceInterfaceName_MethodNameEventA class with a public field representing each parameter of a source interface method. One of these classes exists for each method on the source interface that has one or more arguments. This is the second e parameter used in each delegate signature, but fails to conform to .NET guidelines in two ways: the class does not derive from System.EventArgs and it does not have an EventArgs suffix.
AxCoClassNameEventMulticasterA public sink class that implements the source interface. This serves a similar role as the sink helper class generated by the type library importer.
No event interfaces are generated because the AxCoClassName class is always used directly. There's also no separate event provider class because that functionality is merged into the AxCoClassName class.
DIGGING DEEPER
The reason a separate event provider class is generated by the type library importer is that classes marked with the ComImportAttribute pseudo-custom attribute cannot contain any implementation. None of the classes in an ActiveX Assembly are marked with this attribute because they don't directly represent COM types.
Listing 11 shows snippets of C# code inspired by the code obtained by running the ActiveX importer on the file containing the Microsoft Internet Controls type library, for example:
aximp C:\Windows\System32\shdocvw.dll /source
This type library contains the WebBrowser control introduced in Chapter 3:
[ uuid(8856F961-340A-11D0-A96B-00C04FD705A2), helpstring("WebBrowser Control"), control ] coclass WebBrowser { [default] interface IWebBrowser2; interface IWebBrowser; [default, source] dispinterface DWebBrowserEvents2; [source] dispinterface DWebBrowserEvents; };
The WebBrowser coclass has the same two source interfaces as the InternetExplorer coclass used throughout this chapter, so you can easily compare how events are handled with imported ActiveX controls to how they are handled with plain imported coclasses.
Listing 11Some of the Types and Members Generated by the ActiveX Importer for Event Support
1: [AxHost.ClsidAttribute("{8856f961-340a-11d0-a96b-00c04fd705a2}")] 2: [DesignTimeVisibleAttribute(true)] 3: [DefaultProperty("Name")] 4: public class AxWebBrowser : AxHost 5: { 6: private SHDocVw.IWebBrowser2 ocx; 7: private AxWebBrowserEventMulticaster eventMulticaster; 8: private AxHost.ConnectionPointCookie cookie; 9: ... 10: public event System.EventHandler DownloadBegin; 11: public event DWebBrowserEvents2_CommandStateChangeEventHandler 12: CommandStateChange; 13: ... 14: protected override void CreateSink() 15: { 16: try 17: { 18: this.eventMulticaster = new AxWebBrowserEventMulticaster(this); 19: this.cookie = new AxHost.ConnectionPointCookie(this.ocx, 20: this.eventMulticaster, typeof(SHDocVw.DWebBrowserEvents2)); 21: } 22: catch (System.Exception) {} 23: } 24: 25: protected override void DetachSink() 26: { 27: try { 28: this.cookie.Disconnect(); 29: } 30: catch (System.Exception) {} 31: } 32: ... 33: internal void RaiseOnDownloadBegin(object sender, System.EventArgs e) 34: { 35: if (this.DownloadBegin != null) 36: this.DownloadBegin(sender, e); 37: } 38: 39: internal void RaiseOnCommandStateChange(object sender, 40: DWebBrowserEvents2_CommandStateChangeEvent e) 41: { 42: if (this.CommandStateChange != null) 43: this.CommandStateChange(sender, e); 44: } 45: ... 46: } 47: ... 48: public delegate void DWebBrowserEvents2_CommandStateChangeEventHandler( 49: object sender, DWebBrowserEvents2_CommandStateChangeEvent e); 50: 51: public class DWebBrowserEvents2_CommandStateChangeEvent 52: { 53: public int command; 54: public bool enable; 55: 56: public DWebBrowserEvents2_CommandStateChangeEvent( 57: int command, bool enable) 58: { 59: this.command = command; 60: this.enable = enable; 61: } 62: } 63: ... 64: public class AxWebBrowserEventMulticaster : SHDocVw.DWebBrowserEvents2 65: { 66: private AxWebBrowser parent; 67: 68: public AxWebBrowserEventMulticaster(AxWebBrowser parent) 69: { 70: this.parent = parent; 71: } 72: ... 73: public virtual void DownloadBegin() 74: { 75: System.EventArgs downloadbeginEvent = new System.EventArgs(); 76: this.parent.RaiseOnDownloadBegin(this.parent, downloadbeginEvent); 77: } 78: 79: public virtual void CommandStateChange(int command, bool enable) 80: { 81: DWebBrowserEvents2_CommandStateChangeEvent commandstatechangeEvent = 82: new DWebBrowserEvents2_CommandStateChangeEvent(command, enable); 83: this.parent.RaiseOnCommandStateChange(this.parent, 84: commandstatechangeEvent); 85: } 86: ... 87: }
The snippets of the AxWebBrowser class shown focus on two events and their supporting types and membersDownloadBegin and CommandStateChange. Lines 1012 define the two events. Because the DownloadBegin source interface method has no parameters, the simple System.EventHandler delegate is used rather than defining a new DWebBrowserEvents2_DownloadBeginEventHandler delegate with the same signature. The CommandStateChange source interface method does have parameters, so a specific delegate type is used with this event.
The CreateSink and DetachSink methods in Lines 1431 connect and disconnect the connection point for the object's default source interface. CreateSink is invoked when the control's System.ComponentModel.ISupportInitialize.EndInit implementation is called, as is done inside the Visual Studio .NET-generated InitializeComponent method when a Windows Forms control is dragged onto a form in the designer. DetachSink is invoked inside the control's IDisposable.Dispose implementation. Keep this in mind in case you're using an ActiveX control that's picky about when its default connection point is used (as in the TAPI example earlier in the chapter). If some custom initialization routine must be called first, you'd need to insert a call to it somewhere in-between the control's instantiation and the call to EndInit, which would unfortunately be inside the designer-generated InitializeComponent method that you're not supposed to touch.
The RaiseOn... methods, one per event, are defined in Lines 3344 so that the event multicaster class, defined later, has access to raising the events. Lines 4862 contain the pair of delegate and quasi-EventArgs class for the event that doesn't use the standard System.EventArgs delegate. Besides not having the EventArgs suffix and not deriving from System.EventArgs, the event argument classes generated by the ActiveX importer have another oddity that goes against .NET conventionsevery field name is lowercase, even if the original parameters in the source interface method were uppercase (as were Command and Enable). The constructor in Lines 5661 simply provides a convenient means for setting all of the class's fields.
TIP
To minimize confusion when using types generated by the ActiveX importer and to conform to .NET guidelines, it might be a good idea to modify the types produced. This can easily be done using AXIMP.EXE's /source option to generate C# source code for the ActiveX assembly. Before compiling the generated source, you can rename the ...Event classes to ...EventArgs classes, perhaps capitalize the public fields of these classes, and make them derive from System.EventArgs. Another user-friendly change would be to rename the delegate types from SourceInterfaceName_MethodNameEventHandler to simply MethodNameEventHandler, as long as the name doesn't conflict with others.
When compiling source code generated from AXIMP.EXE, you'll need to reference the corresponding Interop Assembly, System.Windows.Forms.dll, and System.dll.
Finally, the AxWebBrowserEventMulticaster class in Lines 6487 is the event sink that implements the default source interface, receives the callbacks, and raises the .NET event to anyone who may be listening.
TIP
Besides renaming types, you can take advantage of ActiveX importer-generated source code to make changes that can add functionality. A good example of this would be to add the code necessary to handle non-default source interfaces just as the default source interface is currently handled.
Using ActiveX Events
To conclude this chapter, we'll update the Web Browser example from Listing 3.4 in Chapter 3 with event support. We'll not only fix the behavior of the Back and Forward buttons to be implemented the way the ActiveX control intended, but add a history list and a log of all events. The final product is pictured in Figure 4.
Figure 4 The event-enabled .NET Web browser.
Inside Visual Studio .NET, the easiest way to add event handlers to an event is to click on the Events lightning bolt in the property browser, then double-click on any events you wish to handle. An empty method signature and the appropriate event hooking and unhooking code are then emitted for you. Figure 5 displays the events for the WebBrowser control. When displayed in categorized mode, the events originating from COM can easily be identified because they fall under the Misc category and have no description in the lower pane.
Figure 5 The Visual Studio .NET property browser showing events.
Listing 12 shows the important parts of the source code for the updated example. The full source code is available in C# and Visual Basic .NET on this book's Web site.
Listing 12Using Events on a Hosted ActiveX Control
1: using System; 2: using SHDocVw; 3: using AxSHDocVw; 4: using System.Windows.Forms; 5: 6: public class MyWebBrowser : Form 7: { 8: private System.ComponentModel.IContainer components; 9: private AxWebBrowser axWebBrowser1; 10: ... 11: // Used for any optional parameters 12: private object m = Type.Missing; 13: 14: public MyWebBrowser() 15: { 16: // Required for Windows Form Designer support 17: InitializeComponent(); 18: axWebBrowser1.GoHome(); 19: } 20: ... 21: private void InitializeComponent() 22: { 23: ... 24: axWebBrowser1.StatusTextChange += new 25: DWebBrowserEvents2_StatusTextChangeEventHandler( 26: axWebBrowser1_StatusTextChange); 27: axWebBrowser1.CommandStateChange += new 28: DWebBrowserEvents2_CommandStateChangeEventHandler( 29: axWebBrowser1_CommandStateChange); 30: axWebBrowser1.TitleChange += new 31: DWebBrowserEvents2_TitleChangeEventHandler( 32: axWebBrowser1_TitleChange); 33: axWebBrowser1.NavigateComplete2 += new 34: DWebBrowserEvents2_NavigateComplete2EventHandler( 35: axWebBrowser1_NavigateComplete2); 36: axWebBrowser1.ProgressChange += new 37: DWebBrowserEvents2_ProgressChangeEventHandler( 38: axWebBrowser1_ProgressChange); 39: ... 40: } 41: ... 42: private void axWebBrowser1_CommandStateChange(object sender, 43: DWebBrowserEvents2_CommandStateChangeEvent e) 44: { 45: eventList.Items.Add("CommandStateChange: command=" + e.command + 46: ", enable=" + e.enable).EnsureVisible(); 47: 48: if (e.command == (int)CommandStateChangeConstants.CSC_NAVIGATEBACK) 49: { 50: // Toggle the state of the "Back" button 51: toolBar1.Buttons[0].Enabled = e.enable; 52: } 53: else if (e.command == 54: (int)CommandStateChangeConstants.CSC_NAVIGATEFORWARD) 55: { 56: // Toggle the state of the "Forward" button 57: toolBar1.Buttons[1].Enabled = e.enable; 58: } 59: } 60: ... 61: private void axWebBrowser1_NavigateComplete2(object sender, 62: DWebBrowserEvents2_NavigateComplete2Event e) 63: { 64: navigateBox.Text = e.uRL.ToString(); 65: historyList.Items.Add(e.uRL); 66: eventList.Items.Add("NavigateComplete2: " + e.uRL).EnsureVisible(); 67: } 68: ... 69: private void axWebBrowser1_ProgressChange(object sender, 70: DWebBrowserEvents2_ProgressChangeEvent e) 71: { 72: progressBar1.Maximum = e.progressMax; 73: progressBar1.Value = e.progress; 74: eventList.Items.Add("ProgressChange: " + e.progress + " out of " + 75: e.progressMax).EnsureVisible(); 76: } 77: ... 78: private void axWebBrowser1_StatusTextChange(object sender, 79: DWebBrowserEvents2_StatusTextChangeEvent e) 80: { 81: statusBar1.Text = e.text; 82: eventList.Items.Add("StatusTextChange: " + e.text).EnsureVisible(); 83: } 84: 85: private void axWebBrowser1_TitleChange(object sender, 86: DWebBrowserEvents2_TitleChangeEvent e) 87: { 88: this.Text = e.text; 89: eventList.Items.Add("TitleChange: " + e.text).EnsureVisible(); 90: } 91: ... 92: private void toolBar1_ButtonClick(object sender, 93: ToolBarButtonClickEventArgs e) 94: { 95: if (e.Button.Text == "Back") 96: { 97: axWebBrowser1.GoBack(); 98: } 99: else if (e.Button.Text == "Forward") 100: { 101: axWebBrowser1.GoForward(); 102: } 103: else if (e.Button.Text == "Stop") 104: { 105: axWebBrowser1.Stop(); 106: } 107: else if (e.Button.Text == "Refresh") 108: { 109: axWebBrowser1.CtlRefresh(); 110: } 111: else if (e.Button.Text == "Home") 112: { 113: axWebBrowser1.GoHome(); 114: } 115: } 116: 117: private void goButton_Click(object sender, System.EventArgs e) 118: { 119: axWebBrowser1.Navigate(navigateBox.Text, ref m, ref m, ref m, ref m); 120: } 121: }
The first difference between this listing and the corresponding listing in Chapter 3 is in Lines 2438. This shows a sampling of some of the events being handled. This code is automatically generated by Visual Studio .NET when double-clicking on events in the property browser.
Lines 4290 show all of the interesting events that do something other than add information to the log. The implementation of the CommandStateChange event handler in Lines 4259 first adds some information to the eventList log, which is a ListView control. Then, it toggles the state of either the Back or Forward button based on the information passed into the event. The NavigateComplete2 event handler in Lines 6167 updates the TextBox control with the current URL and adds it to the history list, a ListBox control. Notice how the lowercase transformations done by the ActiveX importer produces a funny looking field called uRL!
The ProgressChange event handler in Lines 6976 uses the passed-in information to control the form's ProgressBar control, and the StatusTextChange event handler in Lines 7883 updates the form's StatusBar control with the passed-in text. Finally, the TitleChange event handler in Lines 8590 updates the form's caption with the title of the current Web page.
The updated toolBar1_ButtonClick implementation now simply calls the methods corresponding to each button's action. The calls to GoBack and GoForward no longer need to be wrapped inside exception handling because the user shouldn't be able to click these buttons when there are no more pages in the list. (If an exception were to occur, it would be a problem that we'd want to know about.)