Properties, Methods, and Events
Objects in an Office application's object model are .NET classes that have properties, methods, and events that can be accessed by solution code. An object in the object model is required to have at least one property, method, or event. Most of the objects in an Office application's object model have several properties, a few methods, and no events. The most important objects in the object model, such as Application and Document, are typically much more complex and have a much larger number of properties and methods as well as events. For example, Word's Application object has about 100 properties, 60 methods, and 20 events. Table 1-2 lists some of the properties, methods, and events on the Word Application object to give a sense of the types of functionality an object model object provides.
Name |
What It Does |
---|---|
Properties |
|
ActiveDocument |
Returns a Document object for the active documentthe document that is currently being edited by the user. |
ActivePrinter |
Gets and sets the default printer. |
Caption |
Gets and sets the caption text for the application windowtypically this is set to "Microsoft Word." |
Documents |
Returns a Documents collection that represents the collection of open Word documents. |
Methods |
|
---|---|
Activate |
Brings Word to the front of other windows and makes it the active window. |
NewWindow |
Creates a new Word window that shows the same document as the currently active window and returns a Window object model object representing that new window. |
Quit |
Closes Word. |
Events |
|
---|---|
DocumentBeforeClose |
An event that is raised before a document is closed. The Document object for the document being closed is passed as a parameter to the event along with a bool cancel parameter. If the code handling the event sets the cancel parameter to true, the document will not be closed. |
DocumentOpen |
An event that is raised when a document is opened. The Document object for the document being opened is passed as a parameter to the event. |
WindowActivate |
An event that is raised when a Word window is activated by the user, typically by clicking an inactive window thereby making it active. The Document object for the document being activated is passed as a parameter to the event along with a Window object for the window that was activated (because two windows could be showing the same document). |
In Office object models, properties predominate, followed by methods, and trailed distantly by events. Figure 1-3 shows the distribution of properties, methods, and events in the Word, Excel, and Outlook object models. A couple of general statements can be made about the Office object models as shown by Figure 1-3. The Excel object model is the biggest of the Office object models in terms of total number of properties, methods, and events, followed closely by Word. Word has a very small number of events. We can also say that there are many more properties in Office object models than methods.
Figure 1-3. Distribution of properties, methods, and events in the Word, Excel, and Outlook object models.
Properties
Properties are simple methods which allow you to read or write particular named values associated with an object. For example, Word's Application object has a property called CapsLock, which returns a bool value. If the Caps Lock key is down, it will return true; if the Caps Lock key is up, it will return false. Listing 1-4 shows some code that examines this property. The code assumes that the root Application object of the Word object model has already been assigned to a variable called app.
Listing 1-4. A Property That Returns a Value Typethe bool CapsLock Property on Word's Application Object
if (app.CapsLock == true) { MessageBox.Show("CapsLock key is down"); } else { MessageBox.Show("CapsLock key is up"); }
Another thing to note about the CapsLock property is that it is a read-only property. That is to say, you cannot write code that sets the CapsLock property to false; you can only get the value of the CapsLock property. Within the Office object model, many properties are read-only, and many are not. If you try to set a read-only property to some value, an error will occur when you compile your code.
The CapsLock property returns a bool value. It is also possible for a property to return an enumeration. Listing 1-5 shows some code that uses the WindowState property to determine whether Word's window is maximized, minimized, or normal. This code uses C#'s switch statement to evaluate the WindowState property and compare its value to the three possible enumerated value constants. Notice that when you specify enumerated values in C#, you must specify both the enumerated type name and the enumerated valuefor example, if you just used wdWindowStateNormal rather than WdWindowState.wdWindowStateNormal the code will not compile.
Listing 1-5. A Property That Returns an Enumerationthe WindowState Property on Word's Application Object
switch (app.WindowState) { case Word.WdWindowState.wdWindowStateNormal: MessageBox.Show("Normal"); break; case Word.WdWindowState.wdWindowStateMaximize: MessageBox.Show("Maximized"); break; case Word.WdWindowState.wdWindowStateMinimize: MessageBox.Show("Minimized"); break; default: break; }
Properties can also return other object model objects. For example, Word's Application object has a property called ActiveDocument that returns the currently active documentthe one the user is currently editing. The ActiveDocument property returns another object in the Word object model called Document. Document in turn also has properties, methods, and events. Listing 1-6 shows code that examines the ActiveDocument property and then displays the Name property of the Document object.
Listing 1-6. A Property That Returns Another Object Model Objectthe ActiveDocument Property on Word's Application Object
Word.Document myDocument = app.ActiveDocument; MessageBox.Show(myDocument.Name);
What happens if there is no active documentfor example, if Word is running but no documents are opened? In the case of the ActiveDocument property, it throws an exception. So a safer version of the preceding code would catch the exception and report no active document was found. Listing 1-7 shows this safer version. An even better approach is to check the Count property of the Application object's Documents collection to see whether any documents are open before accessing the ActiveDocument property.
Listing 1-7. A Property That Might Throw an Exceptionthe ActiveDocument Property on Word's Application Object
Word.Document myDocument = null; try { myDocument = app.ActiveDocument; MessageBox.Show(myDocument.Name); } catch (Exception ex) { MessageBox.Show( String.Format("No active document: {0}", ex.Message)); }
Object models sometimes behave differently in an error case in which the object you are asking for is not available or does not make sense in a particular context. The property can return a null value. The way to determine whether an object model property will throw an exception or return a null value is by consulting the object model documentation for the property in question. Excel's Application object uses this pattern for its ActiveWorkbook property. If no Excel workbook is open, it returns null instead of throwing an exception. Listing 1-8 shows how to write code that handles this pattern of behavior.
Listing 1-8. A Property That Might Return nullthe ActiveWorkbook Property on Excel's Application Object
Excel.Workbook myWorkbook = app.ActiveWorkbook; if (myWorkbook == null) { MessageBox.Show("No active workbook"); } else { MessageBox.Show(myWorkbook.Name); )
Parameterized Properties
The properties examined so far are parameterless. However, some properties require parameters. For example, Word's Application object has a property called FileDialog that returns a FileDialog object. The FileDialog property takes an enumeration parameter of type MsoFileDialogType, which is used to pick which FileDialog is returned. Its possible values are msoFileDialogOpen, msoFileDialogSaveAs, msoFileDialogFilePicker, and msoFileDialogFolderPicker.
C# does not support calling parameterized properties as properties. When you go to use the Word object model from C# and look for the FileDialog property on Word's Application object, it is nowhere to be seen. The FileDialog property is callable from C#, but only via a methodthe method is named get_FileDialog. So when you are looking for a parameterized property in C#, be sure to look for the get_Property method (where Property is the name of the property you want to access). To set parameterized properties in C# (assuming they are not read-only properties), there is a separate method called set_Property (where Property is the name of the property you are going to set).
An exception to this is found when using VSTO. A handful of Word and Excel object model objects are extended by VSTO. These objects have been extended to give you a different way of accessing a parameterized propertyvia an indexer. An indexer enables you to access the property in the same way you would access an arraywith the name of the property followed by a parameter list between the delimiters [ and ]. So for an object model object extended by VSTO, such as Worksheet, a parameterized property such as Range, which takes two parameters, can be called using the indexer syntax : Range[parameter1, parameter2] instead of get_Range(parameter1, parameter2).
The code in Listing 1-9 uses the FileDialog property called as a method and passes msoFileDialogFilePicker as a parameter to specify the type of FileDialog object to be returned. It then calls a method on the returned FileDialog object to show the dialog.
Listing 1-9. A Parameterized Property Called as a Method That Takes an Enumeration Parameter and Returns an Object Model Objectthe FileDialog Property on Word's Application Object
Office.FileDialog dialog = app.get_FileDialog( Office.MsoFileDialogType.msoFileDialogFilePicker); dialog.Show();
The Office object models also have properties that have optional parameters. Optional parameters are parameters that can be omitted and the Office application will fill in a default value for the parameter. Optional parameters are typically of type object because of how optional parameters are passed to the underlying COM API. In C#, you must pass a special value to optional parameters that are of type object if you do not want to specify the parameter. This special value is called Type.Missing, and it must be passed for optional parameters that you do not want to specify directly (unlike Visual Basic in which you can omit the parameter entirely). In VSTO projects, a "missing" variable is predeclared for you (that is, set to Type.Missing). Therefore, in VSTO code, you will often see missing passed rather than Type.Missing.
Occasionally, you will find an optional parameter is of some enumeration type rather than of type object. For this kind of optional parameter, you cannot pass Type.Missing and must instead pass a specific enumerated type value. You can find out what the default enumerated type value is for the optional parameter by consulting the documentation for the method or by using the object browser in a Visual Basic projectunfortunately, the C# object browser does not show the default value for an optional enumerated type parameter.
Listing 1-10 shows an example of calling a parameterized property called Range, which is found on Excel's Application object. The Range property is accessed via the get_Range method because parameterized properties can only be called via a method in C#. Calling the get_Range method on Excel's Application object returns the Range object in the active workbook as specified by the parameters passed to the method. The get_Range method takes two parameters. The first parameter is required, and the second parameter is optional. If you want to specify a single cell, you just pass the first parameter. If you want to specify multiple cells, you have to specify the top-left cell in the first parameter and the bottom-right cell in the second parameter.
Listing 1-10. A Parameterized Property Called as a Method with Optional Parameters the Range Property on Excel's Application Object
// Calling a parameterized property with a missing optional parameter Excel.Range myRange = app.get_Range("A1", Type.Missing); // Calling a parameterized property without missing parameters Excel.Range myRange2 = app.get_Range("A1", "B2");
In Word, optional parameters are handled differently than in the other Office applications. Word's object model requires that optional parameters be passed by reference. This means that you cannot pass Type.Missing directly as the code in Listing 1-10 did. Instead, you must declare a variable, set it to Type.Missing, and pass that variable by reference. You can reuse the same declared variable that has been set to Type.Missing if a parameter has multiple parameters you want to omit. In a VSTO project, you can just pass by reference the missing variable that is predeclared for you. Listing 1-11 shows how to specify optional parameters in Word. In this example, the code uses a parameterized property from Word's Application object called SynonymInfo, which has a required string parameter to specify a word you want a synonym for and an optional parameter to specify the language ID you want to use. The SynonymInfo property is accessed via the get_SynonymInfo method because parameterized properties can only be called via a method in C#. By omitting the optional language ID parameter and passing by reference a variable set to Type.Missing, Word will default to use the current language you have installed.
Listing 1-11. A Parameterized Property Called as a Method with Optional Parameters Passed by Referencethe SynonymInfo Property on Word's Application Object
object missing = Type.Missing; // Calling a parameterized property in Word // with a missing optional parameter Word.SynonymInfo synonym = app.get_SynonymInfo( "happy", ref missing);
Properties Common to Most Objects
Because all the object model objects have object as their base class, you will always find the methods GetType, GetHashCode, Equals, and ToString on every object model object. You will also often find a property called Application that will return the Application object associated with the object. This is provided as a quick way to get back to the root of the object model. Many objects have a property called Creator, which gives you a code indicating which application the object was created in. Finally, you will often find a Parent property that returns the object that is the parent in the object model hierarchy.
Methods
A method is typically more complex than a property and represents a "verb" on the object that causes something to happen. It may or may not have a return value and is more likely to have parameters than a property.
The simplest form of a method has no return type and no parameters. Listing 1-12 shows the use of the Activate method from Word's Application object. This method activates the Word application, making its window the active window (the equivalent of clicking the Word window in the taskbar to activate it).
Listing 1-12. A Method with No Parameters and No Return Typethe Activate Method on Word's Application Object
MessageBox.Show("Activating the Word window."); app.Activate();
Methods may also have parameters and no return type. Listing 1-13 shows an example of this kind of a method. The ChangeFileOpenDirectory method takes a string that is the name of the directory you want Word to default to when the Open dialog box is shown. For a method this simple, you might wonder why a property was not used insteadfor example, you can imagine Word having a FileOpenDirectory property. In this case, the ChangeFileOpenDirectory only changes the default open directory temporarilyfor the lifetime of the current Word session. When you exit Word and then restart Word, the default will no longer be what you set with this method. Perhaps for this reason, this functionality was exposed via a method rather than a property. A second reason why object models sometimes use a simple method such as this rather than a property is because some values exposed in an object model are "write-only"; that is, they can only be set but cannot be read. It is common to create a read-only property, but not common to create a write-only property. So when a write-only property is needed, a simple method is often used instead.
Listing 1-13. A Method with Parameters and No Return Typethe ChangeFileOpenDirectory Method on Word's Application Object
app.ChangeFileOpenDirectory(@"c: emp"); MessageBox.Show("Will open out of temp for this session.");
Methods can have no parameters and a return type. Listing 1-14 shows an example of this kind of a method. The DefaultWebOptions method returns the DefaultWebOptions object, which is then used to set options for Word's Web features. In this case, DefaultWebOptions really should have been implemented as a read-only property as opposed to a method.
Listing 1-14. A Method with No Parameters and A Return Typethe DefaultWebOptions Method on Word's Application Object
Word.DefaultWebOptions options = app.DefaultWebOptions(); MessageBox.Show(String.Format("Pixels per inch is {0}.", options.PixelsPerInch));
Methods can have parameters and a return type. Listing 1-15 shows an example of this kind of a method. The CentimetersToPoints method takes a centimeter value and converts it to points, which it returns as the return value of the method. Points is a unit often used by Word when specifying spacing in the document.
Listing 1-15. A Method with Parameters and a Return Typethe CentimetersToPoints Method on Word's Application Object
float centimeters = 15.0; float points = app.CentimetersToPoints(centimeters); MessageBox.Show(String.Format("{0} centimeters is {1} points.", centimeters, points));
Methods can also have optional parameters. Optional parameters do not need to be specified directly to call the method. For any parameters you do not want to specify, you pass a special value defined by .NET called Type.Missing. Listing 1-16 shows a method called CheckSpelling in Excel that has optional parameters. Listing 1-16 illustrates the syntax you use to omit parameters you do not want to specify. The CheckSpelling method takes a required stringthe word you want to check the spelling ofalong with two optional parameters. The first optional parameter enables you to pick a custom dictionary to check the spelling against. The second optional parameter enables you to tell the spell checker to ignore words in all uppercasesuch as an acronym. In Listing 1-16, we check a phrase without specifying any of the optional parameterswe pass Type.Missing to each optional parameter. We also check a second phrase that has an acronym in all uppercase so we pass Type.Missing to the first optional parameter because we do not want to use a custom dictionary, but we specify the second optional parameter to be true so the spell checker will ignore the words in all uppercase.
Listing 1-16. A Method with Optional Parameters and a Return Typethe CheckSpelling Method on Excel's Application Object
string phrase1 = "Thes is spelled correctly. "; string phrase2 = "This is spelled correctly AFAIK. "; bool isCorrect1 = app.CheckSpelling(phrase1, Type.Missing, Type.Missing); bool isCorrect2 = app.CheckSpelling(phrase2, Type.Missing, true);
Optional Parameters in Word
Optional parameters in Word can produce some strange looking C# code because the values passed to optional parameters must be passed by reference. For example, Listing 1-17 shows how to spell check a string using the Word object model in C#.
Listing 1-17. A Method with Optional Parameters Passed by Referencethe CheckSpelling Method on Word's Application Object
void SpellCheckString() { string phrase1 = "Speling erors here."; object ignoreUpperCase = true; object missing = Type.Missing; bool spellingError = app.CheckSpelling(phrase1, ref missing, ref ignoreUpperCase, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing); if (spellingError) MessageBox.Show("Spelling error found"); else MessageBox.Show("No errors"); }
The first thing that probably comes to mind if you are a Visual Basic programmer and you have never seen code written against Word in C# is "Why is this so verbose?" Visual Basic does some special things for you when there are optional arguments in a method, so the Visual Basic version of this is much simpler, as shown in Listing 1-18.
Listing 1-18. A Method with Optional Parameters Passed by Reference Using Visual Basic the CheckSpelling Method on Word's Application Object
Public Sub SpellCheckString() Dim phrase1 As String = "Speling erors here." Dim spellingError As Boolean spellingError = app.CheckSpelling(myString, , True) If spellingError Then MsgBox("Spelling error found.") Else MsgBox("No error found.") End If End Sub
In Visual Basic, you do not have to worry about passing a value for each optional argumentthe language handles this for you. You can even use commas, as shown in Listing 1-18, to omit one particular variable you do not want to specify. In this case, we did not want to specify a custom dictionary, but we did want to pass a value for the parameter IgnoreUpperCase, so we omitted the custom dictionary argument by just leaving it out between the commas.
The first thing that probably comes to mind if you're a C# programmer and you have never seen code written against Word in C#, such as the code shown in Listing 1-17, is "Why is all that stuff passed by reference?" When you are talking to Office object model methods, properties, and events, you are talking to the object model through a .NET technology called COM interop (short for interoperate). The Office object models are all written in unmanaged code (C and C++) that is exposed via COM interfaces. You will read more detail later in this chapter about the technology called interop assemblies that allows managed code to call COM objects.
If you were to examine the COM definition for the CheckSpelling method used in Listing 1-17 as defined by Word's COM type library, you would see something like this:
HRESULT CheckSpelling( [in] BSTR Word, [in, optional] VARIANT* CustomDictionary, [in, optional] VARIANT* IgnoreUppercase, [in, optional] VARIANT* MainDictionary, [in, optional] VARIANT* CustomDictionary2, [in, optional] VARIANT* CustomDictionary3, [in, optional] VARIANT* CustomDictionary4, [in, optional] VARIANT* CustomDictionary5, [in, optional] VARIANT* CustomDictionary6, [in, optional] VARIANT* CustomDictionary7, [in, optional] VARIANT* CustomDictionary8, [in, optional] VARIANT* CustomDictionary9, [in, optional] VARIANT* CustomDictionary10, [out, retval] VARIANT_BOOL* prop);
Note that any parameter that is marked as optional is specified as a pointer to a VARIANT in Word (VARIANT*). A VARIANT is a type in COM that is roughly equivalent to object in .NETit can contain many different types of values. Excel does not typically use a pointer to a VARIANT for optional parameters, so you do not have this by ref issue for most of Excel. When the PIA is generated, the C# IntelliSense looks like this:
bool _Application.CheckSpelling(string Word, ref object CustomDictionary, ref object IgnoreUppercase, ref object MainDictionary, ref object CustomDictionary2, ref object CustomDictionary3, ref object CustomDictionary4, ref object CustomDictionary5, ref object CustomDictionary6, ref object CustomDictionary7, ref object CustomDictionary8, ref object CustomDictionary9, ref object CustomDictionary10)
Because of how Word defined optional parameters in its COM objects (as pointer to a VARIANT) and because of how that translates into .NET code (an object passed by reference), any optional argument in Word has to be passed by reference from C# and has to be declared as an object. Even though you would like to strongly type the IgnoreUppercase parameter to be a bool in the CheckSpelling example, you cannot. You have to type it as an object or you will get a compile error. This ends up being a little confusing because you can strongly type the first argumentthe string you want to check. That's because in the CheckSpelling method, the Word argument (the string you are spell checking) is not an optional argument to CheckSpelling. Therefore, it is strongly typed and not passed by reference. Also note that optional arguments are always listed after all required argumentsthat is, you will never find a situation where argument1 is optional and argument2 is not.
This brings us back to Type.Missing. In C# to omit an optional argument you pass an object by reference set to Type.Missing. In our example, we just declared one variable called missing and passed it in 11 times.
When you pass objects by reference to most managed functions, you do so because the managed function is telling you that it might change the value of that object you passed into the function. So it might seem bad to you that we are declaring one variable and passing it to all the parameters of CheckSpelling that we do not care about. After all, imagine you have a function that takes two parameters by reference. If you pass in the same variable set to Type.Missing to both parameters, what if the code evaluating the first parameter changes it from Type.Missing to some other value such as the bool value true? This would also affect both the first parameter and the second parameter, and the function might do something different when it looks at the second parameter that was originally set to Type.Missing because it is now set to true as well.
To avoid this, you might think we would have to declare a missing1 tHRough missing11 variable because of the possibility that Word might go and change one of the by ref parameters on you and thereby make it so you are no longer passing Type.Missing but something else such as true that might cause unintended side effects.
Fortunately, you do not have to do this when working with Office object models. Remember that the underlying Word Application object is an unmanaged object, and you are talking to it through COM interop. The COM interop layer realizes that you are passing a Type.Missing to an optional argument on a COM object. So interop obliges and instead of passing a reference to your missing variable in some way, the interop layer passes a special COM value that indicates that the parameter was missing. Your missing variable that you passed by reference is safe because it was never really passed directly into Word. It is impossible for Word to mess with your variable, even though when you look at the syntax of the call it looks like it would be possible because it is passed by reference.
Therefore, the CheckSpelling code in Listing 1-17 is completely correct. Your missing variable is safeit will not be changed on you by Word even though you pass it by reference. But remember, this is a special case that only applies when talking through COM interop to an unmanaged object model that has optional arguments. Do not let this special case make you sloppy when calling methods on objects outside the Office object model that require parameters to be passed by reference. When talking to non-Office object model methods, you have to be careful when passing parameters by reference because the managed method can change the variable you pass.
Events
You have now read about the use of properties and methods in some detailthese are both ways that your code controls the Office application. Events are the way the Office application talks back to your code and enables you to run additional code in response to some condition that occurred in the Office application.
In the Office object models, there are far fewer events than there are methods and propertiesfor example, there are 36 events in Word and 84 in Excel. Some of these events are duplicated on different objects. For example, when the user opens a Word document, both the Application object and the newly created Document object raise Open events. If you wanted to handle all Open events on all documents, you would handle the Open event on the Application object. If you had code associated with a particular document, you would handle the Open event on the corresponding Document object.
In most of the Office object models, events are raised by a handful of objects. The only objects that raise events in the Word object model are Application, Document, and OLEControl. The only objects that raise events in the Excel object model are Application, Workbook, Worksheet, Chart, OLEObject, and QueryTable. Outlook is a bit of an exception: About half of the objects in the Outlook object model raise events. However, most of these objects raise the same set of events, making the total number of unique events small in Outlook as well.
Table 1-3 shows all the events raised by Excel's Application object. This table represents almost all the events raised by Excel because events prefaced by Sheet are duplicated on Excel's Worksheet object and events prefaced by Workbook are duplicated on Excel's Workbook object. The only difference in these duplicated events is that the Application-level Sheet and Workbook events pass a parameter of type Sheet or Workbook to indicate which worksheet or workbook raised the event. Events raised by a Workbook object or Sheet object do not have to pass the Sheet or Workbook parameter because it is implicitly determined from which Workbook or Sheet object you are handling events for.
Event Name |
When It Is Raised |
---|---|
NewWorkbook |
When a new workbook is created |
SheetActivate |
When any worksheet is activated |
SheetBeforeDoubleClick |
When any worksheet is double-clicked |
SheetBeforeRightClick |
When any worksheet is right-clicked |
SheetCalculate |
After any worksheet is recalculated |
SheetChange |
When cells in any worksheet are changed by the user |
SheetDeactivate |
When any worksheet is deactivated |
ShetFollowHyperlink |
When the user clicks a hyperlink in any worksheet |
SheetPivotTableUpdate |
After the sheet of a PivotTable report has been updated |
SheetSelectionChange |
When the selection changes on any worksheet |
WindowActivate |
When any workbook window is activated |
WindowDeactivate |
When any workbook window is deactivated |
WindowResize |
When any workbook window is resized |
WorkbookActivate |
When any workbook is activated |
WorkbookAddinInstall |
When any workbook is installed as an add-in |
WorkbookAddinUninstall |
When any workbook is uninstalled as an add-in |
WorkbookAfterXmlExport |
After data in a workbook is exported as an XML data file |
WorkbookAfterXmlImport |
After data in a workbook is imported from an XML data file |
WorkbookBeforeClose |
Before any workbook is closed |
WorkbookBeforePrint |
Before any workbook is printed |
WorkbookBeforeSave |
Before any workbook is saved |
WorkbookBeforeXmlExport |
Before data in any workbook is exported as an XML data file |
WorkbookBeforeXmlImport |
Before data in any workbook is imported from an XML data file |
WorkbookDeactivate |
When any workbook window is deactivated |
WorkbookNewSheet |
When a new worksheet is created in any workbook |
WorkbookOpen |
When any workbook is opened |
WorkbookPivot TableCloseConnection |
After a PivotTable report connection has been closed |
WorkbookPivot TableOpenConnection |
After a PivotTable report connection has been opened |
WorkbookSync |
When a workbook that is part of a document workspace is synchronized with a copy on the server |
To handle the events raised by Office object models, you must first declare a callback method in your code that matches the signature expected by the event being raised. For example, the Open event on the Application object in Excel expects a callback method to match the signature of this delegate:
public delegate void AppEvents_WorkbookOpenEventHandler(Workbook wb);
To handle this event, you must declare a callback method that matches the expected signature. Note that we omit the delegate keyword shown in the signature above in our callback method because we are not defining a new delegate type, just implementing an existing one defined by the Office object model.
public void MyOpenHandler(Excel.Workbook wb) { MessageBox.Show(wb.Name + " was opened. "); }
Finally, you must connect your callback method up to the Excel Application object that raises this event. We create a new instance of the delegate object defined by the Excel object model called AppEvents_WorkbookOpenEventHandler. We pass to the constructor of this object our callback method. We then add this delegate object to the Excel Application WorkbookOpen event using the += operator.
app.WorkbookOpen += new AppEvents_WorkbookOpenEventHandler(MyOpenHandler);
Although this seems complex, Visual Studio 2005 helps by auto-generating most of this line of code as well as the corresponding event handler automatically. If you were typing this line of code, after you type +=, Visual Studio displays a pop-up tooltip. If you press the Tab key twice then Visual Studio generates the rest of the line of code and the callback method automatically.
Listing 1-19 shows a complete implementation of a callback method and event hookup in a simple class. The callback method is called MyOpenHandler and is a member method of the class SampleListener. This code assumes that a client creates an instance of this class, passing the Excel Application object to the constructor of the class. The ConnectEvents method connects the callback method MyOpenHandler to the Excel Application object's WorkbookOpen event. The DisconnectEvents method removes the callback method MyOpenHandler from the Excel Application object's WorkbookOpen event by using the -= operator on the delegate object. It might seem strange that we create a new instance of our delegate object when removing it, but this is the way C# supports removing delegates.
The result of this code is that any time a workbook is opened and ConnectEvents has been called, MyOpenHandler will handle the WorkbookOpen event raised by Excel's Application object and it will display a message box with the name of the workbook that was opened. DisconnectEvents can be called to stop MyOpenHandler from handling the WorkbookOpen event raised by Excel's Application object.
Listing 1-19. A Class That Listens to the Excel Application Object's WorkbookOpen Event
using Excel = Microsoft.Office.Interop.Excel; class SampleListener { private Excel.Application app; public SampleListener(Excel.Application application) { app = application; } public void ConnectEvents() { app.WorkbookOpen += new AppEvents_WorkbookOpenEventHandler(this.MyOpenHandler); } public void DisconnectEvents() { app.WorkbookOpen -= new AppEvents_WorkbookOpenEventHandler(this.MyOpenHandler); } public void MyOpenHandler(Excel.Workbook workbook) { MessageBox.Show(String.Format("{0} was opened.", workbook.Name)); } }
The "My Button Stopped Working" Issue
One issue commonly encountered when beginning to program against Office events in .NET is known as the "my button stopped working" issue. A developer will write some code to handle a Click event raised by a CommandBarButton in the Office toolbar object model. This code will sometimes work temporarily but then stop. The user will click the button, but the Click event appears to have stopped working.
The cause of this issue is connecting an event callback to an object whose lifetime does not match the desired lifetime of the event. This typically occurs when the object to which you are connecting an event handler goes out of scope or gets set to null so that it gets garbage collected. Listing 1-20 shows an example of code that makes this mistake. In this case, an event handler is connected to a newly created CommandBarButton called btn. However, btn is declared as a local variable, so as soon as the ConnectEvents function exits and garbage collection occurs, btn gets garbage collected and the event connected to btn is not called.
The complete explanation of this behavior has to do with btn being associated with something called a Runtime Callable Wrapper (RCW), which is described in Chapter 24, "Creating Outlook Add-Ins with VSTO." Without going into too much depth, btn holds on to an RCW that is necessary for the event to propagate from the unmanaged Office COM object to the managed event handler. When btn goes out of scope and is garbage collected, the reference count on the RCW goes down, and the RCW is disposedthereby breaking the event connection.
Listing 1-20. A Class That Fails to Handle the CommandBarButton Click Event
using Excel = Microsoft.Office.Interop.Excel; using Office = Microsoft.Office.Core; class SampleListener { private Excel.Application app; public SampleListener(Excel.Application application) { app = application; } // This appears to connect to the Click event but // will fail because btn is not put in a more permanent // variable. public void ConnectEvents() { Office.CommandBar bar = Application.CommandBars["Standard"]; Office.CommandBarButton myBtn = bar.Controls.Add(1, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing) as Office.CommandBarButton; if (myBtn!= null) { myBtn.Caption = "My Button"; myBtn.Tag = "SampleListener.btn"; myBtn.Click += new Office. _CommandBarButtonEvents_ClickEventHandler( myBtn_Click); } } // The Click event will never reach this handler. public void myBtn_Click(Office.CommandBarButton ctrl, ref bool cancelDefault) { MessageBox.Show("Button was clicked"); } }
Listing 1-21 shows a second example of a failed event listener class that is attempting to connect to Outlook's NewInspector event, which is raised by Outlook's Inspectors object. This event is raised whenever an inspector window opens (a window where you are viewing or editing an Outlook item). This code will also fail to get any events. In this case, it is more subtle because the event handler is connected to the Inspectors object, which is temporarily created in the line of code that begins with app.Inspectors. Because the Inspectors object returned by app.Inspectors is not stored in a permanent variable, the temporarily created Inspectors object is garbage collected, and the event connected to it will never get called.
Listing 1-21. A Class That Fails to Handle the Outlook Inspectors Object's NewInspector Event
using Outlook = Microsoft.Office.Interop.Outlook; class SampleListener { private Outlook.Application app; public SampleListener(Outlook.Application application) { app = application; } // This will appear to connect to the NewInspector event, but // will fail because Inspectors is not put in a more permanent // variable. public void ConnectEvents() { app.Inspectors.NewInspector += new Outlook. InspectorsEvents_NewInspectorEventHandler( MyNewInspectorHandler); } // The NewInspector event will never reach this handler. public void MyNewInspectorHandler(Outlook.Inspector inspector) { MessageBox.Show("New inspector."); } }
The fix for this issue is to declare a variable whose lifetime matches the lifetime of your event handler and set it to the Office object for which you are handling the event. Listing 1-22 shows a rewritten class that successfully listens to the CommandBarButton Click event. This class works because instead of using the method-scoped variable btn, it uses a class-scoped member variable called myBtn. This ensures that the event handler will be connected for the lifetime of the class when ConnectEvents is called.
Listing 1-22. A Class That Succeeds in Handling the CommandBarButton Click Event Because It Stores the CommandBarButton Object in a Class Member Variable
using Excel = Microsoft.Office.Interop.Excel; using Office = Microsoft.Office.Core; class SampleListener { private Excel.Application app; private Office.CommandBarButton myBtn; public SampleListener(Excel.Application application) { app = application; } public void ConnectEvents() { Office.CommandBar bar = Application.CommandBars["Standard"]; myBtn = bar.Controls.Add(1, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing) as Office.CommandBarButton; if (myBtn != null) { myBtn.Caption = "My Button"; myBtn.Tag = "SampleListener.btn"; myBtn.Click += new Office. _CommandBarButtonEvents_ClickEventHandler( myBtn_Click); } } public void myBtn_Click(Microsoft.Office.Core.CommandBarButton ctrl, ref bool cancelDefault) { MessageBox.Show("Button was clicked"); } }
Listing 1-23 shows a similar fix for our failed Outlook example. Here we declare a class-level variable called myInspectors that we assign to app.Inspectors. This ensures that our event handler will be connected for the lifetime of the class when ConnectEvents is called because the lifetime of myInspectors now matches the lifetime of the class.
Listing 1-23. A Class That Succeeds in Handling the Outlook Inspectors Object's NewInspector Event Because It Stores the Inspectors Object in a Class Member Variable
using Outlook = Microsoft.Office.Interop.Outlook; class SampleListener { private Outlook.Application app; private Outlook.Inspectors myInspectors; public SampleListener(Outlook.Application application) { app = application; } public void ConnectEvents() { this.myInspectors = myAplication.Inspectors; myInspectors.NewInspector += new Outlook. InspectorsEvents_NewInspectorEventHandler( MyNewInspectorHandler); } public void MyNewInspectorHandler(Outlook.Inspector inspector) { MessageBox.Show("New inspector."); } }
When Method Names and Event Names Collide
In several cases in the Office object models, an object has an event and a method that have the same name. For example, Excel's Workbook object has an Activate event and an Activate method. Outlook's Inspector and Explorer objects have Close events and Close methods.
When using an Office object model object that has events such as Workbook, you are actually using an object that implements several interfaces. One of those interfaces has the definition of the Close method and a separate interface has the definition of the Close event. To handle an event for which a method name collides, you must cast your object to the interface that contains the event definitions. The interface that contains the event interfaces is named ObjectNameEvents_Event, where ObjectName is the name of the object such as Workbook or Inspector.
Listing 1-24 casts the Workbook object myWorkbook to Excel.WorkbookEvents_Event when adding the event handler. By casting myWorkbook to the WorkbookEvents_Event interface, we disambiguate between the Close method (which is on the Workbook interface) and the Close event (which is on the WorkbookEvents_Event interface).
Listing 1-24. A Class That Will Listen to the Excel Workbook Object's Activate Event by Casting to WorkbookEvents_Event
using Excel = Microsoft.Office.Interop.Excel; class SampleListener { private Excel.Workbook myWorkbook; public SampleListener(Excel.Workbook workbook) { myWorkbook = workbook; } public void ConnectEvents() { ((Excel.WorkbookEvents_Event)myWorkbook).Activate += new Excel.WorkbookEvents_ActivateEventHandler(Activate) } public void Activate() { MessageBox.Show("Workbook Activated"); } }