Typically, the implementation details of a particular technology are interesting to know but not a prerequisite for using a feature. In the case of Windows Forms control hosting on an Office document, it is important to understand how the feature is implemented because you will be exposed to some implementation details as you create solutions using controls.
The Windows Forms Control Host ActiveX Control
Windows Forms control support in Office 2003 and VSTO is based on the capability of Word and Excel to host ActiveX controls on the document surface. When you add a Windows Forms control to a document, what actually is added is an ActiveX control called the Windows Forms control host. The Windows Forms control host acts as a host for each Windows Forms control added to the document. The Office application thinks that it is just hosting a basic ActiveX control because the Windows Forms control host implements all of the necessary ActiveX control interfaces.
When the customization assembly is loaded for the document or spreadsheet, the actual Windows Forms control instance is created in the same application domain and security context as the rest of the customization code. These Windows Forms control instances are then parented by a special parent Windows Forms control called the VSTOContainerControl that derives from UserControl. The VSTOContainerControl is then sited to the Windows Forms control host ActiveX control. Your controlfor example, a Trade Stock button in a spreadsheetis added as a child of the VSTOContainerControl. Figure 14-11 shows this "sandwich" architecture.
Figure 14-11. The basic hosting architecture for Windows Forms controls on the document.
The fact that an ActiveX control is hosting the Windows Forms control on the document surface does peek through at times. One example is in the Excel design view. When you click a managed control that you have added to the Excel workbook surface, the formula bar shows that it is hosted by an embedded ActiveX control with ProgID "WinForms.Control.Host", as shown in Figure 14-12.
Figure 14-12. Excel shows the ProgID of the underlying ActiveX hosting control.
Why Are VSTO Controls Derived from Windows Forms Controls?
The fact that an ActiveX control is hosting the Windows Forms control dragged onto the document surface does not show up immediately in your code. VSTO adds a member variable to the ThisDocument or Sheet1 class named something like Button1 that you can code against just as you would if you were working with a traditional Windows Forms form. At first glance, the experience appears to be identical to working with a Windows Forms form, but the type of the control that you added to the document is not quite what you would expect. If you drag a button from the Windows Forms toolbox, it would be natural to expect the type of the button created on the document to be System.Windows.Forms.Button. However, when you add a button to a spreadsheet, VSTO creates a button with type Microsoft.Off-ice.Tools.Excel.Controls.Button that derives from System.Windows.Forms.Button. When you add a button to a Word document, VSTO creates a button with type Microsoft.Office.Tools.Word.Controls.Button that derives from System.Windows.Forms.Button. Understanding why a button in VSTO derives from the standard Windows Forms button requires some further digging into the details of how Windows Forms controls are hosted in a Word or Excel document.
Windows Forms controls, be it a control in the System.Windows.Forms namespace or a custom control written by a third party or you, were originally designed to be added to a Windows Forms form and not an Office document. Luckily, much of the Windows Forms control works just fine when used in an Office document. The main special case is around the positioning of the control. If you set the Left property of a Windows Forms control hosted in a form, it sets the distance in pixels between the left edge of the control and the left edge of its container's client area. This works fine in a form or a container control but does not work well when the control is placed in a document or spreadsheet.
The reason it does not work well is directly related to the hosting architecture of controls in the document, because the control is actually hosted by the VSTOContainerControl, which is hosted by the ActiveX control. As a result, if VSTO was to expose the raw positioning properties of the control, they would be relative to the area of the VSTOContainerControl container not the document. Setting the Left property of a control should actually move the ActiveX control within the document rather than the hosted Windows Forms control within the VSTOContainerControl.
Listing 14-3 illustrates this point. In Listing 14-3, we have a spreadsheet that we have added some Windows Forms buttons to, as shown in Figure 14-1. The Refresh button shown in Figure 14-1 is added to Sheet1 as a member variable called refreshButton of type Microsoft.Office.Tools.Excel.Controls.Button. We display that type in the Startup event. As mentioned earlier, Microsoft.Office.Tools.Excel.Controls.Button derives from System.Windows.Forms.Button. TheMicrosoft.Office.Tools.Excel.Controls.Button's override of Left sets the position of the ActiveX control hosting the Windows Forms control. The code in Listing 14-3 sets this Left to 0, which causes the control to move to the left edge of the worksheet. Casting refreshButton to a System.Windows.Forms.Button strips the override that VSTO adds for the Left property. Setting the Left property on refreshButton when cast to a System.Windows.Forms.Button sets the Left property of the control relative to the parent VSTOContainerControl. This listing when run gives the strange result in Figure 14-13, where the first call to Left moved the ActiveX control to the far-left edge of the worksheet but the subsequent calls to Left and Top on the base class System.Windows.Forms.Button moved the managed control relative to the VSTOContainerControl.
Listing 14-3. A VSTO Excel Customization That Exposes the Windows Forms Control Hosting Architecture
using System; using System.Data; using System.Drawing; using System.Windows.Forms; using Microsoft.VisualStudio.OfficeTools.Interop.Runtime; using Excel = Microsoft.Office.Interop.Excel; using Office = Microsoft.Office.Core; namespace ExcelApplication7 { public partial class Sheet1 { private void Sheet1_Startup(object sender, EventArgs e) { MessageBox.Show(refreshButton.GetType().ToString()); // Cast to a System.Windows.Forms.Button // to set position on underived control System.Windows.Forms.Button refreshButtonBase = refreshButton as System.Windows.Forms.Button; MessageBox.Show(refreshButtonBase.Parent.GetType().ToString()); MessageBox.Show(refreshButtonBase.Parent.GetType(). BaseType.ToString()); // Moving the control on Microsoft.Office.Tools.Button refreshButton.Left = 0; // Moving the control again on the base // System.Windows.Forms.Button refreshButtonBase.Left = 10; refreshButtonBase.Top = 10; } #region VSTO Designer generated code private void InternalStartup() { this.Startup += new System.EventHandler(this.Sheet1_Startup); } #endregion } }
Figure 14-13. The result of running Listing 14-3the Refresh button has been offset relative to the VSTOContainerControl in the VSTO hosting architecture.
To enable your code to set the position of the control relative to the document, VSTO creates a derived class for each control that extends the class of the original Windows Forms control and overrides the positional information with the positional information from the ActiveX control in the Office document. The object model object for Excel that provides the properties and methods to position the ActiveX control is called OLEObject, and for Word it is called OLEControl. The derived classes created for each VSTO Windows Forms control effectively merges together the original Windows Forms control class and the OLEObject object for Excel or the OLEControl object for Word.
If you create a Windows Forms control of your own or use a third-party control, when you drag and drop the control to a document or spreadsheet, VSTO automatically generates an extended class for you that merges your control with OLEObject or OLEControl. Because the ability to add custom Windows Forms controls onto a document requires the control to be extended, you can only use controls that are not sealed. The good news is that the vast majority of third-party controls are unsealed.
Security Implications of the VSTO Control Hosting Model
The security minded might be wondering about the implications of having to use an ActiveX control to host managed controls added to a document. This is something that we spent considerable time on to ensure that the ActiveX control did not provide a vulnerability to Office. The Windows Forms control host ActiveX control when initialized does not actually do anything and will not run any code until it is accessed by the customization assembly. This means that the control is safe for initialization, and the only way for it to do anything is for code with full trust (the customization) to call it. The control is marked safe for initialization to ensure that it will load in Office with the default security settings.
One strange side effect of our control hosting architecture is that Office requires Visual Basic for Applications (VBA) to be installed in order to add ActiveX controls to a document. Adding ActiveX controls to a document does not add VBA code to that document, but it does require the use of parts of the VBA engine. You therefore need to ensure that your Office installation has VBA installed to use managed controls in the document. VBA is installed by default in all versions of Office, so it is unusual for it not to be installed. VSTO also requires that the Trust access to Visual Basic Project check box be checked in the Macro security dialog box of Office on a development machine. This check box does not have to be checked on end-user machines unless you are adding dynamic worksheets at runtime as described in Chapter 13.
The macro security level in VBA can affect the loading of ActiveX controls and hence managed controls. If your user sets the VBA macro security settings to Very High (it is set to High by default), any ActiveX controls in the document will only be allowed to load in their inactive design mode state. In this state, Windows Forms controls in the document will not function properly. Luckily, the default macro security setting of High allows controls to be loaded assuming they are safe for initialization. Because all Windows Forms controls in the document are loaded by the Windows Forms control host ActiveX control, which is marked as safe for initialization, all managed controls can load in the High setting.
Limitations of the Control Hosting Model
Each Windows Forms control on the document is contained by an instance of the Windows Forms control host ActiveX control, which leads to some limitations. The most noticeable limitation that affects all controls is the lack of support for a control's TabIndex property. Tab order in a Windows Forms is determined by the containing form or control. This is not a problem with a traditional Windows Forms form because all controls on the form are contained by one container. In VSTO, each control placed onto a document or spreadsheet is contained by it is own containerby it is own unique instance of the Windows Forms control host. The net result of this is that the tab index of the control is scoped to its container and because there is a one-to-one relationship between control and container, the TabIndex property is of little use. This can have impact on the accessibility of your application because users would expect to be able to tab between fields, but nothing happens when they press the Tab key.
Another limitation is that controls such as radio buttons really require the control be contained within a container to make the controls mutually exclusive so that only one radio button within the container can be selected at a time. Without a common container, the radio button is not particularly useful. Adding each radio button directly onto a document or spreadsheet causes each radio button to be hosted in its own container. There is a simple way to work around this problem, however; you just create a user control that has a container (a group box, for example), and then add the radio buttons to the group box within the user control. The user control can then be added as a single control to the document.
Control State Is Not Saved in the Document
We already considered this limitation briefly in the introduction of this chapterthe limitation that the state of a Windows Forms control is not saved in the document. To illustrate, imagine a solution that generates customer service letters in Word. One of the key pieces of information in the document is the date the customer first called customer service. To aid with entering this date, the Word document contains a DateTimePicker, as shown in Figure 14-14.
Figure 14-14. A DateTimePicker control in a Word document.
This is great functionality for your users, but where will the date that the user picks with the DateTimePicker be stored in the document? For example, consider the scenario where the user opens the document for the first time. The DateTimePicker defaults to show today's date. The user then picks a different date using the DateTimePicker and saves the document. Now we have a problem: Windows Forms controls placed in a document do not save their state into the document when the document is saved. The next time the document is opened, the DateTimePicker will just show today's date again rather than the date picked by the user the last time the document was saved.
To get the DateTimePicker to remember the date picked by the user the last time the document was saved, you have to write code to detect when the user picks a new date by handling the DateTimePicker control's ValueChanged event. You need to store the date in the document somehow so that it will be saved when the document is saved. Some options you have for storing the date that was picked include inserting some hidden text in the document, adding a custom property to the document, or using the cached data feature of VSTO to cache the date in the data island of the document. Then you have to write some code in the Startup event handler to set DateTimePicker.Value to the saved date.
Listing 14-4 shows some VSTO code associated with the Word document shown in Figure 14-14. The code uses the cached data feature of VSTO described in Chapter 18, "Server Data Scenarios," to save the date that was picked in the DateTimePicker in a public field called lastPickedDate that has been marked with the Cached attribute. The Cached attribute causes the value of lastPickedDate to be automatically saved in a data island in the document from session to session. The Startup handler puts the stored value of lastPickedDate back in the DateTimePicker each time the document is reopened.
Listing 14-4. A VSTO Word Customization That Saves the Date That Was Picked Using the Cached Data Feature of VSTO
public partial class ThisDocument { [Cached()] public DateTime lastPickedDate = DateTime.MinValue; private void ThisDocument_Startup(object sender, System.EventArgs e) { if (lastPickedDate != DateTime.MinValue) { this.dateTimePicker1.Value = lastPickedDate; } this.dateTimePicker1.ValueChanged += new EventHandler( DateTimePicker1_ValueChanged); } void DateTimePicker1_ValueChanged(object sender, EventArgs e) { lastPickedDate = dateTimePicker1.Value; } #region VSTO Designer generated code private void InternalStartup() { this.Startup += new System.EventHandler(ThisDocument_Startup); } #endregion }
Why Are Controls Sometimes Slightly Blurry?
Have you noticed how sometimes a control in Word or Excel looks a little blurred when you are in the designer, but that it snaps back into focus when you run the project? This is because the Windows Forms control host ActiveX control stores a bitmap of the hosted Windows Forms control so that when Excel or Word first opens the document it can display the bitmap until the actual control is loaded. This was done because the actual control is not loaded until the customization assembly is fully loaded. If we did not do this, the control would have an attractive red x through it until the customization assembly loaded.
The reason it looks a bit out of focus is because Office anti-aliases the image when it stores it so it is not an exact copy of the original bitmap. So if you see a slightly out-of-focus control on your document, you know that your customization assembly has not loaded yet, did not load properly, or you have been up too late writing a book about Windows Forms controls on Office documents!
Part One. An Introduction to VSTO
An Introduction to Office Programming
Introduction to Office Solutions
Part Two. Office Programming in .NET
Programming Excel
Working with Excel Events
Working with Excel Objects
Programming Word
Working with Word Events
Working with Word Objects
Programming Outlook
Working with Outlook Events
Working with Outlook Objects
Introduction to InfoPath
Part Three. Office Programming in VSTO
The VSTO Programming Model
Using Windows Forms in VSTO
Working with Actions Pane
Working with Smart Tags in VSTO
VSTO Data Programming
Server Data Scenarios
.NET Code Security
Deployment
Part Four. Advanced Office Programming
Working with XML in Excel
Working with XML in Word
Developing COM Add-Ins for Word and Excel
Creating Outlook Add-Ins with VSTO