Enhancing the Smart Desktop Add-in

 < Free Open Study > 



Debugging Macros

When errors occur in your macros, you can debug the code in the Macros IDE by setting breakpoints, watch variables, and so forth. By now, you should be familiar with debugging procedures in .NET, but if you aren't, you can see details about debugging in the MSDN topic "Debugging".

Tip 

The default keys for stepping through the debugger are F10 for Step Over and F11 for Step Into. I had to be reminded of this the first time I tried using F8 to step through a macro. Also, the reason that I was expecting F8 to step is that I have set my keystrokes to use the Microsoft Visual Basic Keystroke option in the Options dialog box. I have to confess that because I have been using such keys as F8 for debugging, I have a hard time moving to F11 and F10.You can set the keyboard to respond to VB 6.0 shortcut keys by selecting Tools Options and then selecting the Keyboard option. Click the Keyboard Mapping Scheme combo box and select Visual Basic 6. This will make the shortcut keys the same as they were in VB 6.0.

To catch runtime errors in the Macros IDE and to be able to navigate to lines that contain errors, you must either run with debugging enabled, which requires starting the macro by pressing F5, or choose Start on the Debug menu. If you start the macro by using Ctrl-F5 or by choosing Start Without Debugging on the Debug menu, runtime errors will be ignored.

Whether or not debugging is enabled depends on how a macro is executed. If you click inside a macro and press F5 or choose Start on the Debug menu, the macro is run with debugging enabled. If you click inside a macro and press Ctrl-F5 or choose Start Without Debugging on the Debug menu, the macro is run without debugging enabled. If the insertion point is not inside a macro and you press F5 or choose Start on the Debug menu, the Macros IDE goes into Run mode with debugging enabled. Additionally, if you set a breakpoint anywhere in your macro code, debugging is automatically enabled.

I now want to show you how to create a fairly complex macro that will provide a great improvement on a procedure you created in an earlier chapter. That procedure was designed to capture a block of code that the user has selected. The procedure worked fine, except that sometimes the user wants to use an add-in feature that requires a whole procedure to be selected. In this case, the user was required to select the whole proc and was responsible for doing it correctly prior to invoking an add-in feature such as CloneProcedure. This was not only an extra burden for the user, but it was also a place where errors could occur if the user was not careful in selecting every line of the procedure.

Therefore, I needed to develop a procedure for automatically picking up the whole procedure when the user simply places the cursor within the procedure but does not select anything. I have this kind of functionality in VBCommander in VB 6.0. The problem is that VB 6.0 provides high-level methods for facilitating such functionality. My challenge was to find the objects that could accomplish the same functionality in VB .NET, where the classes and objects are not as intuitive as was VB 6.0.

Because my ultimate goal was to produce a debugged procedure for use in an add-in, I elected to write the procedure as a Function that will return the string containing the whole procedure. In the Macro Explorer, Functions do not show up in the listing of macro commands that can be executed.

Note 

The fact that a macro Function does not show in the Macro Explorer does not mean that you cannot debug Functions in the Macros IDE. Rather, it means that they cannot be called directly from the Macro Explorer. The Macro Explorer only lists Sub commands. Therefore, you debug a macro Function by calling it from a macro Sub.

Because macro Functions are not listed or executable from the Macro Explorer, I created a test Sub macro command for calling the macro Function that I want to develop. This Sub macro command is shown in Listing 8-6. Its sole reason for existence is to provide a callable interface to the GetWholeProc function.

Listing 8-6: Testing GetWholeProc

start example
 Sub TestGetWholeProc()     Dim s As String     s = GetWholeProc()     Debug.WriteLine(s) End Sub 
end example

Listing 8-8 shows the code for retrieving the whole procedure, in which the cursor resides at the time the user invokes an add-in feature that needs to work on a whole procedure. The only one that I have implemented thus far is CloneProcedure, but I implement others in Chapter 12.

Developing this function took a series of debugging sessions in the Macros IDE. I had several requirements to fulfill to accomplish the design goals of the function. These requirements are outlined here:

  • Determine the starting point of the procedure.

  • Determine the ending point of the procedure.

  • Determine if there are any comment lines preceding the procedure definition line. If so, they belong to the procedure also.

  • Select the whole procedure, including preceding comment lines.

Remember that there are over 3,400 classes in the .NET Framework and the biggest challenge is to ferret out the one(s) that will provide the functionality that you need at a given time. In order to create the new procedure, the first thing that I needed was some code that would position the cursor at the top and bottom of the procedure in which the user has placed the cursor. This is where the sample macros that Microsoft has provided are so helpful. In the VSEditor module of the Samples node, I found a macro command named OneFunctionView.

In the OneFunctionView macro command (see Listing 8-7), I found the two lines that I was looking for that would locate the beginning and end of the procedure in which the user positioned the cursor. Obviously, there are support lines, such as the dimension of the objects that are used by these two lines, and I picked them up also.

Listing 8-7: OneFunctionView Macro from the Sample Macro Projects

start example
 Sub OneFunctionView()          Dim ts As TextSelection = ActiveWindow().Selection          Dim tsSave As EditPoint = ts.ActivePoint.CreateEditPoint          Dim ep As EditPoint = ts.ActivePoint.CreateEditPoint          " Get the start and outline to start of doc.          ep.MoveToPoint(ep.CodeElement(EnvDTE.              vsCMElement.vsCMElementFunction)              .GetStartPoint(vsCMPart.vsCMPartWhole))          ep.LineUp()          Debug.WriteLine(ep.Line)          ts.MoveToPoint(ep, False)          ts.StartOfDocument(True)          ts.OutlineSection()          ep.LineDown()          " Move ep back in function and outline from end to end of doc.          ep.MoveToPoint(tsSave)          ep.MoveToPoint(ep.CodeElement(EnvDTE.              vsCMElement.vsCMElementFunction)              .GetEndPoint(vsCMPart.vsCMPartWhole))          ep.LineDown() Debug.WriteLine(ep.Line)          ts.MoveToPoint(ep, False)          ts.EndOfDocument(True)          ts.OutlineSection()          ts.MoveToPoint(tsSave)     End Sub 
end example

Having found the major code lines that I needed in the OneFunctionView macro, I began to construct the code shown in Listing 8-8. Next, I added lines of code that back up from the top of the procedure to determine whether there are comment lines preceding the procedure definition line. If there are, they actually belong to the procedure also.

Listing 8-8: GetWholeProc Macro Function

start example
 01 Function GetWholeProc() As String 02     Dim ts As TextSelection = ActiveWindow().Selection 03     Dim ep As EditPoint = ts.ActivePoint.CreateEditPoint 04     Dim sLine As String 05     Dim i As Integer 06 07     Try 08         ' if the user has selected the whole proc, then just return it 09         ' otherwise select it for them... 10         If Len(ts.Text) > 0 Then 11            If (InStr(1, ts.Text, "Sub ", 1) > 0 Or _ 12                InStr(1, ts.Text, "Function ", 1) > 0) And _ 13                (InStr(1, ts.Text, "End Sub", 1) > 0 Or _ 14                InStr(1, ts.Text, "End Function", 1) > 0) _ 15                Then 16                   Return ts.Text 17            End If 18           GoTo SelectTheProc 19        Else 20 SelectTheProc: 21           " Get the start of the proc 22           ep.MoveToPoint(ep.CodeElement(EnvDTE.vsCMElement.  23                vsCMElementFunction).GetStartPoint(vsCMPart.vsCMPartWhole)) 24 25             ' move selection start point to top of proc 26             ts.MoveToPoint(ep, False) 27 28             ' back up to previous line looking for comments 29             i = 0 30             Do 31                 ep.LineUp() 32                 ts.MoveToPoint(ep, False) 33                 ts.SelectLine() 34                 sLine = ts.Text 35                 If Left(Trim(sLine), 1) <> ""' Then 36                     ep.LineDown() 37                     ts.MoveToPoint(ep, False) 38                     Exit Do 39                 End If 40                 i = i + 1 41             Loop 42 43             ' if the count of comment lines > 0 the ts point is set 44             ' else we must move it back to the original 45             ep.LineDown(i + 1) 46 47             ' move to bottom of proc 48             ep.MoveToPoint(ep.CodeElement(EnvDTE.vsCMElement.  49                    vsCMElementFunction).GetEndPoint(vsCMPart.vsCMPartWhole)) 50 51             ' select the proc 52             ts.MoveToPoint(ep, True) 53             Return ts.Text 54         End If 55     Catch 56             System.Windows.Forms.MessageBox.Show  57             ("You must either select the whole  58             procedure or your cursor must  59             be within the procedure to be selected.") 60       Return "" 61     End Try 62 End Function 
end example

So now having assembled the procedure shown in Listing 8-8, I began to debug it. At this point, I will not show images depicting the various debugging sessions that I went through to arrive at the desired procedure-that would take up too much time and space. Needless to say, it took several iterations through the code in the debugger in the Macros IDE to arrive at the finished code.

There were a couple of final details that I had to take into consideration. First, I had to make a determination as to whether the user had already selected the whole procedure before invoking the add-in. That is entirely possible and a viable option that I must account for. Second, if the cursor is not positioned within the procedure, the GetStartPoint method will raise an exception. It will not work if the cursor is positioned on the procedure definition line. Therefore, I had to protect the code with structured error handling.

In the procedure, lines 02 through 10 dimension the objects required by the procedure. Line 02 sets a pointer to the whole of the selection if the user has selected the procedure prior to invoking the add-in feature that will call this function. If the user has not selected any code block, but has positioned the cursor within the procedure to be selected, the TextSelection (ts) object will simply point to the position of the cursor within the procedure to be retrieved. Line 03 creates an EditPoint object pointing to the current cursor position. This EditPoint (ep) and the TextSelection (ts) objects will also be moving as I progress through the code.

Lines 10 and 11 are simply testing for the presence of code first and then for the presence of the beginning and ending lines of the procedure. If these tests are positive, the function returns the selected procedure.

Caution 

If there were comments preceding the procedure definition line, but the user did not select them, and they are needed for some feature (such as a procedure-documenting feature), then this test is not sufficient.You should remember that double-clicking in the left margin of a procedure will automatically select the whole procedure, but it will not select comments preceding that procedure, even though comments immediately preceding a procedure definition line actually are a part of that procedure rather than the previous procedure.

If the tests in line 10 or 11 fail, then control passes to line 22, where I'll begin the process of selecting the desired procedure. Line 22 is a complex line that begins by calling a method of the EditPoint object. It uses the EditPoint object's MoveToPoint method, which will actually move to the beginning of the procedure. To do this, it references the EditPoint object's CodeElement property. The CodeElement property returns the code element at the location of a TextPoint or EditPoint. A code element can be any fragment of code, but generally there's a CodeElement object for each definition or declarative syntax in a language. This means that for most top-level definitions or declarations in a file, or for any syntactic form in a class definition, there's an associated CodeElement object. Because the CodeElement property is an object (remember, everything is an object), it has a method called GetStartPoint. This method is passed a constant specifying the type of CodeElement to return. In this case, it is requesting the start point of the procedure. The result of this complex line is that the EditPoint (ep) is moved to the beginning of the procedure.

Having moved the EditPoint, line 26 moves the TextSelection point to the same position as the EditPoint. I do this so that when the desired EditPoint is reached, I will be ready to select the block of text.

Lines 29 through 41 are fairly straightforward. They back up the EditPoint and TextSelection point, looking for comment lines immediately preceding the procedure definition line. The Integer (i) is a count of the lines that I have moved above the procedure definition line. Line 31 moves the EditPoint backward and line 32 keeps the TextSelection point in sync. I do this so that I can select the current line to examine its text for a comment delimiter. If at line 35 I do not find the comment delimiter, I am finished moving backward and must move the EditPoint and TextSelection point down one line. This will either place the TextSelection point at the procedure definition line if there were no comments found or at the first comment line belonging to the procedure being selected.

Line 45 moves the EditPoint back into the procedure. Remember that the cursor must be within the procedure to prevent raising an error in the GetStartPoint or GetEndPoint methods of the CodeElement property.

Line 48 uses the same mechanism to find the end of the procedure as line 22 did to find the start of the procedure. The only difference is that it calls the GetEndPoint method of the CodeElement property. Having found the end of the procedure, line 52 does the actual selection of the whole procedure. If the second parameter of the MoveToPoint method of the TextSelection object is True, the MoveToPoint method will select all text from the current TextSelection point to the point to which it is being moved.

To run the completed macro command, I place the cursor anywhere inside of the procedure to be selected. In this case, it is the procedure named TestMacroProcedureSelection. Next, I double-click the TestSelectWholeProc command in the Macro Explorer. Figure 8-10 shows the result of running the macro.

click to expand
Figure 8-10: Automatic selection of a whole procedure by a macro

Obviously, this new function, GetWholeProc, is a valuable addition to your library of reusable procedures. You will find that it has many uses, especially in features such as CloneProcedure, which was developed in an earlier chapter.



 < Free Open Study > 



Writing Add-Ins for Visual Studio  .NET
Writing Add-Ins for Visual Studio .NET
ISBN: 1590590260
EAN: 2147483647
Year: 2002
Pages: 172
Authors: Les Smith

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net