Click here to Skip to main content
15,885,546 members
Articles / Programming Languages / C#
Article

Macro Management Framework

Rate me:
Please Sign up or sign in to vote.
4.92/5 (23 votes)
28 Feb 200510 min read 42.2K   494   35   2
Pluggable text macros.

Sample Image - MacMan.gif

Introduction

Macros are a useful concept in a number of applications. I am talking about text macros like "$APPDIR" or "$DIR($EDITFILE)", commonly found in external tool configurations. Although external tool management is a prominent use case for text macros, other applications can benefit from their use, too. In fact, inspired use of macros can greatly simplify and generalize virtually any software code that heavily works with strings. Examples include:

  • Controls that display text information:
    • Label
    • LinkLabel (URL can be retrieved through a macro).
    • StatusBar/StatusPanel (message displayed can be configured with macros).
    • Form (title can be retrieved through a macro).
    • AboutBox (title, product name, version etc.).
    • Listbox/ListView (the text of each item can be retrieved through a macro).
  • Configuration files
    • configuration entries can contain macros that are dynamically resolved elsewhere, so the configuration files themselves become more general and more reusable.
  • Many others (imagination is the only limit).

So, what does it take to lay out a foundation for all this? Herein I would like to share with you a solution that helped me utilize the "macro power".

Background

Macros defined

Basically, macros are special tags embedded in strings. I will call strings that contain macros macro expressions. A macro expression acts as a template that can generate string instances with conforming but variable, context-dependent content.

An example: The string "$PRJDIR\config.xml" denotes a file in a project folder. This is the general semantics. Its actual content depends on the time it is resolved and the context it is resolved in (in a multi-project environment, which project is active, if any).

As such, macros can be viewed as (sub-) string generators with defined semantics.

During macro resolution, the tags in a macro expression are replaced by their corresponding values, yielding the final string content. In the example above, this may be "C:\Programs\MyApp\UserProjects\Project01\config.xml", assuming that "Project01" is open and active, or maybe null or an empty string, if no projects are open.

Macros may be parameterized (although not all applications seem to make use of these – Visual Studio for example does not support parameterized macros). However, parameterized macros typically greatly reduce the total amount of distinct macros an application needs to support. For example, with a macro $DIR(path) that returns a directory part of a path, all macros that denote a directory of some file (like $PRJDIR denotes the directory where $PRJFILE is located) become a matter of convenience rather than a matter of necessity. This means that an application needs only define macros like $PRJFILE, $EDITFILE and $SOLUTIONFILE, and not $PRJDIR, $EDITFILEDIR and $SOLUTIONDIR any more.

Obviously, macros imply semantics specific to an application. $PRJDIR, for example, may be a directory or a URL, depending on application needs. And in applications that do not deal with the concept of a project, it would not have a sense at all.

So, whatever the solution may look like, it must deal with the fact that particular macros are application specific. This includes their sole existence as well as the semantics attached to them.

On the other hand, the structure/syntax of macro expressions may quite safely be assumed as common to all macro applications and should be fully covered by the solution.

Macro Expression Syntax

A (quite informal) EBNF syntax of macro expressions would be:

MacroExpression ::= { ArbitraryText | "$$" | MacroCall }.
MacroCall       ::= "$"MacroIdentifier["("ArgumentText")"].>
ArgumentText    ::= MacroExpression.
MacroIdentifier ::= any valid C# identifier.
ArbitraryText   ::= <>any character but "$".

In plain words: macros may occur anywhere in arbitrary text, and are identified by the prefix "$" and a unique macro identifier. They may have an argument, enclosed in parenthesis, which again may contain macros. To include the literal "$" in the text, it must be doubled.

The (somewhat) complicated part here is the recursive definition of MacroExpression (a macro argument is a macro expression itself). This is beyond the scope of regular expressions (and the Regex class) and requires a more sophisticated parser. However, it is a necessary prerequisite to enable recursive macro calls, i.e. calls of the form $F($G($H(some text))) - a feature naturally called for as soon as parameterized macros are included.

The Solution

Clearly, the solution is a macro management framework that unites the two main aspects:

  1. Definition of macros specific to an application.
  2. Macro expression translation (parsing of macro expressions and generation of the actual string content).

These aspects are covered by two public classes in the framework, MacroManager and MacroInterpreter, respectively.

Translation of Macro Expressions

The class MacroInterpreter provides a simple interface for translation. Translation is a matter of a single call to MacroInterpreter.Translate (a static method), like:

C#
label.Text = MacroInterpreter.Translate(macroExpressionBox.Text);

which may result in something like:

Macro translation

(Here, $File(path) is meant to return a file name part of a path without the extension.)

If a macro cannot be resolved, it is replaced by the string "<???macro name>", which results in something like:

Effect of unresolvable macros

If an exception is thrown during macro resolution, the macro is replaced by "<!!!macro name: exception message>", like here:

Effect of exceptions during 
macro resolution

Errors in nested macro calls invalidate the entire call chain and are expanded like this:

Effect of errors in nested macro calls

Defining Custom Macros

The class MacroManager, together with some auxiliary classes, constitutes the interface for macro definition. It contains the static methods DefineSuite and UndefineSuite to add and remove macro definitions.

For convenience, macros are defined in macro suites. A macro suite is any class marked with the attribute MacroSuiteAttribute (part of the framework), that contains macro resolution methods. A macro resolution method is any method marked with the attribute MacroAttribute (also part of the framework), that has the signature.

C#
string method (string argument);

The name of the method becomes the macro identifier (thus ensuring macro identifiers are valid C# identifiers). MacroAttribute specifies additional macro properties, Caption and Category. The first should contain a human-readable name of the macro and defaults to the macro identifier if not specified otherwise. It may be (and is) used in controls that need to list macros, like the macro selection menu in the picture at the top of this article. The second is used to group macros by category (again in the macro selection menu). If omitted or empty, the macro category defaults to the suite category. The suite category is specified by the MacroSuiteAttribute.Caption property and defaults to "Misc".

So, the definition of macros is a matter of implementing a macro suite class ...

C#
[MacroSuite]
public class MyMacros
{
   [Macro(Caption="Application File Path", Category="File Path")]
   public string AppPath(string arg)
   {
      return Application.ExecutablePath;
   }
}

... and registering an instance of it with the framework somewhere in the application ...

C#
MacroManager.DefineSuite( new MyMacros() );

You may also consider employing the convention of making MyMacros a singleton, accessible through the static field Suite, which would result in:

C#
[MacroSuite]
public class MyMacros
{
   public static MyMacros Suite = new MyMacros();
   private MyMacros(); // singleton
   ...
}

...
MacroManager.DefineSuite( MyMacros.Suite );

Note that macro identifiers must be unique across macro suites. Already defined macros take precedence over new macros with the same identifier.

To remove a macro suite definition, simply call,

C#
MacroManager.UndefineSuite( MyMacros.Suite );

The taken approach mimics the one in NUnit. I dropped my first idea quite early, which was an interface with the properties Identifier, Caption, Category and the method GetValue that the application would implement once for every macro, since it would have required too much coding on the application side (and it is my conviction that frameworks should simplify application code as much as possible).

(Indeed, when I came to the part on defining macros myself, I was really glad about this effort-saving decision. By the way, this shows a great strength of writing sample applications for components. Combined with a healthy portion of laziness, it generates great motivation to rethink and simplify interfaces.)

Another advantage of this approach is the ability to make your form (or control or component) a macro suite, because there is no predefined base class for macro suites. This may be appropriate for smaller applications, where you may not want to add an extra class for a couple of macros. It may be even mandatory in situations where macros refer to information in the form (or control or ...). (This situation is easily created, given the fact that Visual Studio seems to promote a data-on-the-GUI attitude - in my eyes an unlucky choice). Anyway, if you chose to take this approach, do not forget to undefine the suite before the component is destroyed.

Plugability

Maybe the most useful (and most easily overseen) side benefit of the chosen design is the clear separation between information providers (code that implements macro suites) and information consumers (code that uses macros). This makes the solution perfectly suitable for plug-in architectures, where individual plug-ins can bring in their own macros and hereby extend an external tool manager (or whatever) without even knowing it is there.

Framework Extensions

Macro Suites

The core framework does not predefine any macros. However, I have supplied a basic macro suite with macros that are generally useful for many macro applications, like the macro $AppPath, but also the basic path functions $Dir(path), $File(path) and $Ext(path) (yielding the directory, file name and file extension part of a path, respectively). Also included are some macros for special characters, like $TAB and $NL (tabulator and new-line character), useful for basic string formatting.

Macro-Related Components and Controls

Some useful components and controls can be built on top of the framework. These should be located in the Components sub-namespace. Currently, the only control supplied is MacTextBox, a combination of a text box and a button. The latter brings up a popup menu with the list of all currently available macros. A click on the menu inserts the macro at the caret position in the text box.

The control re-publishes some properties of the underlying controls: Text, TextAlign and CharacterCasing of the text box, as well as the ButtonImage property that delegates to the button's Image. In addition, it raises the event EnterKeyPressed (of course, upon pressing the Enter key), which I found more useful than reacting to every TextChanged event.

I would be grateful for any suggestions on what other components or controls might be useful.

Accompanying Demos

There are two demos packaged in one application. You can choose a demo from the main form.

The first demo shows how to configure command line arguments with macros. The translated command line arguments are assigned to a LinkLabel. Clicking on the label, CMD.EXE runs with the arguments and redirects the output to a text box on the form.

Demo 1

The second demo is illustrated in the picture at the top of this article and is the more interesting one. It defines a parameterized macro that accesses a data field value by field name, and uses it to construct a string from the field values in the current row. In the demo, the string is displayed in a label. However, basically the same technique can be applied to generate reports, serial letters or e-mails etc.

Conclusion

The presented solution (of which I have not shown any internals and how's – please consult the sources and give me a hint if details on that level would significantly add to the article's value) provides a working framework and a simple yet solid foundation for further extensions.

More controls could be useful, such as an "external tool dialog", as well as a DataMacros component for generic access to data fields and other dataset-related information, but they can be easily implemented on top of the framework.

Anyway, I hope you will find the code (and this article) useful and interesting. As always, feedback and suggestions are welcome, anytime.

History

05/03/03

  • Added short sub-chapter about plugability.
  • Augmented the description of MacTextBox.
  • Added property Category to MacroSuiteAttribute to serve as default macro category if not specified otherwise with the macro resolution method. (When macros are grouped to suites sensibly, saves lots of repeated typing.)
  • Improved error handling - errors in nested macros are shown where they occur.

05/02/29

  • Fixed identifiers in the text to match identifiers in the code.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionGoto? Pin
Madmaximus11-Jan-07 9:31
Madmaximus11-Jan-07 9:31 
AnswerRe: Goto? Pin
z.i.10-Mar-07 2:37
z.i.10-Mar-07 2:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.