8.2 MacrosVS.NET macros are small VB.NET functions that group together one or more actions that manipulate the development environment using the VS.NET automation object model. VS.NET makes it easy to create and use macros in a way that does not interfere with the way you develop your software—macro projects operate entirely independently of VS.NET solutions. Once you have created a macro, you can then make it available on a menu or toolbar for easy access. 8.2.1 Recording and Running a MacroThe easiest way to get started using macros is to use the macro recording functionality built into VS.NET. With macro recording, you use the IDE in the normal way, but VS.NET will record all of the actions you perform and save them in a macro. As an example, consider the common task of changing a project's default HTML layout from Grid to Flow. (See Chapter 2 for information about the HTML designer and layout issues.) Since this is a common but slightly awkward task, it would be nice to have an automated way to set the value to Flow. This is a perfect job for a macro. To record a macro, go to Tools Macros Record TemporaryMacro (Ctrl-Shift-R). Selecting this menu item brings up a small recorder toolbar with three buttons, one to pause recording, one to cancel the recording, and one to stop recording and generate a macro from the recorded operations. After starting the recording, you can just go through the motions of the task you'd like to record. When you have finished, press the Stop Recording button (Ctrl-Shift-R). (In this example, we are changing the project default HTML layout, so we would go to the Project Properties dialog box, go down to the Designer Defaults node, and change the layout. Once finished, we would press the Stop Recording button.)
To execute your newly recorded macro, go to Tools Macros Run TemporaryMacro (Ctrl-Shift-P). Whenever you ask VS.NET to record a macro, it creates a temporary macro called TemporaryMacro to store the results. It will not save this macro unless you tell it to, so each time you record a new temporary macro, you will be destroying the previous one you recorded. To store a recorded macro permanently, use Tools Macros Save TemporaryMacro. This will display the Macro Explorer window, which is shown in Figure 8-3, and will give you an opportunity to rename your macro. (You must rename it in order to save it—merely selecting the Save TemporaryMacro item is not enough.) Figure 8-3. Macro ExplorerThe Macro Explorer lets you see all the macros on your system. (You can display the Macro Explorer using View Other Windows Macro Explorer or with Alt-F8.) To run a macro from the Macro Explorer, you can either double-click it or right-click on it and select Run from the context menu. You can rename and delete macros from this menu. The menu also allows you to edit a macro, which is useful, because even when you create macros by recording them, you will often need to make a few modifications to the generated macro. When you choose to edit a macro, VS.NET will open the macro IDE.
8.2.2 Editing with the Macro IDEThe macro IDE can be invoked via Tools Macros Macros IDE (Alt-F11), or by choosing to edit a macro in the Macro Explorer. The macro IDE looks very much like a trimmed-down version of the VS.NET IDE, as Figure 8-4 shows. Figure 8-4. The macro IDEThe Project Explorer window (which is on the left side of the IDE by default) shows all of the macro projects that VS.NET is currently configured to use. (See the next section, Section 8.2.3 for information on how VS.NET manages the files for these projects.) The editor is the normal VB.NET editor, so editing macros works in exactly the same way as writing VB.NET code in the main IDE. Each macro project contains "files" (although in reality all of the "files" shown are typically contained in a single binary file). When you want to add a new macro, you can either edit an existing code file or add a new one (File Add New Item). When you add a new file, you get three choices: a module, a class, or a source file. The only difference between the three is the declarations VS.NET places in the new file. A module contains a module declaration, a class file contains a class declaration, and the source file option creates an empty file.
8.2.3 Managing Macro FilesVS.NET stores your macros in one or more macro project directories. There is one macro project directory for each item listed under the Macros node in the Macro Explorer (Figure 8-3). These are entirely unrelated to normal VS.NET projects and solutions. By default, macro project directories will be in either a VSMacros or a VSMacros71 directory underneath your My Documents\Visual Studio Projects directory. (You can place macro project directories wherever you like—these are just the default locations.) You will normally find two macro project directories here—MyMacros, which is intended for your own use, and Samples, which contains a set of example macros. By default, VS.NET will put newly recorded macros in the MyMacros project. You can select a different project by right-clicking on the project in the Macro Explorer and selecting Set as Recording Project. Macro project directories typically contain just one file, ProjectName.vsmacro, where ProjectName is the same as the containing directory name. The .vsmacro file is a COM structured storage file that contains all of the source files for the macro project. You can have VS.NET store each of the source files for a project separately, instead of lumping them all into one structured storage file. (This would be a good idea if you wanted to place your macros into a source control system. However, you're on your own if you want to do that—VS.NET offers no integrated support for revision control of macros.) If you select the project in the Macro Explorer, the Properties panel (F4) will show a Storage Format property. By default, this is set to Binary (.vsmacros) but changing it to Text (UNICODE) will cause VS.NET to store the project as a collection of files instead of one single binary file.
Macro projects are not associated with VS.NET projects or solutions. VS.NET stores the list of macro projects in a per-user section of the registry: HKCU\Software\Microsoft\VisualStudio\7.1\vsmacros If you want to share your macro with someone else, you can export one of the individual files by right-clicking on it in the Project Explorer in the macro IDE, and selecting Export Filename.... This will export the macro file as a .vb file. Another developer can then import the macro on her copy of VS.NET using File Add Existing Item, in the macro IDE. Or you can just email someone the text of the macro, and she can add it to her system using cut and paste. 8.2.4 Extending a Recorded MacroAlthough many tasks can be recorded as macros, often you will want to edit a recorded macro to extend its functionality beyond what was initially recorded. For example, you may wish to add looping or conditional execution into your macro. Also, it is not uncommon for macro recording to miss steps—some actions, such as typing data into a dialog box, are not recordable,—so recorded macros often require a little tweaking. Example 8-10 shows the macro that we recorded earlier to change a project's default HTML designer layout property from Grid to Flow. It is typical of recorded macros, in that it needs a little work before it will be useful. Example 8-10. A recorded macroOption Strict Off Option Explicit Off Imports EnvDTE Imports System.Diagnostics Public Module RecordingModule Sub TemporaryMacro( ) DTE.Windows.Item(Constants.vsWindowKindSolutionExplorer).Activate( ) DTE.ActiveWindow.Object.GetItem("NSChange\NSChange").Select( _ vsUISelectionType.vsUISelectionTypeSelect) DTE.Commands.Raise("{5EFC7975-14BC-11CF-9B2B-00AA00573819}", 397, _ Customin, Customout) DTE.Windows.Item(Constants.vsWindowKindSolutionExplorer).Activate( ) End Sub End Module The first problem with this macro that it is not very general purpose—it selects a particular project ("NSChange\NSChange"). Moreover, the part of the macro that does the actual work is hard to decipher: the DTE.Commands.Raise call is a generic method for invoking commands, and anybody who wanted to work out what this macro does by looking at it would have a hard time interpreting the command's GUID and ID. (See the sidebar, Interpreting Command GUIDs and IDs for notes on how to do this.) But worst of all, the macro didn't record the actual change we were trying to make in the properties—it just activated the properties dialog window. (This illustrates the problem with that impenetrable Raise method—it is wholly unobvious that the command being invoked happens to be the one that opens the Project Properties dialog.)
In all, this recorded macro is not very helpful. The success you will have with recorded macros depends on what you are trying to do. In general, they don't work at all well for anything involving dialogs. For most other kinds of user interface activity, they fare rather better though. The best approach when using macro recording is usually to use the recorded macro as a starting point for a new macro. Your final macro will probably look quite different, but the recorded macro may provide a quick path to learning how the object model works for a particular action. 8.2.4.1 Setting project propertiesSo how do we fix the rather pointless macro in Example 8-10? The macro recorder leaves us in the lurch when it comes to project properties. To fix the code, we must use the Project object's Properties property, as we did in Example 8-2. This is a collection of Property objects that represent the project properties. The exact set of properties that you will find in the Properties collection will depend on the project type. However, it is straightforward to write code that just ignores projects that do not have the property you are looking for. (As mentioned in Section 8.1.1.2 earlier, the VS.NET documentation describes the set of properties available for each object that supports a Properties collection.) In our case, we are looking for the property called DefaultHTMLPageLayout. The code in Example 8-11 iterates through all of the projects currently selected in the Solution Explorer and looks for that property. When it finds it, it sets it to Flow layout. Example 8-11. Setting the default HTML layoutImports EnvDTE Imports VSLangProj Public Module FlowModule Public Sub FlowLayout( ) Dim proj As Project For Each proj In DTE.ActiveSolutionProjects Dim prop As [Property] For Each prop In proj.Properties If prop.Name = "DefaultHTMLPageLayout" Then prop.Value = prjHTMLPageLayout.prjHTMLPageLayoutFlow End If Next Next End Sub End Module This code looks nothing like the code that the macro recorder generated for us. (It also behaves nothing like it—this code actually does what it is supposed to, unlike the recorded code.) Since we know that the macro recorder often doesn't do a good job of recording the setting of properties in dialogs, in retrospect this was a bad choice for the macro recorder—we would have done better to have started out from scratch with a custom macro. 8.2.5 Building a Custom MacroYou are not required to use a recorded macro as the starting point for all of your macros. After all, the macro recorder just ends up generating code that you could have written yourself. Sometimes it will be simpler to start from scratch. We will now work through the creation of an example custom macro that could not reasonably have been created with the macro recorder: it will transfer the contents of the TaskList to a web page. Visual Studio .NET provides a TaskList that can keep track of outstanding development chores (see 'TaskList Comments' in Chapter 2). Imagine a situation in which your team runs a daily build and you would like to make the resulting TaskList available in a web page so that management and other members of your team could see the remaining tasks. In this section, we will develop a custom macro that does just that. Our macro will read the contents of the TaskList into a DataSet. It will then write the DataSet to disk as XML in a location accessible to the web page. The web page will load the XML back into another DataSet and bind it to a DataGrid control in order to present the results. Example 8-12 shows the code for our macro. This example shows an entire source file, including all necessary Import statements, so you will need to add a new file to one of your macro projects if you plan to try this code out. Call the new file BuildCommentDataSet. Since this code uses the ADO.NET DataSet class, you will also need to add references to the System.Data.dll and System.Xml.dll components in your macro project. Example 8-12. Example custom macroImports EnvDTE Imports System.Data Public Module BuildCommentDataSet Public Sub Build( ) Dim tl As TaskList Dim ti As TaskItem " Ask VS.NET for the Task List"s Window object Dim win As Window = _ DTE.Windows.Item(Constants.vsWindowKindTaskList) ' Get the TaskList object associated with the Window tl = win.Object ' Create a new DataSet and DataTable ' for holding the data ' Dim ds As New DataSet("SolutionBuildDataSet") Dim dt As New DataTable( _ DTE.Solution.Properties.Item("Name").Value.ToString( ) _ & "Tasks") ' Need a column for each interesting property ' dt.Columns.Add(New DataColumn("Category", GetType(String))) dt.Columns.Add(New DataColumn("Priority", GetType(String))) dt.Columns.Add(New DataColumn("Description", GetType(String))) dt.Columns.Add(New DataColumn("File", GetType(String))) dt.Columns.Add(New DataColumn("Line", GetType(String))) ' Add each task to the table ' Dim dr As DataRow For Each ti In tl.TaskItems dr = dt.NewRow( ) dr.Item("Category") = ti.Category dr.Item("Priority") = _ ti.Priority.ToString( ).Replace("vsTaskPriority", "") dr.Item("Description") = ti.Description dr.Item("File") = ti.FileName dr.Item("Line") = ti.Line.ToString( ) dt.Rows.Add(dr) Next ' Add the DataTable to the DataSet ' ds.Tables.Add(dt) ' save the DataSet as an XML document ' ds.WriteXml("c:\inetpub\wwwroot\tasklist.xml") End Sub End Module With this DataSet generation in place, building the ASP.NET page to display the data is quick and easy. Here is code in the .aspx file: <%@ Page language="c#" Codebehind="SolutionTasks.aspx.cs" Inherits="Automate.SolutionTasks" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <HTML><HEAD></HEAD> <body> <form id="SolutionTasks" method="post" runat="server"> <asp:DataGrid id="DataGrid1" runat="server" BorderColor="#3366CC" BorderStyle="None" CellPadding="4"> <HeaderStyle Font-Bold="True" ForeColor="#CCCCFF" BackColor="#003399"></HeaderStyle> </asp:DataGrid> </form> </body> </HTML> If you are just copying these files into a web directory rather than adding them to a VS.NET web project, you will need to change the Codebehind attribute to an Src attribute, in order to get ASP.NET to compile the codebehind file. Here is the codebehind file: using System; using System.Data; using System.Web.UI; using System.Web.UI.WebControls; namespace Automate { public class SolutionTasks : System.Web.UI.Page { protected DataGrid DataGrid1; private void Page_Load(object sender, System.EventArgs e) { DataSet ds = new DataSet( ); ds.ReadXml(MapPath("tasklist.xml")); DataView dv = new DataView(ds.Tables[0]); dv.Sort = "Priority, Category, File, Line DESC"; DataGrid1.DataSource = dv; DataBind( ); } } } You can see the result in Figure 8-5. Figure 8-5. Tasklist displayed in an ASP.NET page8.2.6 Handling Events in MacrosAs described earlier in the section entitled Section 8.1.1.2, the VS.NET automation object model provides objects that raise events. Each category of events (e.g., build events, debugging events, text editor events) has a corresponding event source object. Writing macros that get called when these events are raised is very easy. Whenever you create a new macro project, the macro IDE adds a module called EnvironmentEvents. The sole purpose of this module is to let you handle events raised by the IDE. If you open this file and click on the drop-down list at the top left of the editor window, you will see a list of event sources—BuildEvents, DebuggerEvents, DocumentEvents, and so forth. If you select one of these, the drop-down list at the top right will be populated with a list of events. If you select one of these, the IDE will add an event handler for you. Example 8-13 shows a typical event handler. It handles the OnBuildDone event from the BuildEvents object. This example will display a message box every time a build completes. Example 8-13. Handling a build event in a macroPrivate Sub BuildEvents_OnBuildDone(ByVal Scope As EnvDTE.vsBuildScope, _ ByVal Action As EnvDTE.vsBuildAction) _ Handles BuildEvents.OnBuildDone MsgBox("Build complete!") End Sub
8.2.7 DebuggingMacros are debugged in much the same way as regular code. (See Chapter 3 for more information on VS.NET's debugging facilities.) The main difference is that the debugging occurs in the macro IDE, not in the main IDE. The main IDE becomes inaccessible when you are debugging a macro. 8.2.8 LimitationsMacros provide a powerful way to automate and customize the IDE, but they do have certain limitations. For example, you cannot invoke a macro as part of a command-line-based automated build, because VS.NET will display the IDE when it runs the macro. Here are some other limitations on macros:
Our TaskList DataSet macro would be much more useful if we could arrange for the DataSet to be created after a solution is built without user intervention. But we would need some way of allowing the user to configure which solutions require a DataSet to be generated and where each solution should write the XML file. This kind of configurability is difficult to achieve with a macro, because macros cannot display user interfaces. Fortunately, we can solve this problem by writing an add-in instead of a macro. |