In Chapter 2, "Introduction to Office Solutions," you saw that VSTO allows developers to build customized document solutions by using Word and Excel as designers inside Visual Studio. The host item classes expose the host controls present at design time as custom properties on a class that aggregates the underlying unmanaged object. But what about host controls not present at design time? What if you want to create new named ranges, bookmarks, buttons, or other controls at runtime? It would be nice to be able to use the new events and other extensions to the programming model on dynamically generated controls. As you will see, VSTO supports adding both host items and host controls dynamically, although the former is a little bit trickier to pull off. Chapter 14, "Using Windows Forms in VSTO," shows how to add Windows Forms controls dynamically to Word and Excel documents. The Controls CollectionIn a Windows Forms application, every form class has a property called Controls that refers to a collection of all the controls hosted by the form. In VSTO, each worksheet and document class contains a similarly named property. In Word, the document class contains an instance of Microsoft.Office.Tools.Word.ControlCollection. In Excel, each worksheet class contains an instance of Microsoft.Office.Tools.Excel.ControlCollection. These two classes are quite similar; the following sections discuss their differences. Enumerating and Searching the CollectionYou can use the Controls collection to enumerate the set of aggregated controls and perform actions upon all of them. You could disable all the button controls on a sheet or document, for example: For Each control As Object In Me.Controls If TypeOf control Is Button Then Dim button As Button = control button.Enabled = False End If Next The Controls collection also has some of the indexing and searching methods you would expect. Both the Excel and Word flavors have methods with these signatures: Public Function Contains(ByVal control As Object) As Boolean Public Function Contains(ByVal name As String) As Boolean Public Function Index(ByVal control As Object) As Integer Public Function Index(ByVal name As String) As Integer If the collection does not contain the searched-for control, IndexOf returns 1. Both collections can be enumerated via a For Each loop; should you want to enumerate the collection yourself, you can call GetEnumerator. This method returns a ControlCollectionEnumerator object from the Microsoft.Office.Tools.Excel or Microsoft.Office.Tools.Word namespace, as appropriate. They are essentially identical functionally. Both classes have only three public methods:
Current returns Nothing when moved past the final element in the collection; MoveNext moves the enumerator to the next element; and Reset starts the enumerator over at the beginning of the collection. Both collections also expose three index operators, which take a name as a String, an Integer index, and an Object, respectively. The indexers throw an ArgumentOutOfRangeException if there is no such control in the collection. Adding New Word and Excel Host Controls DynamicallyThe worksheet and document Controls collections provide methods to create host controls dynamically. In Word, you can create aggregated bookmarks dynamically: Public Function AddBookmark( _ ByVal range as Microsoft.Office.Interop.Word.Range, _ ByVal name as String) As Microsoft.Office.Tools.Word.Bookmark This method creates a new bookmark on the given range and aggregates it with the VSTO host control class. Note XMLNode and XMLNodes host controls cannot be created dynamically in Word. The XMLMappedRange host control cannot be created dynamically in Excel. In Excel, you can create aggregated NamedRanges, ListObjects, and Chart controls dynamically. Of those, only Chart controls can be positioned at arbitrary coordinates; all the rest must be positioned with a range object: Public Function AddChart( _ ByVal range As Microsoft.Office.Interop.Excel.Range, _ ByVal name As String) As Microsoft.Office.Tools.Excel.Chart Public Function AddChart(ByVal left As Double, _ ByVal top As Double, _ByVal width As Double, _ ByVal height As Double, ByVal name As String) _ As Microsoft.Office.Tools.Excel.Chart Public Function AddNamedRange( _ ByVal range As Microsoft.Office.Interop.Excel.Range, _ ByVal name As String) As _ Microsoft.Office.Tools.Excel.NamedRange Public Function AddListObject( _ ByVal range As Microsoft.Office.Interop.Excel.Range, _ ByVal name As String) _ As Microsoft.Office.Tools.Excel.ListObject Removing ControlsThe host controls added to a worksheet or document host item class at design time are exposed as properties on the host item class. If at runtime the user were to delete one accidentally, save the document, and then reload it, the customization code would be unable to find the aggregated control. This would likely result in an exception because eventually the customization would try to listen to an event or call a method on the missing aggregated control. If the customization detects this condition, it will throw a ControlNotFoundException. Although it is difficult to prevent end users from accidentally or deliberately deleting controls without locking the document, the Controls collection can at least try to prevent programmatic destruction of controls added at design time. There are four equivalent ways to remove controls from the Controls collection; all will throw a CannotRemoveControlException if you attempt to remove a control that was not added dynamically. The four ways to remove a dynamic control are to call Delete() on the control itself or to call Remove(ByVal control As Object), Remove(ByVal name as String), or RemoveAt(ByVal index as Integer) on the Controls collection itself. All four of these methods remove the control from the collection, remove the control from the document or worksheet, and destroy the extender object. Most collections have a Clear() method that removes every member from the collection. Because completely clearing a Controls collection would almost always result in an exception when a design-time control was removed, this method always throws a NotSupportedException and is hidden from IntelliSense. Saving and Loading ControlsWhat happens when you add one or more dynamic controls to a document, save it, and reload it later? Dynamically created Windows Forms controls such as buttons and check boxes do not survive being saved and then loaded. They just disappear; your customization code can create them again afresh the next time the document is loaded. Because "host" controls such as ranges and bookmarks are themselves part of the document, they will be persisted along with the rest of the document. The controls do not save any information about any aggregating objects you may have created around them, however. When the document is reloaded, the controls will still be there, but there will be no aggregates wrapping them. You will have to add the controls back to the Controls collection to create new aggregates for the controls. The Controls collection provides Add methods that can reconnect an aggregate to an existing control in the document without creating a new control in the document.
The Globals Class in ExcelSuppose that you're writing code in the Sheet1 class that needs to set a property on a control hosted by Sheet2. You are probably going to need to obtain the instance of the aggregated Sheet2 class somehow. Instead of aggregating properties representing all the other sheets and the workbook aggregates onto each startup class, VSTO exposes all the sheets and the workbook as static members of the Globals class: Private Sub Sheet1_Startup(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Startup Globals.Sheet2.MyRange.Value = "Hello" End Sub Because at least the first three phases of the startup sequence have finished at this point, you know that the Globals class and Sheet2 have been initialized, although Sheet2's Startup event has probably not fired yet. Notice that by default, controls aggregated onto the worksheet classes are given the Friend visibility modifier. You can change the visibility modifier generated for a control by selecting the control in the designer and then selecting the Modifiers property in the Properties window. If you change the visibility of the control to Private, however, you will be unable to access the control's field from the Globals class. The Globals class is also constructed using partial classes, although by default, there is no visible portion. Rather, each generated code file defines a portion of the Globals class. You can see this code at the bottom of the hidden file for each class. Should you for some reason want to add your own custom members to the Globals class, you can always create your own portion of the partial class. |