Refactoring the ShapeApp MacroRecorder

As the ShapeAppAdvancedCSharp sample demonstrates, CodeDom offers a generalized representation of source code that works nicely with VSTA (ie: IVstaProjectHostItem.AddMethod()) and the DTE (ie: DTE.Solution.SolutionBuild.Build()). 

 

With this approach, your macro recorder can generate source for either C# or VB from a single code execution path.  Using the CodeDom, you can build up a 'graph' of your new macro that can be easily traversed and modified.

 

To improve on this example we will refactor the code to create a more specialized, limited set of methods that reflect the unique object syntax and macro recording activity patterns of ShapeApp:

 

At present, each recorded command is built up with individual, repetitive CodeDom expressions that generate one line of C# source.  For example, the following class ShapeColorChangeCommand  generates the C# statement:

this.ActiveDrawing.Shapes[0].Color = this.CreateColor(-16744193);

 

===

 

    internal class ShapeColorChangeCommand : Command

    {

        private int index;

        private Color color;

 

        internal ShapeColorChangeCommand(int index, Color color)

        {

            this.index = index;

            this.color = color;

        }

 

        /// <summary>

        /// Generates code for the ShapeColorChangeCommand object.

        /// This method will generate code of the form:

        ///     this.ActiveDrawing.Shapes[index].Color = this.CreateColor(argb);

        /// </summary>

        /// <param name="w">The command writer to write the statement to.</param>

        internal override void Save(ICommandWriter w)

        {

            CodePropertyReferenceExpression drawingRef =

                new CodePropertyReferenceExpression(new CodeThisReferenceExpression(), "ActiveDrawing");

 

            CodePropertyReferenceExpression shapesRef =

                new CodePropertyReferenceExpression(drawingRef, "Shapes");

 

            CodeIndexerExpression shapeRef =

                new CodeIndexerExpression(shapesRef, new CodePrimitiveExpression(index));

 

            CodePropertyReferenceExpression colorRef =

                new CodePropertyReferenceExpression(shapeRef, "Color");

 

            CodeMethodInvokeExpression createColorStatement =

                new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "CreateColor",

                    new CodePrimitiveExpression(System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B).ToArgb()));

 

            CodeAssignStatement assignStatement =

                new CodeAssignStatement(colorRef, createColorStatement);

 

            w.Write(assignStatement);

        }

    }

 

===

 

...this can be dramatically refactored specifically for use in ShapeApp.  As an example, we'll reduce ShapeColorChangeCommand.Save() from 7 lines down to 1 line via code reuse:

 

===

 

        /// <summary>

        /// Generates code for the ShapeColorChangeCommand object.

        /// This method will generate code of the form:

        ///     this.ActiveDrawing.Shapes[index].Color = this.CreateColor(argb);

        /// </summary>

        /// <param name="w">The command writer to write the statement to.</param>

        internal override void Save(ICommandWriter w)

        {

            //refactored to one line!!

            w.Write(CommandHelper.AssignColor(

                CommandHelper.SelectedShapeExpression(null,index),color));

        }

 

===

 

...where the CommandHelper methods:

      AssignColor() and SelectedShapeBLOCKED EXPRESSION

are reusable for every future macro line where a selected shape or a color is used:

 

===

 

        public static CodeExpression SelectedShapeExpression(

                        CodeExpression preFixExpression, int shapeIndex)

        {

            CodePropertyReferenceExpression drawingRef =

                new CodePropertyReferenceExpression(preFixExpression == null ?

                    new CodeThisReferenceExpression() : preFixExpression, "ActiveDrawing");

 

            CodePropertyReferenceExpression shapesRef =

                new CodePropertyReferenceExpression(drawingRef, "Shapes");

 

            CodeIndexerExpression shapeRef =

                new CodeIndexerExpression(shapesRef, new CodePrimitiveExpression(shapeIndex));

 

            return shapeRef;

        }

        public static CodeAssignStatement AssignColor(

                        CodeExpression preFixExpression, Color color)

        {

            CodePropertyReferenceExpression colorRef =

                new CodePropertyReferenceExpression(preFixExpression == null ?

                    new CodeThisReferenceExpression() : preFixExpression, "Color");

 

            CodeMethodInvokeExpression createColorStatement =

                new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "CreateColor",

                    new CodePrimitiveExpression(

                        System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B).ToArgb()));

 

            CodeAssignStatement assignStatement =

                new CodeAssignStatement(colorRef, createColorStatement);

 

            return assignStatement;

        }

    }

 

===

 

And finally...

Another possible approach is to add macro text by 'spitting' the source code of your macro, as raw text, into a VB/or C# Code editor:

 

===

           TextDocument txtDoc = this.macroProject.DTE.ActiveDocument as TextDocument;

            if(txtDoc.Language == CodeModelLanguageConstants.vsCMLanguageCSharp)

            {

                txtDoc.StartPoint.CreateEditPoint();

                txtDoc.Selection.Insert("//Comment",

                        (int)vsInsertFlags.vsInsertFlagsCollapseToEnd);

            }

 

===

 

This is similar to VisualStudio automation

(ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.VisualStudio.v80.en/dv_extvbcs/html/2b46709c-5cef-4ec5-9d8b-e2f6127613f7.htm)

 

But inserting raw text into a document this way yields a much more brittle macro recorder full of static string constants that are language-specific, and may be more difficult to maintain.  Also, text code-spit does not allow for the OO-benefits of codeDom and the textdocument contains only raw text until the document is compiled.

 

 


Posted Jan 18 2007, 04:10 PM by Gary
Copyright Summit Software Company, 2008. All rights reserved.