Switching ISPs

After several years of rather satisfying service from Telia I decided - on a whim - to switch to Tele2.

Actually, there were a few things that annoyed me on Telia. One was that I didn’t manage to change the machine that was connected to the ADSL connection. I followed all instructions I could find and even called support. All I had to do was switch off the modem for 20 minutes and turn it back on with a new box plugged in. Never worked. Three hours. Still no new IP. Of course, it may have been just me, but that has never stopped anyone from being annoyed before.

The other thing was that I upgraded to 24Mbit/s, paid more, but they didn’t send me a new modem. I also had a feeling that new customers got better deals than me.

So, I switched. The switch was surprisingly painless. The target date was April 30th, and I expected all connections to go down and I’d have to install the new shiny modem I received from Tele2. Nothing of the kind! All of a sudden my dhcp address changed and that was it!

Then my wife started complaining about her mail not reaching her friends. My first suspicion was user error, but then I realized I had set up my qmail mail server to route via Telia’s smtp-server. Doh. I can’t blame Telia for not routing mail from Tele2 customers.

The neat thing about my linux servers is that they never crave any attention. That’s also a bad thing. While documentation is fun of the highest degree, my own network is slightly less than 6 sigma standard in that regard. Luckily I remembered that all I had to do was change the server name in the /var/qmail/control/smtproutes file.

Unfortunately Tele2 have blocked all outbound traffic on port 25. Instead they require mail clients to use port 587 and also to authenticate.

I had received a paper with a user name and password. I also set up a mail account with a new password. Furthermore I spent significant time researching how to shove that into the smtproutes file.

All to no avail. Support closes at 8 pm, but then I’ll try to explain my problem. The support site mentions very little about how to configure an smtp-server, though it does mention a lot about configuring mail clients. They also have a robot help desk service named Sara, where you can presumably ask questions in natural language. She claims to answer questions about internet, broad band and so on. I asked “How can I relay from my smtp server?” and got the reply “Server… Not my thing really. I don’t know much about hardware”.

Tomorrow I’ll bother tech support. That’ll be fun…

–Jesper

Share/Save/Bookmark

This Week in Virology

Whenever I take off on an off-line session, I spend some time to find interesting podcasts to fill up my mp3 player with.

This time I ran into This Week in Virology, http://www.twiv.tv/. It’s about viruses, not computer viruses, but “the kind that makes you sick”.

I have no prior education in virology, but this podcast made it very interesting and accessible, and on a level that discusses receptor molecules, gene sequences and RNA strands - in other words significantly deeper than your ordinary daily news paper. A whole new world opened, and I immediately subscribed to it when I got back home.

If virology and/or molecular biology is something that interests you, I really recommend this show. It’s weekly, one hour long and consists of recent news in virology, interview with a leading researcher and then weekly picks of books and other science podcasts. Everything done in a down to earth discussion style, with enormous amounts of information to learn!

–Jesper Hogstrom

Share/Save/Bookmark

Send me to oz

I have applied for a new job. I want to be island keeper in Australia. That may sound like a joke, but it is for real. Those crazy oz people dreamed up a fantastic publicity stunt, where they hire someone to basically stay in a luxuary resort for six months to do nothing but do interviews and go on excursions. They pay nicely too.

I think I am well suited for the job (given that … eh… I am an excellent programmer, quite a nice guy and appreciate Australian movies like Crocodile Dundee). However, ten thousand other people also think they are the perfect fit.

Here’s how you can help me. Visit my application and vote. The star to aim for is the right-most. Be sure not to click any of the four left-most stars as high ranking will be of the essence. You want them all to be yellow when you click that button!

Feel free to pass this instruction on to trusted friends or anyone who’s willing to watch the one minute video and vote.

Obviously, beer’s on me when you come visit, mate!

Now, off to vote while I start packing.

Best regards,

–Jesper Hogstrom

Share/Save/Bookmark

How to write a CCNet publisher in C#

Anyone doing development and want to be serious about need a build machine. If you’re not only into being serious but also want some fun, why not try enhance the infrastructure by for instance writing your own CCNet plugins?

After some experimenting it turns out to be really simple. All you need to do is implement the interface ITask. There are however a few more things to keep in mind.

Before I start, let me say that everything I learned about this comes from looking at the source of CruiseControl.Net and also peeking at the Twitter Publisher by Thomas Freudenberg.

Your assembly must have a name on the pattern ccnet.*.plugin.dll. The location of the dll must be the same as the ccnet server directory (typically c:\Program Files\CruiseControl.NET\server). During development, feel free to name that as your output directory.

The namespace and class name are not important. However, you need to add an attribute, ReflectorType, to the class. The constructor takes a string. That string is the node you specify in ccnet.config.

I think it is time for an example.

  1. using ThoughtWorks.CruiseControl.Core;
  2. using System.Windows.Forms;
  3. using Exortech.NetReflector;
  4.  
  5. namespace any.name.isvalid
  6. {
  7.   [ReflectorType("mynewpublisher")]
  8.   public class ClassNameNotImportant : ITask
  9.   {
  10.     public void Run(IIntegrationResult result)
  11.     {
  12.       MessageBox.Show(string.Format("yehaa {0} - {1}", result.ProjectName, result.Status));
  13.     }
  14.   }
  15. }

This can be tested by the following configuration file:

  1. <cruisecontrol xmlns:cb="urn:ccnet.config.builder">
  2.   <project name="MyFirstProject" >
  3.     <publishers>
  4.       <mynewpublisher/>
  5.     </publishers>
  6.   </project>
  7. </cruisecontrol>

When you connect your cctray to the project and force it you’ll get a happy message box. Please refrain from using actual message boxes on your server! This is something you should try at home, not at work :)

There are times when your publisher needs some configuration. This is easily accomplished by more attributes;

  1. [ReflectorProperty("user", Required = true)]
  2. public string User { get; set; }

The config file now looks like

  1. <publishers>
  2.   <mynewpublisher>
  3.     <user>jesper</user>
  4.   </mynewpublisher>
  5. </publishers>

If you specify Required=True you will get a startup failure if the xmlnode is not specified!

Some properties are numbers (he said numbly). Fear not, just adorn the adornment some:

  1. [ReflectorProperty("intvalue", Required = true, InstanceType=typeof(int))]
  2. public int intvalue { get; set; }

There is also the odd chance you need a list of items as a property to your publisher. That’s a tad bit more involved, but here goes. First, the config file we want to specify looks like:

  1. <mynewpublisher>
  2.   <user>jesper</user>
  3.   <intvalue>33</intvalue>
  4.   <recipients>
  5.     <recipient name="jesper"/>
  6.     <recipient name="jonas"/>
  7.   </recipients>
  8. </mynewpublisher>

In other words, a list of recipients that each have a name. First, the property on the publisher-class:

  1. [ReflectorHash("recipients", "name")]
  2. public Hashtable Recipients { get; set; }

To make sure we don’t inadvertently hand out nulls we need to add a constructor as well

  1. public ClassNameNotImportant()
  2. {
  3.   Recipients = new Hashtable();
  4. }

We also need to define the class to hold each item. Luckily it is simple.

  1. [ReflectorType("recipient")]
  2. public class Recipient
  3. {
  4.   [ReflectorProperty("name")]
  5.   public string Name { get; set; }
  6. }

Test it in your Run-method.

  1. public void Run(IIntegrationResult result)
  2. {
  3.   MessageBox.Show(string.Format("Recipients: {0}", Recipients.Count));
  4.   foreach (Recipient r in Recipients.Values)
  5.     MessageBox.Show(r.Name);
  6. }

There are a few more attributes that can be used, but I must admit I haven’t investigated them thoroughly. The NetReflector package is available on sourceforge here.

At any rate, this article should be enough to get you started. I’ll definitely start implementing some of my publisher ideas - the Twitter Publisher is admittedly already done, but it was only number three on my list.

–Jesper Hogstrom

Share/Save/Bookmark

Development and energy

I recently read the book “Olja” (ISBN 91-7232-043-5) by Gunnar Lindstedt. Unfortunately it is only available in Swedish.

The book is about what is known as Peak Oil, i.e. the point in time when we have extracted 50% of the world’s oil supply. According to the experts interviewed in the book that point is here or very near. Actually, the date varies from “just passed it” to “within 10 years”. I’d say that is rather irrelevant. I firmly believe that the amount of oil is finite (if for nothing else the volume of the planet is finite) and it seems reasonable to believe the experts that the half-way point is near. Thus the time for action is close.

In the book mr Lindstedt not only explains details about oil extraction ond consumption, he also parallells this with backdrops from a “middle earth” era. His youth and his grand parents youth. So much has changed in the last 100 years it’s almost unbelievable, and not only in terms og technology.

Mr Lindstedt suggests that the curve of the western world’s improved efficiency in production is virtually identical to the curve of oil import. That idea took me by surprise, and after pondering it it does make sense.

Way back when a man and his horse would till a field in a day or two (or whatever). Now, a man and his tractor does it significantly faster, and things look more efficient on the surface. However, that doesn’t take into account the hidden addition of a few hundred litres of diesel.

So, thinking about it, have we made significant improvements in terms of process, or are all our increases in efficientcy by shortening the time and adding energy? Somewhere in the back of my mind a page from a physics book lurks, mumbling something about efficiency as work x time. As long as we only count human power as “work” and forget about the work fossil fuel does we look a lot more efficient, but that seems like a small white lie…

Scanning the net I found a link to http://www.eia.doe.gov/ipm/supply.html with the oil production of various countries  and summaries.

oilproduction

It seems the world is indeed reducing its production and it s worth noting that the current economic slump/crisis/depression is not in these numbers, as they only include up to 2007.

Interesting read and lots of food for thought. For further reading, google for peak oil.

–Jesper Hogstrom

Share/Save/Bookmark

Subtitles to your movies

There are several times you may want to add subtitles to movies. The question is how to find them. I usually use google but there are plenty of sites that offer the goods (the subtitles, not the movies!)

Have fun!

–Jesper Hogstrom

I

Share/Save/Bookmark

Gain ssh access to ReadyNAS

In my server closet I run a Netgear ReadyNAS+ with 1.5TB of effective storage. It’s a really neat box with support for various access methods, like nfs, ftp, http and cifs and a few more.

My latest informational reorg included an idea to publish sound and video on a share that the kids could access. All that stuff used to sit on a share deep in a structure from a previous box I used to run.

Easy thing to figure out what to do; simply create a new share, call it Media and allow all the internal network read-access.  Mount it on a linux box with root access, then move everything from the old jesper/backup/jesper/bigdrive/private/media to that directory.

The only thing is that it’s not possible to simply rewrite the inodes when moving things from one nfs-mounted volume to another nfs-mounted volume. Every single bit needs to be copied.

It was a somewhat lengthy procedure to move everything onto the NAS, but at that time I had a gigabit card (still have it but it is not recognized by vmware esxi) and data was only going one way - from one box onto the NAS. This time, I was stuck with a 100MBit nic and data had to travel from the NAS, to the computer and then back to the NAS. Admittedly I didn’t have to watch, but I figured it would take me days to shuffle all of that data.

As the ReadyNAS runs a linux distro behind the scenes I figured the solution would be to gain terminal access to the NAS using ssh. Unfortunately that wasn’t on by default. I googled and found several solutions to fix the problem, ranging from opening the NAS and mounting the disks on another computer to using some odd “feature” in Apple File Protocol and hacking the crontab file.

Eventually I found a much simpler solution: Installing the official ‘enable ssh access’ patches. :)

On http://www.readynas.com/?page_id=94 you’ll find - at the bottom of the page - two patches; ToggleSSH and EnableRootSSH. Both need to be installed, and the NAS wants to reboot after each one.

ssh into the nas. Log in as root with your existing admin password. Voila! you’re in!

Now moving the remaining data was a few keystrokes away and obviously lightning fast using the good ol’ mv command.

Happy hacking!

–Jesper Hogstrom

Share/Save/Bookmark

See you in Motala

Better late than never. We finally got around to register to VätternRundan. It’s a 300 kilometer bicycle trip around lake Vättern in the middle of Sweden. This year me and Jonas will do our third trip and we somehow managed to trick our brother Christer into joining us.

It’s going to be great fun. We start around one am which isn’t my favorite time - I prefer starting earlier, as it is impossible for me to nap during the evening.

Last year we started about that time too, and we had all intentions of napping. We parked the car in central Motala near a lawn. Had dinner, looked at the commotion at the city square and all the cool bicycle gear up for sale. Went back to the car, unpacked our sleeping bags and lay down on the lawn, set the alarm and hoped to get some sleep.

After 5 minutes the first drops came down. I tried to pretend it didn’t rain. Another 10 minutes of increasing precipitation  and I said to Jonas that we’ll stay the course - it will end soon. Yeah, right… We spent the next two hours in the car while the rain poured down…

Luckily it cleared up and the weather during the ride was excellent.

Anyway, time to get mentally ready for those 100km training trips - and of course gather some nice stuff to listen to. Last year I found a 10 hour series about the Greek-Persion wars which was very interesting. Given the fact that I have forgotten most of it I might as well listen to it again :)

See you in Motala!

–Jesper Hogstrom

Share/Save/Bookmark

Nested foreach in ECO report generator

As Jonas pointed out in a comment to my last post, it was not possible to nest foreach-loops in the report.

One minor change was required to support that, namely the locating of the position of the - matching - {/foreach}.

Original code looked like:

  1. if (toExpand.StartsWith(startForeach))
  2. {
  3.   endPos = s.IndexOf(endForeach, endPos);   // <– This line finds the next {/foreach} - not the matching one
  4.   string repeatBlock = s.Substring(i + toExpand.Length + 2, endPos - i - toExpand.Length - 2);
  5.   expanded.Append(ExpandBlock(roots, toExpand, repeatBlock));
  6.   i += toExpand.Length + repeatBlock.Length + endForeach.Length + 1;
  7.   break;
  8. }

Instead, that has to change to

  1. if (toExpand.StartsWith(startForeach))
  2. {
  3.   endPos = GetMatchingEndPos(s, endPos);
  4.   if (endPos == -1)
  5.   throw new InvalidOperationException(string.Format("Unable to find matching /foreach after start on pos {0}", i));
  6.   string repeatBlock = s.Substring(i + toExpand.Length + 2, endPos - i - toExpand.Length - 2);
  7.   expanded.Append(ExpandBlock(roots, toExpand, repeatBlock));
  8.   i += toExpand.Length + repeatBlock.Length + endForeach.Length + 1;
  9.   break;
  10. }

Again, interesting lines of code somewhere else (the method was already way too long for my taste anyway!}

  1. private int GetMatchingEndPos(string s, int startPos)
  2. {
  3.   int result;
  4.   int openCount = 1;
  5.   do
  6.   {
  7.     result = s.IndexOf(endForeach, startPos);
  8.     int nextOpen = s.IndexOf(startForeach, startPos);
  9.  
  10.     if (nextOpen == -1 || nextOpen > result)
  11.     {
  12.       openCount–;
  13.       startPos = result + 1;
  14.     }
  15.     else
  16.     {
  17.       openCount++;
  18.       startPos = nextOpen + 1;
  19.     }
  20.   } while (openCount > 0);
  21.   return result;
  22. }

What the code does is look for the next open and close token. If the first token is “open”, increase the openCount. If the next token is “close”, decrease openCount. Next time, start looking after the first token found. If the openCount is zero, we’ve found ourselves a match.

This can be tested with template strings like

{foreach:Person.allInstances}
  outer {Person:self.fullName}
  {foreach:Person.allInstances}
  inner {Person:self.fullName}
  {/foreach}
  {foreach:Person.allInstances}
  inner2 {Person:self.fullName}
  {/foreach}
{/foreach}

i.e. a sequence of “start start stop start stop stop”. The sequences I alread had works fine still, as well as “start start stop stop”. Should be pretty safe.

–Jesper Hogstrom

Share/Save/Bookmark

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.Length - 2);
  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/Save/Bookmark

←Older