Simple report generator for ECO

Ever so often customers want things printed. It may be a recepit, a complex report, an inventory list or just a page about the customer object being viewed.

Thus I found myself volonteering to write a library system (simple book lending and returning) for a friend. I had implemented the most critical parts of the system (lending and returning) when I realized the current system they have print a lending receipt and a return receipt. So, rather than learning everything there is to know about printing and pixel adjustments of labels I decided to cheat just a little bit.

My first realization was that if I could generate a html-file with the required information, I could rely on a browser to render and print the receipts. That simple thought reduced the problem to how to generate the html-file.

My second idea was that instead of generating the full html-file, I would be better off expanding a html template. That way the system is future proof in the sense that a change in the layout of the printed stuff does not require programming, merely updating the template.

Now the problem is reduced to data driven template expansion.

Using ECO this should be a no-brainer, given the ability to use OCL to query the object layer without having to worry about fetching information from a storage backend.

I set out to write a general purpose expansion service with an interface initially looking like the following.

  1. public interface IExpansionService
  2. {
  3.   string Expand(IElement[] roots, string s);
  4. }

Simplistic, yes, but it can easily be expanded to cover a few more bases.

Then I pondered an expansion language. As can be guess from the interface, I would like to be able to pass several objects that can serve as root elements when evaluating OCL.

I finally settled for the following:

Hello {Person:self.firstName}!

You have purchased the following items:

{foreach:Invoice:self.invoiceItems}

* {Product:self.quantity} pieces of {InvoiceItem:self..Product.Name}

{/foreach}

(Add html elements as you please)

The things I need to parse are expressions within curly brackets. What is inside curly brackets are either:

* typename colon expression – must yield a string* “foreach” colon typename colon expression – must yield a collection

* “/foreach”

First, add a method to implement the interface.

  1. public string Expand(IElement[] roots, string s)
  2. {
  3.   return ExpansionCore(roots, s).ToString();
  4. }

It’s pretty clear  that I’ve hidden all the goodies in ExpansionCore.

  1. private StringBuilder ExpansionCore(IElement[] roots, string s)
  2. {
  3.   string startForeach = "foreach:";
  4.   string endForeach = "{/foreach}";
  5.  
  6.   StringBuilder expanded = new StringBuilder();
  7.   for (int i = 0; i < s.Length; i++)
  8.   {
  9.     char c = s[i];
  10.     switch (c)
  11.     {
  12.       case \\:
  13.         expanded.Append(s[++i]);
  14.         break;  
  15.  
  16.       case ‘{’:
  17.         int endPos = s.IndexOf(‘}’, i);
  18.         string toExpand = s.Substring(i + 1, endPos – i – 1);
  19.         if (toExpand.StartsWith(startForeach))
  20.         {
  21.           endPos = s.IndexOf(endForeach, endPos);
  22.           string repeatBlock = s.Substring(i + toExpand.Length + 2, endPos – i – toExpand.Length2);
  23.           expanded.Append(ExpandBlock(roots, toExpand, repeatBlock));
  24.           i += toExpand.Length + repeatBlock.Length + endForeach.Length + 1;
  25.           break;
  26.         }
  27.         i += toExpand.Length + 1;
  28.         expanded.Append(ExpandString(roots, toExpand));
  29.         break;
  30.  
  31.       default:
  32.         expanded.Append(c);
  33.         break;
  34.     }
  35.   }
  36.   return expanded;
  37. }

The special handling of back-slash allows the escaping of curly brackets. It also means back-slashes must be escaped.

If the upcoming character is an opening bracket, then simply find the closing bracket. If the string in between starts with “foreach” we handle it in ExpandBlock. Otherwise the string is expanded using ExpandString.

ExpandString doesn’t need to do much, really.

  1. private string ExpandString(IElement[] roots, string toExpand)
  2. {
  3.   return EvaluateExpression(roots, toExpand).GetValue<string>();
  4. }

Again, interesting bits are elsewhere. Let’s take a peek at EvaluateExpression.

  1. private IElement EvaluateExpression(IElement[] roots, string toExpand)
  2. {
  3.   var ocl = ServiceProvider.GetEcoService<IOclService>();
  4.   string[] parts = toExpand.Split(new char[] { ‘:’ }, 2);
  5.   string expression = toExpand.Trim();
  6.   IElement root = null;
  7.   if (parts.Length == 2)
  8.   {
  9.     root = GetRootElement(parts[0].Trim(), roots);
  10.     expression = parts[1].Trim();
  11.   }
  12.   return ocl.Evaluate(root, expression);
  13. }

Here we see that the string toExpand is split at the colon. If the resulting array is of length 1, then we didn’t specify a root type, and we simply use no root. That is perfectly ok for expressions like “Color.allinstances->first.colorname”.

Thanks to the OclService in ECO there is no need to worry about how to actually evaluate the string. Just get the service and invoke Evaluate. Before I forget, I passed an IEcoServiceProvider to the constructor of my ExpansionService.

There is one interesting bit left in EvaluateExpression – GetRootElement.

What I wanted was to specify the type by modeled name, and then match that against the  modeled names of the elements in the submitted IElement[] roots.

  1. private IElement GetRootElement(string typename, IElement[] roots)
  2. {
  3.   string[] nameparts = typename.Split(new char[] { ‘[', ']‘ });
  4.   int rootIndex = 0;
  5.   if (nameparts.Length > 1)
  6.   {
  7.     typename = nameparts[0].Trim();
  8.     rootIndex = int.Parse(nameparts[1].Trim());
  9.   }
  10.   int scanCount = 0;
  11.   foreach (var element in roots)
  12.   {
  13.     if (element.UmlType.Name.Equals(typename, StringComparison.InvariantCultureIgnoreCase) &amp;&amp; (scanCount++ == rootIndex))
  14.     return element;
  15.   }
  16.   return null;
  17. }

Again, I have deviated from the grammar specification. As you may or may not realize the type name can be followed by an index in hard brackets. If the index is left out, the first match is used. The rationale for this is that I might want to pass in two root elements of the same type, so there must be a way to select the first or the second of them.

The only part left to investigate is the ExpandBlock method.

  1. private string ExpandBlock(IElement[] roots, string toExpand, string repeatBlock)
  2. {
  3.   StringBuilder result = new StringBuilder();
  4.   IElementCollection list = EvaluateExpression(roots, toExpand.Substring(toExpand.IndexOf(‘:’) + 1)).GetAsCollection();
  5.   foreach (IElement element in list)
  6.   {
  7.     List<IElement> newRoots = new List<IElement>(roots);
  8.     newRoots.Insert(0, element);
  9.     result.Append(Expand(newRoots.ToArray(), repeatBlock));
  10.   }
  11.   return result.ToString();
  12. }

Given the neat partitioning of the code above, there’s not much required to expand the block.

First, evaluate the expression into an IElementCollection.

Then iterate over the collection and build a new IElement-array with the current element added as the first root element.

Finally use the new root array and expand the block using the Expand method.

Test it by something like:

  1. Person p1 = new Person(EcoSpace);
  2. p1.firstName = "Wolfgang";
  3. Person p2 = new Person(EcoSpace);
  4. p2.firstName = "Amadeus";
  5. MessageBox.Show(EcoSpace.ExpansionService.Expand(new IElement[] { p1.AsIObject(), p2.AsIObject() }, "First we have {Person:firstName} and then of course  {Person[1]:firstName}"));

Such nifty code requires the service to be registered and exposed as a property:

  1. public EcoProject1EcoSpace(): base()
  2. {
  3.   InitializeComponent();
  4.   this.RegisterEcoService(typeof(IExpansionService), new ExpansionService(this));
  5. }
  6. public IExpansionService ExpansionService { get { return GetEcoService<IExpansionService>(); } }

It is pefrectly possible to reach this service from anywhere in your application where an IEcoServiceProvider is available. Notably inside your business methods.

I rapidly enhanced my interface slightly to look like

  1. public interface IExpansionService
  2. {
  3.   string Expand(IElement[] roots, string s);
  4.   string Expand(IObjectProvider[] roots, string s);
  5.   string ExpandFile(IElement[] roots, string fileName);
  6.   void ExpandTemplateIntoFile(IElement[] roots, string templateName, string targetName);
  7. }

Most of the additional functionality os trivial to implement. The overload that takes an array of IObjectProvider allows calls to shorten ever so slightly:

  1. MessageBox.Show(EcoSpace.ExpansionService.Expand(new IObjectProvider[] { p1, p2 }, "First we have {Person:firstName} and then of course  {Person[1]:firstName}"));

In the name of reuse, I chose the following implementation for the second Expand.

  1. public string Expand(IObjectProvider[] roots, string s)
  2. {
  3.   return Expand(IElementArray(roots), s);
  4. }
  5.  
  6. private IElement[] IElementArray(IObjectProvider[] roots)
  7. {
  8.   List<IElement> result = new List<IElement>();
  9.   foreach (var item in roots)
  10.   {
  11.     result.Add(item.AsIObject());
  12.   }
  13.   return result.ToArray();
  14. }

..figuring I probably want to overload the other interface methods too at some point in the future.

Here’s how my Print method looks on one of my business objects.

  1. public void Print()
  2. {
  3.   string fileName = Environment.GetFolderPath(Environment.SpecialFolder.Personal) + Path.DirectorySeparatorChar +  "temp.html";
  4.   if (File.Exists(fileName)) File.Delete(fileName);
  5.   AsIObject().ServiceProvider.GetEcoService<IExpansionService>().ExpandTemplateIntoFile(new IElement[] { this.AsIObject() }, "receipt.html", fileName);
  6.   ProcessStartInfo pstart = new ProcessStartInfo("RUNDLL32.EXE");
  7.   pstart.Arguments = string.Format("MSHTML.DLL,PrintHTML \"{0}\"", fileName);
  8.   Process.Start(pstart);
  9. }

I got the details on how to print rendered html-files from http://www.robvanderwoude.com/printfiles.html#PrintHTM.

The above should give you enough to handle most of the every-day style printing efforts that your users want to do. There are no doubt countless ways code can be improved and made more generic, and I hope to hear from you how you have enhanced the basic idea.

Update: To support nested foreach-blocks, please refer to this post.

–Jesper Hogstrom

  • Share/Bookmark

Comments (3)

Jonas HögströmDecember 17th, 2008 at 12:54 pm

Nice expander. I can see one area with room for improvement… nested foreach statements :) You need to check if the expandblock contains a new {foreach}-statements, and match them with their ending-statements.

adminDecember 17th, 2008 at 6:48 pm

Doh. Yes, you are quite right. I did think about nestedness, but didn’t test it. It should be a relatively quick fix.

Dmitriy NagirnyakDecember 22nd, 2008 at 1:44 am

Nice.
But I think instead of parsing the strings manually you might want to use some engine for that that supports foreach, if and others operations.

Just from the top of my head: StringTemplate should be ideal for this.

Or maybe something like NVelocity

Leave a comment

Your comment