Team LiB   Previous Section   Next Section

8.2 Macros

VS.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 Macro

The 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.)

While you are recording a macro, VS.NET will still perform all of the actions you tell it to as well as recording them. So be careful if you are recording a sequence of operations that involves deletion—VS.NET really will delete whatever you tell it to even while recording a macro.

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 Explorer
figs/mvs_0803.gif

The 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.

You will often need to edit a recorded macro—as we shall see later, the macro we recorded for changing the HTML flow settings will need to be edited before it is useful.

8.2.2 Editing with the Macro IDE

The 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 IDE
figs/mvs_0804.gif

The 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.

Because macros are simply VB.NET code that gets compiled and run, if you make a change to a macro that causes it not to compile, you will not be able to use any of the macros in that project. This should not be surprising—normal VS.NET projects are much the same in that the whole project must compile without errors before it can run. However, this is a change from how Microsoft's older macro systems used to work—they were based on script code rather than compiled code, which meant that macros would run in the presence of syntax errors so long as you didn't attempt to run the erroneous lines. Unfortunately, compiled code cannot offer this degree of latitude. This is not necessarily a disadvantage—it means that you get to find out about problems sooner rather than later. (And not only does compiled code offer much better compile-time type checking, it will also run faster once compiled.)

8.2.3 Managing Macro Files

VS.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.

When you change the storage format of a macro project, the format you select becomes the default format for any new macro projects that you create.

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 Macro

Although 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 macro
Option 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.)

Interpreting Command GUIDs and IDs

You may sometimes find yourself needing to work out what a command in a recorded macro actually does from the GUID and ID alone. The best way to deal with this is to write an experimental macro and single-step through it in the macro IDE, in order to observe the behavior. You can examine commands with the following code:

Public Sub DumpCommand(cmdGuid As String, _
                       cmdId As Integer)
    Dim cmd As Command
    cmd = DTE.Commands.Item(cmdGuid, cmdId)
    Debug.WriteLine(cmd.Name)
    Dim binding As Object
    For Each binding In cmd.Bindings
        Debug.WriteLine(binding)
    Next
End Sub

You would call this method with the GUID and ID of the command you are trying to decipher. In Example 8-10, these are "{5EFC7975-14BC-11CF-9B2B-00AA00573819}" and 397, respectively.)

If you single-step through this code in the macro IDE, it will print out the command's name and any key bindings to the Output window. (To show the Output window, use View Other Windows Command Window or Ctrl-Alt-A. This window will show any text that you print with Debug.WriteLine.) In this particular case, the command turned out not to have a name, which was not very helpful. Fortunately, this code revealed a key binding to "Alt+Enter". This just happens to be the shortcut for bringing up the properties window, thus showing what the command really does.

Of course, the other way of interpreting a command GUID and ID is just to execute the command and see what happens. However, this is potentially risky—some commands are destructive, and you may end up deleting something. Do you feel lucky?

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 properties

So 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 layout
Imports 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 Macro

You 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 macro
Imports 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 page
figs/mvs_0805.gif

8.2.6 Handling Events in Macros

As 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 macro
Private Sub BuildEvents_OnBuildDone(ByVal Scope As EnvDTE.vsBuildScope, _
        ByVal Action As EnvDTE.vsBuildAction) _
        Handles BuildEvents.OnBuildDone
   
    MsgBox("Build complete!")
   
End Sub

Because macros are stored per-user and are not associated with any particular project or solution, this macro will be run any time any solution is built. You should, therefore, exercise caution when writing an event-handling macro—it will be run whenever the selected event is raised, regardless of context.

8.2.7 Debugging

Macros 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 Limitations

Macros 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:

  • Cannot create custom property pages for the Options dialog box on the Tools menu

  • Cannot create custom tool windows

  • Cannot dynamically enable and disable items on menus and toolbars

  • Cannot add contact and descriptive information to the Visual Studio .NET Help About box

  • Cannot build user interfaces for 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.

    Team LiB   Previous Section   Next Section