Using the Proxy Pattern to Write to Multiple TextWriters ~ D. Patrick Caldwell on Software Engineering

Wednesday, December 17, 2008

Using the Proxy Pattern to Write to Multiple TextWriters

I was working on a data synchronizing application the other day.  I needed to write to a file for the export, write to a string builder for logging and analysis, and write to the console for debugging.  I know that it's pretty common that I'll need to write to more than 1 text stream at the same time, so I figured I could write a quick proxy application to write to a collection of TextWriters.Please comment on this post and let me know how this TextWriterProxy article helped you.Here's what I came up with:
    1 using System.Collections.Generic;
    2 using System.Text;
    3 using System.IO;
    4 
    5 namespace ESG.Utilities
    6 {
    7     public class TextWriterProxy : TextWriter
    8     {
    9         // store TextWriters here
   10         private List<TextWriter> _writers = new List<TextWriter>();
   11 
   12         #region Properties
   13 
   14         /// <summary>
   15         /// This property returns Encoding.Default.  The TextWriters in the
   16         /// TextWriterProxy collection can have any encoding.  However, this
   17         /// property is required.
   18         /// </summary>
   19         public override Encoding Encoding { get { return Encoding.Default; } }
   20 
   21         /// <summary>
   22         /// Gets or sets the line terminator string used by the TextWriters in
   23         /// the TextWriterProxy collection.
   24         /// </summary>
   25         public override string NewLine
   26         {
   27             get
   28             {
   29                 return base.NewLine;
   30             }
   31 
   32             set
   33             {
   34                 foreach (TextWriter tw in _writers)
   35                     tw.NewLine = value;
   36 
   37                 base.NewLine = value;
   38             }
   39         }
   40 
   41         #endregion
   42 
   43         #region Methods
   44 
   45         /// <summary>
   46         /// Add a new TextWriter to the TextWriterProxy collection.  Setting properties 
   47         /// or calling methods on the TextWriterProxy will perform the same action on 
   48         /// each TextWriter in the collection.
   49         /// </summary>
   50         /// <param name="writer">The TextWriter to add to the collection</param>
   51         public void Add(TextWriter writer)
   52         {
   53             // don't add a TextWriter that's already in the collection
   54             if (!_writers.Contains(writer))
   55                 _writers.Add(writer);
   56         }
   57 
   58         /// <summary>
   59         /// Remove a TextWriter from the TextWriterProxy collection.
   60         /// </summary>
   61         /// <param name="writer">The TextWriter to remove from the collection</param>
   62         /// <returns>True if the TextWriter was found and removed; False if not.</returns>
   63         public bool Remove(TextWriter writer)
   64         {
   65             return _writers.Remove(writer);
   66         }
   67 
   68 
   69         // this is the only Write method that needs to be overridden
   70         // because all of the Write methods in a TextWriter ultimately
   71         // end up calling Write(char)
   72 
   73         /// <summary>
   74         /// Write a character to the text stream of each TextWriter in the 
   75         /// TextWriterProxy collection.
   76         /// </summary>
   77         /// <param name="value">The char to write</param>
   78         public override void Write(char value)
   79         {
   80             foreach (TextWriter tw in _writers)
   81                 tw.Write(value);
   82 
   83             base.Write(value);
   84         }
   85 
   86         /// <summary>
   87         /// Closes the TextWriters in the TextWriterProxy as well as the 
   88         /// TextWriterProxy instance and releases any system resources
   89         /// associated with them.
   90         /// </summary>
   91         public override void Close()
   92         {
   93             foreach (TextWriter tw in _writers)
   94                 tw.Close();
   95 
   96             base.Close();
   97         }
   98 
   99         /// <summary>
  100         /// Releases all resources used by the TextWriterProxy and by the 
  101         /// TextWriters in the TextWriterProxy collection. 
  102         /// </summary>
  103         /// <param name="disposing">Pertains only to the TextWriterProxy instance: 
  104         /// true to release both managed and unmanaged resources; false to release 
  105         /// only unmanaged resources.</param>
  106         protected override void Dispose(bool disposing)
  107         {
  108             foreach (TextWriter tw in _writers)
  109                 tw.Dispose();
  110 
  111             base.Dispose(disposing);
  112         }
  113 
  114         /// <summary>
  115         /// Clears all buffers for each TextWriter in the TextWriterProxy 
  116         /// collection and causes all buffered data to be written
  117         /// to the underlying device.
  118         /// </summary>
  119         public override void Flush()
  120         {
  121             foreach (TextWriter tw in _writers)
  122                 tw.Flush();
  123 
  124             base.Flush();
  125         }
  126 
  127         #endregion
  128     }
  129 }

So far, it works great. It cleans up a lot of my code and gives me the option to write to any number of TextWriters with only one call. Further, if you are calling a method that takes a TextWriter as a parameter, you can pass the TextWriterProxy to it because it extends the TextWriter class. Here's what the usage syntax looks like:
    1 // create a TextWriterProxy instance
    2 TextWriterProxy proxy = new TextWriterProxy();
    3 
    4 // add the Console.Out TextWriter
    5 proxy.Add(Console.Out);
    6 
    7 // you can still write directly to console
    8 Console.WriteLine(string.Empty.PadRight(80, '='));
    9 
   10 // add a StreamWriter for a FileStream
   11 FileStream fs = new FileStream("C:\\TestExportFileAutoGen.abx", FileMode.Create);
   12 StreamWriter resultWriter = new StreamWriter(fs);
   13 proxy.Add(resultWriter);
   14 
   15 // add a StringWriter for a StringBuilder
   16 StringBuilder sb = new StringBuilder();
   17 StringWriter resultStringWriter = new StringWriter(sb);
   18 proxy.Add(resultStringWriter);
   19 
   20 // call a method that takes a TextWriter
   21 ClientSync.GenerateSessionDataExport("Sync.ServerExport", proxy);
   22 
   23 // write directly to the TextWriterProxy
   24 proxy.WriteLine("Export Complete!");
   25 
   26 // close all of my writers
   27 proxy.Close();

And there you have it. A TextWriterProxy class to write to multiple TextWriters at once.
I really appreciate comments so please feel free to comment on my posts. Whether you agree or disagree, I'd love to hear from you. Also, feel free to link back to your own blog in your comments. You can even subscribe to an RSS feed of the comments on this thread.

© 2008 — , D. Patrick Caldwell, President, Autopilot Consulting, LLC

4 comments:

  1. It seems like a good, simple alternative to the pub/sub pattern, where some class will be subscribing to the publisher via events.

    ReplyDelete
  2. looks good..

    question.. what is the point of making the calls to the base textwriter? ie. base.write(value)

    the base doesn't seem to be ever assigned.

    ReplyDelete
  3. Good call Zj24. In this case, it's because it wasn't originally a textwriter. I don't remember what it was, but it wasn't abstract.

    There's no exception thrown because the base is defined, but it's dumb to call because ultimately the TextWriter abstract class is going to end up calling a virtual write method that doesn't do anything (despite the fact that it has formatted the string, converted it to a char array, and created a buffer).

    Good eye Zj24.

    ReplyDelete
  4. Great Explanation. Another great article i recommend is this one

    ReplyDelete