Adding Controls at RuntimeSo far this chapter has described how to add controls to the document or worksheet at design time with the Visual Studio control toolbox. Often the controls needed for your application need to be added (and deleted) at runtime. For example, consider the worksheet in Figure 14-1 again. Suppose you want to provide a trade button at the end of every row that shows a stock. This would be impossible to achieve by adding buttons at design time because the number of stock rows will vary at runtime as the workbook is edited. You would need to add a button to the end of the row dynamically as stock is added at runtime. VSTO provides a mechanism to add controls at runtime via the Controls collection present on Word's Document class and Excel's Worksheet classes. This Controls collection works a bit differently than the Controls collection in Windows Forms. In the Controls collection associated with a Windows Forms form class, you can add controls at runtime by creating an instance of the control and adding it to the form's collection of controls. You can then set positioning on the control you created: System.Windows.Forms.Button btn = new System.Windows.Forms.Button(); form1.Controls.Add(btn); btn.Left = 100; The VSTO Controls collection cannot take this approach because although the instance of the button could be added to the collection, there would be no way for the developer to change any positional properties on it because these are not available until the ActiveX control is created and connected to the Windows Forms control. There needs to be a way to return to the developer a wrapped control that has both the original control and the OLEObject or OLEControl. The VSTO Controls collection provides two mechanisms for adding controls:
Listing 14-5 shows code that dynamically adds a group box to an Excel worksheet using the AddControl mechanism. It doesn't even use the returned OLEObject because it sets the position as part of the initial call to AddControl. It then goes further and adds additional RadioButton controls to that group box. Listing 14-5. A VSTO Excel Customization That Adds a Group Box to an Excel Worksheet at Runtimeusing 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 ExcelWorkbook1 { public partial class Sheet1 { System.Windows.Forms.GroupBox myGroupBox; private void Sheet1_Startup(object sender, EventArgs e) { myGroupBox = new System.Windows.Forms.GroupBox(); // Add the group box to the controls collection on the sheet this.Controls.AddControl( myGroupBox, 100, 100, 150, 100, "groupbox"); // Set the title of the group box myGroupBox.Text = "Insurance type"; // Add the radio buttons to the groupbox myGroupBox.Controls.Add(new RadioButton()); myGroupBox.Controls.Add(new RadioButton()); // Set the text of the radio buttons myGroupBox.Controls[0].Text = "Life"; myGroupBox.Controls[1].Text = "Term"; // Arrange the radio buttons in the group box myGroupBox.Controls[0].Top = myGroupBox.Top + 25; myGroupBox.Controls[1].Top = myGroupBox.Controls[0].Bottom + 20; // iterate through each button in the controls collection foreach (RadioButton rb in myGroupBox.Controls) { rb.Left = myGroupBox.Left + 10; } } #region VSTO Designer generated code private void InternalStartup() { this.Startup += new System.EventHandler(this.Sheet1_Startup); } #endregion } } Working with the Controls CollectionThe Controls collection provides a simple mechanism to add controls to your document or worksheet at runtime. Before we get into the details of the Controls collection, it is important to note that the implementation and methods exposed are different between Word and Excel. Although the behavior of the collection is the same in each application, it was necessary to have a different implementation to ensure that the collection takes advantage of the host application. For example, if you want to add a control to Excel, passing in an Excel.Range object for its position makes a lot of sense. If you want to add a control to Word, passing in a Word.Range object makes sense. To illustrate using the collection, we start by looking at the helper methods available for all the supported Windows Forms controls that ship with the .NET Framework. The helper methods follow a common design pattern; call the method with positional arguments and an identifier and the method returns you the wrapped type for the control. Word has two overloads for each helper method:
Excel also has two overloads for each helper method:
After the control has been added to the document or worksheet, you can program against it just as you do a control added at design time. Table 14-3 shows the complete list of helper methods to add controls on the Controls collection.
AddControlUnfortunately, helper methods are not available for every control on your machine, so there needs to be a way to add controls outside the list in Table 14-3. To do this, the Controls collection provides an AddControl method that enables you to pass in an instance of any Windows Forms control, and it will return the OLEObject (for Excel) or the OLEControl (for Word) that can be used to position the control after it is added. // Declare a OLEObject variable Microsoft.Office.Interop.Excel.OLEObject myobj; // Add the control to the A10 cell myobj = Controls.AddControl(new UserControl1(), this.Range["A10", missing], "DynamicUserControl"); // Reposition it to the top of B15 myobj.Top = (double)this.Range["B15", missing].Top; A common pitfall of using AddControl is forgetting to set the positioning on the OLEObject and setting it directly on the Windows Forms control itself. If you do this, the control will change its position relative to the container rather than move its position correctly in the document. For an example of this issue, consider Listing 14-3 and Figure 14-13. Deleting Controls at RuntimeNow that we have some controls added to the document at runtime, it is important that there be a mechanism to delete controls from the collection. VSTO provides three ways to achieve this:
Why Are Controls Added at Runtime Not Saved in the Document?We wanted to keep the behavior of the Controls collection as close to the Windows Forms development experience so that any control added at runtime is deleted from the document when the user saves the document. For example, if you add controls to a Windows Forms application at runtime, you do not expect those controls to just appear the next time you run the application without code being written to re-create those controls. We spent many hours debating the relative merits of this approach over the alternative, which was to allow Word or Excel to save the newly added control when the document was saved. The main deciding argument for not saving the newly added control was to make it easier to write dynamic control code in the document. If we had left the control in the document when the user saved the document, it would have been very difficult to write code that could hook up controls that had been added dynamically the last time the document was open. To understand why this was difficult really involves looking into how a control is added to the document at runtime. When a control is added to the Controls collection, the VSTO runtime adds an instance of the ActiveX control that will host the control and then sets it to host the provided control. This works fine when the document is running but quickly becomes complicated when the user saves the document. If we were to save the control into the document, all that would be stored would be the ActiveX control itself but without any instance of the Windows Forms control because it must be provided by the code at runtime. The next time the document loaded up, the ActiveX control would load but would not get an instance of the control because the code that added the instance of the Windows Forms control would run again and add a new instance of the ActiveX control because it would have no link back to the saved ActiveX control. Extrapolate this situation out over a few hundred saves of a document and you quickly get a lot of "orphaned" ActiveX controls that will never be used. The solution that was implemented in VSTO was to remove all ActiveX control instances that were added as a result of adding a control at runtime to the Controls collection. This way there will never be any "orphaned" ActiveX controls on the document, and it also makes your code simpler to write. Why is the code simpler to write? Imagine writing the code to add the buttons at the end of each row containing a stock: foreach (StockRow stock in Stocks) { // add stock information to row here this.Controls.AddButton( this.Range[currentrow, "12"], stock.Ticker + "btn"); } If the control was persisted with the worksheet on save, the code would have to go through each control and ensure the buttons added in the last run were there, and quite possibly delete and add them again since the stock list changed. We believed it was more straightforward to just iterate through the stocks on every run of the workbook and add the buttons. Why Are Controls in the Controls Collection Typed as Object Instead of Control?VSTO documents and worksheets can have Windows Forms controls added to them at runtime via the Controls collection as well as host controls such as NamedRange and ListObject. Both these types of controls act like controls in the VSTO model. For example, you can click a NamedRange in VSTO and display a property window for it. You can establish data bindings to a NamedRange just as you can with a text box or any other Windows Forms control. As a result, the VSTO model considers both NamedRange and a Windows Forms control to be a "control" associated with the worksheet or document. The Controls collection contains both host controls and Windows Forms controls. Although providing a strongly typed collection was something that we would have liked to do, there was no common type other than object that a host control and a Windows Forms control share. |