C# Corner

Achieve Low-Impact Reuse

Sometimes it makes more sense to separate functionality you use repeatedly into its own component. Learn how to create a special command-line processing component.

TECHNOLOGY TOOLBOX: C#

Several generations of technology toolkits have given us different ways to support late binding, or -- more simply -- loose binding. By late binding and loose binding, I refer to the ability to design a bit of software that should be useful in almost any situation, not just your application. Ideally, you'd want this new building block to be useful for any application, even if that application has already been written, and the developer wants to plug your new feature into his existing application.

In this article, I'll show you how to implement a discrete piece of functionality you can use from an existing application. I based this solution on a recent problem I had to solve. The simple class I'll describe uses C# expressions to solve the problem of command-line parsing for console applications. The existing application in this case is an original clipboard app I wrote about five years ago (see Listing 1). This application lets you put output from a single console app on the clipboard. It has saved me countless hours when running through apps, paging up in the console window, selecting text, and copying it to the clipboard.

Recently, I found myself wanting to update this C# 1.0 application by adding some features that take advantage of new C# syntax. My main motivation for making these changes was that I needed a set of command-line options. The original clipboard program was hardcoded to echo all output

and to send that output simultaneously to the clipboard. I wanted the updated version of the app to behave differently in a handful of ways. First, I wanted the option to run the command-silently, without echoing output to the console; second, I wanted to send the app's output to a text file in addition to the clipboard; and finally I wanted to read the input from a text file and copy that text file to the clipboard.

To implement these features, I added three switches to the command line (see Listing 2). The original design used an optional first argument to specify an input file that would be copied to the clipboard. No arguments meant that clipboard.exe should obtain its input from System.Console.In. I had to change that because I wanted to add extra input parameters: -s instructs the app to be silent and not to echo any of the text to the console; -w <filename> tells the app to write the text to a file, in addition to the clipboard; and -r <filename> tells the app to read the input text from a file.

Build a Reusable CLP
The new version of the code is considerably larger than the original. Worse, all the new code performs command-line processing. This makes the code harder to read and understand, obscuring the original intent of the code.

These minuses prompted me to consider creating a reusable component for command-line processing. Almost every console application is going to have some form of command-line options. That means almost every console application needs to parse a command line.

The next step is to design a reusable component for command-line parsing. You could create a base class or define an interface to describe the code that should be executed, but that's more effort than is necessary for processing a command line. A much simpler solution is to create an API that uses Func<T> or even Expression<T> to create a component that has much less coupling between any application and the component that processes the command line.

Consider what the component must do: It must examine each argument on the command line. If the argument is one specified for this application, the component must perform whatever processing is required for that argument. "Whatever processing is required" sounds like a method call of some sort. A method call of some sort looks like a delegate, which you can represent with a Func<T> or an Expression<T>. You can almost see the basis of the component in the code that already exists in Listing 2. But you need to have a basic understanding of how Func<T> works before you can understand exactly how you should implement it.

Func<T> stores a type safe wrapper for a delegate call. You can access any objects or information you need in a lightweight manner because the C# compiler creates a Closure around any local variables accessed by the target of an expression. You can learn more about this in the article, "Capture Variables with Closures" (Ask Kathleen, February 2008).

The next step is to separate the common blocks of code in Listing 2 from the specific parts of the code in that listing:

{
   // Create the processor
   ClipboardWriter processor = 
      new ClipboardWriter();
   CommandLineProcessor argParser 
      = new CommandLineProcessor();

   // -s means silent
   argParser.AddParameter("-s", (i) =>
      {
      processor.Silent = true;
         return i;
         });
   // -w means set output file:
   argParser.AddParameter("-w", (i) =>
      {
         i++;
         string outputFile = args[i];
         string dir = Path.GetDirectoryName(
            outputFile);
         if ((!string.IsNullOrEmpty(dir))
            && (!Directory.Exists(dir)))
         {
            throw new 
               InvalidOperationException(
               "Error:  Specified Output Directory 
               doesn't exist");
          }
         processor.OutputFile = outputFile;
         return i;
      });

   // -r means set the input file
   argParser.AddParameter("-r", (i) =>
      {
         i++;
          string fName = args[i];
          if (File.Exists(fName))
          {
             processor.InputStream = new 
                StreamReader(fName);
          }
          else
          {
             throw new 
               InvalidOperationException(
               "Error: Input file does not exist");
          }
          return i;
      });
   // process the args:
   try
   {
      argParser.Process(args);
   }
   catch (InvalidOperationException e)
   {
      Console.WriteLine(e.Message);
      return;
   }
   // Do the work
   processor.processStream();
}

This code details how to use the command-line processor component from the new Main() method. This method creates the

command-line processor component and defines the code that should be executed when a particular command-line option has been found. Once all the command-line options for this application have been set, the command-line processing component parses the command line. Finally, the Main() method performs the clipboard processing after the command line has been processed.

The usage is interesting for what's missing, and how this reads. You can examine the Main() method to see exactly what parameters are used by this program, and what actions each parameter controls. The command line-processing component now handles any and all parsing tasks, and loops through the command-line arguments.

Create the Component
The CommandLineProcessor is simple enough to create:

public class CommandLineProcessor
{
   private Dictionary<string, Func<int, int>> 
      commandOptions = 
      new Dictionary<string, Func<int, int>>();

   public void AddParameter(string parameter, 
      Func<int, int> workToDo)
   {
      if (commandOptions.ContainsKey(
         parameter.ToLower()))
         throw new ArgumentException(
         "Parameter already being processed", 
         parameter);
      commandOptions.Add(
         parameter.ToLower(), workToDo);
   }

   public void Process(string[] args)
   {
      for (int i = 0; i < args.Length; i++)
      {
         string standardArg = args[i].ToLower();
         if (commandOptions.ContainsKey(
            standardArg))
         {
            i = commandOptions[standardArg](i);
         }
      else
      {
         throw new InvalidOperationException(
            "Error: Unknown Command line option.");
      }
      }
   }
}

Note that there are few demands placed on the users of this component. Using the Func<int,int> means that client code needs only to specify a block of code. For example, it doesn't need to create extra infrastructure to support an object-oriented object.

The AddProcess method adds a new parameter string and the associated code to set any state when that parameter is specified on the command line. The Process() method parses the command line, and when any known parameters are found, the CommandLineProcessor executes the corresponding code.

What's important about this isn't the example implementation, but the concept and the minimal requirements on the client code. The CommandLineProcessor isn't large, but it has taken over all the common tasks for command-line processing. You could easily enhance it to check for required parameters, handle quoted strings, or perform any other common tasks you find for your needs.

More importantly, there's no extra work for the client code. If you look at the different versions of the Main() method in the different versions, you can see that there's no extra code needed to support the infrastructure of the CommandLineProcessor: no new base class requirements, no new interfaces to create -- no new requirements at all. The same blocks of code that were used in the original version are still present in the version that separates out the command-line processor. It's just that you've offloaded the common processing to the new component.

Using delegates and Func<> definitions to specify the code needed for your component creates an interface with much lower coupling than you would've had if you had mandated an interface or an abstract base class on your client developers. This approach often proves a better way to create simple components that need to communicate with each other.

About the Author

Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at wwagner@srtsolutions.com.

comments powered by Disqus
Most   Popular
Upcoming Events

.NET Insight

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.