LavaBlast Software Blog

Help your franchise business get to the next level.
AddThis Feed Button

Simplified Chinese on Epson TM-T88IV Receipt Printer

clock January 15, 2010 14:27 by author JKealey


As you might know, our favourite teddy bear franchise is now opening stores in China. Since we’ve developed a fully integrated point of sale for the teddy bear industry, we’re helping them setup the first store over there. One of our challenges has been getting the receipt printer to recognize Simplified Chinese characters. We posted this issue on StackOverflow and contacted Epson support for the first time to try and resolve this issue but we ended up finding the solution ourselves. I’d like to share this solution with you (and some of the hoops we had to go through) as it might prove helpful to other point of sale developers out there.

 Chinese Teddy Bear Birth Certificate

For neophytes, using point of sale hardware is usually straightforward. We use the Microsoft Point of Sale SDK for .NET which is a .NET class library that interfaces with OPOS (OLE for POS). OPOS is the first widely-adopted POS device standard, allowing developers like us to write code that will work with hardware using a unified interface. I will spare you the details of how to get a handle on the printer (open it, claim it, enable it) and will focus on the actual printing portion.

string str = "this is a test";
PosPrinter printer = GetPrinter(); // open it, claim it, enable it. 
printer.PrintNormal(PrinterStation.Receipt, str);
ReleasePrinter(); // unclaim it. 


Easy enough?  This code worked for us for our stores in Québec (French), Spain (Spanish), and Denmark (Danish) and still worked for us in China when printing Latin characters, but all the Simplified Chinese characters appeared as question marks. 

Question Marks

The first thing to note is that you cannot use any plain old Epson TM-T88IV to print Chinese characters. You need the special multilingual version (which we have: TM-T88IVM). Second, you need to ensure that Epson OPOS sees it configured as the multilingual version, otherwise it won’t know it can print in simplified Chinese. In our tests, we were able to print to the printer via the sample application that comes with the Microsoft POS SDK.

Epson OPOS Configuration ms

Doing a bit of research, we found that we simply had to change the printer’s codepage (from 1252 to 936) for it to recognize the simplified Chinese characters. (Our CharacterSetList=255,437,850,852,858,860,863,865,866,936,998,999,1252 which implied that we could actually use this character set value. If we had not configured Epson OPOS to use the multilingual version, we would get an exception because 936 is not in the list.)

   1:  string str = "重新开始";
   2:  string str = "this is a test";
   3:  PosPrinter printer = GetPrinter(); // open it, claim it, enable it. 
   4:  printer.CharacterSet = 936;
   5:  printer.PrintNormal(PrinterStation.Receipt, str);
   6:  ReleasePrinter(); // unclaim it. 


However, this changed absolutely nothing. At this point, we contacted Epson support who could not help us. Our printer self tests showed the printer was capable of printing the characters, but we were still unable to print Chinese characters. Furthermore, the build-publish-test cycle was very slow because we did not have this printer on site (70 days to have it delivered from our regular supplier!) – we had to rely on our partner who was in China to help with the store setup. We tried dozens of things, but could not find the answer. We needed to have the printer on site – we had our partner ship one to us – it arrived three days later. We then decided to run another test:  printing the following website.

Some characters printed image

Interesting… some Chinese characters printed. But not where we were expecting them! I immediately realized it was encoding other (simpler) characters as Chinese characters. In this case, I took the first one above that was generated when the source string was ài. I did a few tests to confirm that àj, àk, àl were all printing different Chinese characters.

Having no knowledge of Chinese, finding the character’s unicode/decimal value in some character map was impossible for me. I had to reverse engineer the problem. 

  • à = 0xE0 in hex = 224 in decimal
  • i = 0x69 in hex = 105 in decimal
  • ài is therefore {224, 105} as confirmed by
byte[] source = Encoding.Unicode.GetBytes(text);


I looked up 0xE069 but found it was nothing. I then reloaded one of my old tests to convert this byte array to Code Page 936.

   1:  // simplified chinese
   2:  var encoding = Encoding.GetEncoding(936);
   4:  // convert the text into a byte array
   5:  byte[] source = Encoding.Unicode.GetBytes(text);
   7:  // convert that byte array to the new codepage. 
   8:  byte[] converted = Encoding.Convert(Encoding.Unicode, encoding, source);


Looking in the resulting byte array, I saw {145, 6}. However, converting this byte array back to a string and sending it to the printer did not work. It did not work because I was simply reconverting it back into a Unicode string (C#). I also did not have a PrintNormal method I could call that would accept a byte array. I therefore computed the decimal value of {145, 6} (256 * 145 + 6 = 37126) and looked it up to see it was indeed the character I was looking for ()!  I therefore implemented an ugly byte-by-byte conversion and sent it off to the printer. It worked!

   1:  StringBuilder builder = new StringBuilder();
   3:  // simplified chinese
   4:  var encoding = Encoding.GetEncoding(936);
   6:  // convert the text into a byte array
   7:  byte[] source = Encoding.Unicode.GetBytes(text);
   9:  // convert that byte array to the new codepage. 
  10:  byte[] converted = Encoding.Convert(Encoding.Unicode, encoding, source);
  12:  // take multi-byte characters and encode them as separate ascii characters 
  13:  foreach (byte b in converted)
  14:      builder.Append((char)b);
  16:  // return the result
  17:  string result = builder.ToString();

Thanks to other Stack Overflow users, I found the following concise implementation.

string result =  Encoding.GetEncoding("ISO-8859-1").GetString(Encoding.GetEncoding(936).GetBytes(text));

Thus, it appears that although the printer supports multilingual characters, one needs to re-encode them in the target codepage and then back into Latin-1 encoding for the Epson TM-T88IV Multilingual to detect it properly. The only things left for us to fix was the string padding (because these characters are twice as wide as latin characters on our printer) and finish off the receipt translation before the grand opening.


As a side note, if any of our readers have experience with controls (ActiveX or other Windows-specific applications) that allow to print to receipt printer, control the cash drawer, and receive barcodes from a barcode scanner from within a web browser, Flash, or Silverlight, please let us know.

ASP.NET callback not being fired in Internet Explorer

clock January 13, 2010 10:03 by author JKealey

Happy New Year!

Very brief news: We’ve been working hard on our software products for the last few months. We’re building a large line of business application for a service franchise and a few stores opened with our software in Ireland, Scotland, Mexico, and, more recently, China. We’ve also contributed some work to the jUCMNav open source project, which focuses on visually representing software requirements.

Today, we encountered a weird little bug in Internet Explorer and we thought it might be good to describe the workaround which we found thanks to StackOverflow.

We're using a control that uses ASP.NET callbacks (not postbacks) in our page. However, in certain circumstances, the control stops working.

Problem: ASP.NET callbacks are not fired in Internet Explorer

  • The control works fine in FireFox, Google Chrome, etc.
  • The control works fine if we do not use ASP.NET AJAX History. As soon as we call the following code, the callbacks stop working in IE (6, 7, and 8)
ScriptManager.GetCurrent(Page).AddHistoryPoint("h", "12");


  • I did some server side debugging to figure out that RaiseCallbackEvent was not fired in IE, but was fired for others.
  • I have debugged using Fiddler and observed that it was not querying the appropriate URL. The server returns an invalid request error.

    Root Cause : IE thinks the anchor (hash tag) is part of the filename.

    Other browsers send the HTTP POST to: test.aspx but Internet Explorer is sending it to: test.aspx%23&&h=12

This is because the URL in the browser's bar is test.aspx#&&h=12, because of our AJAX History Control. For some reason, it URL Encodes the hash tag, but not the rest, and appends it to the aspx filename. Other browsers don’t exhibit this behaviour.


Goal: Force IE to drop the AJAX history anchor when calling WebForm_DoCallback via JavaScript?

Technique 1: Add a query string parameter

I found that if the browser was at a certain URL such as test.aspx?test=ing that the control worked fine. This is because appending the AJAX history to this URL makes test.aspx?test=ing%23&&h=12. This implies that we’re sending an invalid test query string of (ing%23&&h=12), but since we’re not using this query string parameter, it does not really matter. However, this does give you an ugly address.

Technique 2: Change the form’s action parameter

Gabriel McAdams lead me in this direction on StackOverflow.

Reading the contents on WebForm_DoCallback, I didn't see anything that set the URL of the server call. This means that it is either getting it from the form action or sending it to the current page. Try setting the form's action attribute.

Indeed, this is the solution we were looking for. ASP.NET has a single server-side FORM element named aspnetForm (auto-generated). If you remove the hash-tag from the form’s action parameter, the control works in all browsers. I assumed that the following code in the right spot would fix the issue, but it did not since ASP.NET’s action element is auto-generated. Any changes made to action don’t seem to do anything. (By default, Action is empty, forcing the browser to decide what URL to use for the current page, which must be the root problem here.)

this.Page.Form.Action = HttpContext.Current.Request.Url.PathAndQuery; 


However, if you change it using JavaScript (here I am using jQuery to make my life easier), then your problem is solved.

<div onmousedown="$('#aspnetForm').attr('action', '<%= HttpContext.Current.Request.Url.PathAndQuery %>')">
<!-- my control is in here - I could have executed the above code after every postback, but was sufficient for my needs -->


I’m executing the code when the mouse button is pushed here (sufficient for my needs, and executed before the click event which caused my problems). A cleaner solution would have you output this automatically when necessary. Note that I had tried changing window.location.hash to an empty string before the callback, but this caused the page to scroll, left the hash character in the URL, and broke my AJAX history.

kick it on

Penniless Startup Founders

clock November 6, 2009 09:02 by author JKealey

Where will this path lead you? This post is a follow up to one of our previous posts that discussed starting a software business during the recession. In this post, I want to focus on the cash flow aspects for very early stage software startups. A few years ago, we started the company with nothing in the bank and we've managed to not only survive but prosper regardless of today's tough economic conditions. It is possible to launch a software startup with no money: the tradeoff is time. It will take longer to get out of the very early stages.

Context / Introduction

Before starting, I'd like to point out that the tips that follow are only valid in a particular context:

Understand that these tips are for the very early stage

  • Your first business goal is to get out of the very early stage as soon as possible.
  • Lots of these tips concern petty little details. However, together these details matter when at the very early stage, when you're fighting for survival.
  • Survival is a huge milestone but it isn't the end goal

You have no money and aren't interested in loans.

  • If you have no money, this is probably your first venture. I strongly feel loans are a bad idea for your first venture, but others have different opinions.
  • Cash is a great accelerator - once you've launched your first business you'll probably have a need for speed and will either have cash or be more open to debt/equity financing because you'll have already learned what you have to learn in organic growth.

You're starting a software company.

  • It is possible to start a software startup with limited cash. You've picked a good industry. If you wanted to become a dairy farmer, you would need a massive initial investment. However, for a software startup, your investment will be time writing code - not acquiring assets.

Tip 1) Sell to the right group

Since this is your first business and you have no money, you need to establish a consulting sideline selling to businesses that will give you a good return for your time (even if you're building a product for individuals). You won't be able to pay your bills selling $20 licenses to individuals in the early stage. We recommend selling customized versions of something that will help you grow your core product, as long as you can keep the intellectual property. Read more about this strategy in our previous post.

Everyone values the dollar differently. The earlier stage you are, the harder it is to define appropriate pricing given your credibility level and you don't give the same value to each dollar as your customers. As you grow, you’ll find your sweet spot and will be able to focus on your consulting clients that are right for you.

Eventually, you should aim at moving out of consulting, as it doesn't scale.

Tip 2) Minimize your expenses

No, this isn't where we started LavaBlast! Assuming you have no money, it's important that you only spend when necessary. At a high level, you need to be versatile and be able to do as much as you can on your own. Later, you'll be able to delegate but not in the early days. Of course, know your limits and get pros to do things that are impossible for you to do properly.

  • Don't hire an accountant to prepare invoices for you. Learn how to use accounting software and do it yourself. Only hire the accountant for an annual review or for real accounting work. Once you know how, it will take you a few seconds or a few minutes to do the most common tasks - you won't be paying someone four hours of work at a high hourly rate. 
  • Don't hire a law firm to review a simple non-disclosure agreement sent to you by a customer. Learn to read legal text on your own. Only hire a law firm when you've got something important to prepare or review.

There are plenty of examples of ways not to spend money when you're just starting out and have none of your own. It's time to learn things on your own.

Be smart about the commercial bank account you choose

I've dealt with a few different banks over the years. If you're a tiny business, it is good to know a few simple facts and comparison points.

Get a business account with a variable monthly fee

  • Don't bite when they offer you a $50/month fixed rate. You won't have enough transactions to make it worthwhile to upgrade. When you reach that point, switch to the fixed rate plan that is a best fit for your business. You can easily save $480 per year.
  • Some Canadian examples: Desjardins: $7/month, TD Canada Trust: $12.50/month, Royal Bank of Canada: $6/month
  • Some variable plans charge transactions on top of the minimum monthly fee. Do your homework.

Know the minimum balance you need to get it for free

    Get an ING Direct Savings Account
  • Some customers may pay you in advance or you may get grant money. Bottom line: you may end up with cash that you can't spend for a few months to a year. (Actually, you can spend it if you know more will be coming in - depends on your management style.) If you do have it in the account, earn interest on it.
  • Business accounts often don't give interest. If they do, the interest is horribly low if you don't lock it in. (Not paying service fees is often more than the amount you'd earn in interest anyways).
  • ING Direct's account is free. They have the best rates I've seen for low amounts that can be withdrawn at any time.
  • Best of all, they have a referral program. Both the new member and the referrer earn $25. In today's market, this could easily end up being worth more than the interest you'll generate in your first year. Our orange key is 33514316S1 – go ahead, signup (personal or business) and you’ll help support us and receive $25! :)

Don't get a commercial credit card for your purchases

  • Unlike personal cards, they're not free and most don't have any rewards programs.

Do you really need to accept credit cards?

  • It is a good fit for some users or services, but know the costs. If you have few transactions but most of them are high value, you're better off with a wire transfer.
  • Some banks charge you more for wire transfers than others.
  • Remember that cheques are slow - you don't have access to the funds are week.
  • You'll be paying $20-50 per month plus 2-3% per transaction. This quickly amounts to several thousand dollars.

Will you be dealing with multiple currencies?

  • We're a Canadian company but we have lots of clients in the US and in Europe. In the very early days, we chose not to open two separate bank accounts (one in each currency) because of the associated ongoing operating costs and increased accounting complexity.
  • Banks all have different exchange rates. However, I've found one bank consistently gives us a significantly worse rate when receiving transfers in another currency. A few percentage points makes a huge difference as the size of the payments increases.
  • The larger your conversion, the better your rates. Talk to a specialist like @JamesonBankTrav.

Minimize your telecommunication fees

Don't get a commercial telephone line via large companies

You'll pay much more than needed. Investigate Voice Over IP solutions such as Skype. You can get your own telephone number and free long distance in Canada + USA for an annual fee of $60. This service saves you hundreds per year. Don't get a fax unless your customers nag you for your fax phone number often enough. If you need one, look at online services such as which deliver faxes by email and give you toll-free fax numbers for less than what you'd pay to get a separate telephone line in your office for the fax, without the clutter of a deprecated device.

Don't sign-up for a massive cell phone plan if you've got empty pockets

Depending on what you do with your phone, you can save upwards of a thousand dollars a year by downsizing to a prepaid plan. Smartphones are great, but depending on your situation, it might be a wise choice to minimize those expenses. Let's hope you're not locked into a crazy-expensive three year plan! In the end, this is a personal decision which depends on your personality; once you've tasted a smartphone you may be unable to go back. Just keep in mind you might be paying much more for your cell phone than the much faster Internet connection you use all day.

Minimize your rent

Use a co-working facility

One tip often given to people starting their own company is to avoid renting office space too early in the process. Instead, work from home or from a more affordable co-working location. Not only do co-working locations reduce costs, they help you build your business because of the contacts you can make there. Once you’re read, upgrade to shared office space.

Don't minimize everything

In addition to being able to exchange services with other companies to cut costs, there are a few places where you can't afford to cut costs.

Your Image

One thing you don't want to be cheap on is branding. Your image is everything - quality needs to be high. Get nice business cards created by pros. Don't do your own web design if that's not something you specialize in. Your product will look amateurish and you'll lose sales. There are tons of affordable graphic designers out there: find one and have something nice created. Use online marketplaces such as 99designs. Since you're still a software expert, however, you should know enough HTML and search engine optimization techniques to be able to maintain your website. If you're a horrible writer, have someone review your content. This basically boils down to knowing your limits; there are some things you won't be good enough at even if you try.

Hardware & work area

We agree with Joel Spolsky's view that you should buy the best computer hardware and computer chair you can find. These are your primary tools and they are relatively inexpensive compared to your salary, even when you have very low revenue. One investment that is definitely worth it is a second monitor as it tremendously increases your productivity.

Your health

As much as your work environment is important, you should also value your health. Even if you're living on a very tight budget, don't eat hot dogs all day. Proper nutrition and good sleep cycles keep you in good health and makes you more productive. You should not be falling asleep in the afternoon. Starting your business is a marathon, not a sprint. Make sure your lifestyle is well adapted for a marathon.

Tip 3) Leverage your money

We've already covered this part in a previous blog post. Know what government funding opportunities are out there. Some require matching contributions. Some are based on your expenditures. Look around for these opportunities but mostly talk to other people to know what's out there and what's worthwhile.

Tip 4) Cash flow projections for dummies

You should always keep an eye on your cash flow, not just your revenue. I've created a very simple Excel spreadsheet to help with our cash flow projections. This one is simply a template with some random numbers in there. The one we use internally is a bit more complicated as it includes things such as currency exchange rates, taxes, etc. Build it however you like, but I've found that the two most important elements in there are:

1) Past Sales versus Projected Sales

What are my known sales (recurring revenue) versus what serious leads do I have in the pipeline. Being conservative, I base my business decisions on my past sales not my projected sales because I've learned that projected sales are often postponed. We have long sales cycles that culminate with a large sale which has a big impact on that month's revenue. Separating known sales from projected sales is of critical importance because of this because we either make the sale or get zero revenue from that customer in that month. If you're selling lots of lower value items (subscriptions to your service, for example), each individual customer has less impact on your total monthly revenue.

2) Runway

Given our current burn rate, when will we run out of cash if none of the sales in the pipeline are realized. This is useful to help you decide if you can hire and/or if you can give yourself a raise. It can also make you realize you're heading towards a problem and you need to correct the situation as soon as possible.


It would be nice to have a simple, open source, application that helps business owners track their cash flow projections in this fashion. You could go overboard and integrate it with accounting software, but I think it's nice when it's simple.


The path ends up being longer than expected When you start your first company, and you have no cash on hand, you need to focus on making money and keeping the little money you have. Survival is a major milestone, but remember that it isn't the end goal. You'll learn tons of things along the way, and once you do leave the very early stages, you'll need to manage your cash flow properly. Later on in life, you'll probably start another business - this time you hopefully won't be as strapped for cash - and you'll be able to speed up the whole process.

I'm not sure what is harder between:

  • A) Going from nothing to survival
  • B) Going from survival to success
    I do know, however, that going from nothing to survival appears a lot easier if you have cash to start off with or if you've done it before. Since success is in the eye of the beholder, it all depends on what you want to achieve.

Gotcha: gzip compression in SOAP message calls

clock August 20, 2009 10:30 by author JKealey

Some of our software systems here at LavaBlast exchange “large” data sets (only a few megabytes) in order to synchronize data between the systems. The data being exchanged is simple XML and we assumed that the SOAP calls were compressed by the built-in HTTP compression in the web server. However, checking the HTTP headers on the server when a call was made, we noticed that the Accept-Encoding header was missing. (HttpContext.Current.Request.Headers["Accept-Encoding"] did not exist).

Surprised, we turn to Google and immediately found a reference to the EnableDecompression property of the SOAP client. By setting it to true, we ensured our SOAP client was requesting gzip compression when making calls.

LavaBlastServer.Inventory centralServer = new LavaBlastServer.Inventory();
centralServer.EnableDecompression = true;

There is not much more to it. However, if you’re unaware that this setting is required, you may have clients running in the wild that don’t use gzip compression but could benefit from it.

kick it on

Multi-level contextual menus in Eclipse/GEF

clock June 22, 2009 10:46 by author JKealey


Scenario: I want my ContextMenuProvider to have multiple levels

Imagine you already have a ContextMenuProvider setup in your GEF editor but you would like to have multiple levels of actions, grouping elements together. This is one of the elements we recently had to accomplish in jUCMNav and since the we couldn’t easily find sample code on Google, we thought it would be nice to share this code with you.


Step 1) Code for the sub menu container

   1:  package seg.jUCMNav.actions;
   3:  import org.eclipse.jface.action.Action;
   4:  import org.eclipse.jface.action.IAction;
   5:  import org.eclipse.jface.action.IMenuCreator;
   6:  import org.eclipse.jface.resource.ImageDescriptor;
   7:  import org.eclipse.swt.SWT;
   8:  import;
   9:  import;
  10:  import org.eclipse.swt.widgets.Control;
  11:  import org.eclipse.swt.widgets.Menu;
  12:  import org.eclipse.swt.widgets.MenuItem;
  14:  /**
  15:   * This action contains other actions and helps create another level of
  16:   * contextual menus.
  17:   * 
  18:   * @author jkealey
  19:   * 
  20:   */
  21:  public class SubmenuAction extends Action implements SelectionListener
  22:  {
  23:      // / Who to inform when this action is fired (meaning display the submenu)
  24:      private SelectionListener actionInstance;
  26:      // the list of actions that are contained within this action
  27:      private IAction[] actions;
  29:      // should we hide the disabled ones (if not, they will appear as grayed out)
  30:      private boolean hideDisabled;
  32:      /***
  33:       * Create a submenu.
  34:       * 
  35:       * @param subactions
  36:       *            the actions that are contained within
  37:       * @param text
  38:       *            the container's textual label
  39:       * @param toolTip
  40:       *            the container's tooltip
  41:       * @param descriptor
  42:       *            the container's image descriptor
  43:       * @param hideDisabledActions
  44:       *            should we hide the disabled ones (if not, they will appear as
  45:       *            grayed out)
  46:       */
  47:      public SubmenuAction(IAction[] subactions, String text, String toolTip, ImageDescriptor descriptor, boolean hideDisabledActions)
  48:      {
  49:          // indicate that this is a secondary fly-out menu.
  50:          super("", IAction.AS_DROP_DOWN_MENU);
  52:          this.actionInstance = this;
  53:          this.actions = subactions;
  54:          this.hideDisabled = hideDisabledActions;
  56:          setText(text);
  57:          setToolTipText(toolTip);
  58:          setImageDescriptor(descriptor);
  60:          // the secondayr menu logic
  61:          setMenuCreator(new IMenuCreator()
  62:          {
  63:              public Menu getMenu(Control parent)
  64:              {
  65:                  // this would be used outside of a menu. not useful for us.
  66:                  return null;
  67:              }
  69:              public Menu getMenu(Menu parent)
  70:              {
  71:                  // create a submenu
  72:                  Menu menu = new Menu(parent);
  73:                  // fill it with our actions
  74:                  for (int i = 0; i < actions.length; i++)
  75:                  {
  76:                      // skip the disabled ones if necessary (or null actions)
  77:                      if (actions[i] == null || !actions[i].isEnabled() && hideDisabled)
  78:                          continue;
  80:                      // create the submenu item
  81:                      MenuItem item = new MenuItem(menu, SWT.NONE);
  83:                      // memorize the index
  84:                      item.setData(new Integer(i));
  86:                      // identify it
  87:                      item.setText(actions[i].getText());
  89:                      // create its image
  90:                      if (actions[i].getImageDescriptor() != null)
  91:                          item.setImage(actions[i].getImageDescriptor().createImage());
  93:                      // inform us when something is selected.
  94:                      item.addSelectionListener(actionInstance);
  95:                  }
  96:                  return menu;
  97:              }
  99:              public void dispose()
 100:              {
 101:              }
 102:          });
 104:      }
 106:      /**
 107:       * Returns how many items are enabled in the flyout. Useful to hide the
 108:       * submenu when none are enabled.
 109:       * 
 110:       * @return the number of currently enabled menu items.
 111:       */
 112:      public int getActiveOperationCount()
 113:      {
 114:          int operationCount = 0;
 115:          for (int i = 0; i < actions.length; i++)
 116:              operationCount += actions[i] != null && actions[i].isEnabled() ? 1 : 0;
 118:          return operationCount;
 119:      }
 121:      /**
 122:       * Runs the default action
 123:       */
 124:      public void run()
 125:      {
 126:          actions[0].run();
 127:      }
 129:      /**
 130:       * Runs the default action
 131:       */
 132:      public void widgetDefaultSelected(SelectionEvent e)
 133:      {
 134:          actions[0].run();
 135:      }
 137:      /**
 138:       * Called when an item in the drop-down menu is selected. Runs the
 139:       * associated run() method
 140:       */
 141:      public void widgetSelected(SelectionEvent e)
 142:      {
 143:          // get the index from the data and run that action.
 144:          actions[((Integer) (((MenuItem) (e.getSource())).getData())).intValue()].run();
 145:      }
 146:  }

Step 2) Setup your GEF ContextMenuProvider

   1:  public class UrnContextMenuProvider extends ContextMenuProvider {
   3:      private ActionRegistry actionRegistry;
   4:      /**
   5:       * @param viewer
   6:       * @param registry
   7:       *            has to be passed in case we don't want to use the action registry used in the viewer. [is this bad coding?]
   8:       */
   9:      public UrnContextMenuProvider(EditPartViewer viewer, ActionRegistry registry) {
  10:          super(viewer);
  11:          setActionRegistry(registry);
  12:      }
  14:      /**
  15:       * 
  16:       * @return the action registry used by the context menu provider.
  17:       */
  18:      private ActionRegistry getActionRegistry() {
  19:          return actionRegistry;
  20:      }
  22:      /**
  23:       * 
  24:       * @param registry
  25:       *            the action registry used by the context menu provider.
  26:       */
  27:      private void setActionRegistry(ActionRegistry registry) {
  28:          actionRegistry = registry;
  29:      }
  32:      /**
  33:       * Looks up a set of actions in the action registry. If they are enabled, adds them to the correct groups.
  34:       */
  35:      public void buildContextMenu(IMenuManager manager) {
  36:          GEFActionConstants.addStandardActionGroups(manager);
  38:      // regular action
  39:      IAction action = getActionRegistry().getAction(AddLabelAction.ADDLABEL);
  40:          if (action.isEnabled())
  41:              manager.appendToGroup(GEFActionConstants.GROUP_REST, action);
  43:      // compound action
  44:          IAction[] actions = new IAction[13];
  45:          actions[0] = getActionRegistry().getAction(AddOrForkAction.ADDORFORK);
  46:          actions[1] = getActionRegistry().getAction(AddAndForkAction.ADDANDFORK);
  47:          actions[2] = getActionRegistry().getAction(AddOrJoinAction.ADDORJOIN);
  48:          actions[3] = getActionRegistry().getAction(AddAndJoinAction.ADDANDJOIN);
  50:          SubmenuAction submenu = new SubmenuAction(actions, "Path Operations", "Path operations", actions[0].getImageDescriptor(), true); 
  51:          if (submenu.getActiveOperationCount()>0)
  52:              manager.appendToGroup(GEFActionConstants.GROUP_REST, submenu);
  54:      // ... add other actions ... 
  55:     }
  56:  }

That’s all there is to it!

Eclipse Contextual Help in WizardDialog

clock June 22, 2009 10:08 by author JKealey

Although LavaBlast mainly produces .NET-based solutions, we’re currently working on usability enhancements to our favourite requirements engineering tool, jUCMNav. jUCMNav is Java-based open source project (an Eclipse plug-in) for graphical software requirements modelling built using the Graphical Editing Framework (GEF) and the Eclipse Modeling Framework (EMF).  Personally, I love the development cycle (with automatic incremental compilation) in Java/Eclipse. Writing plug-ins for Eclipse is tons of fun and there is an active community contributing to the Eclipse project (and its subprojects). However, I often find myself wanting to do something simple and having to fiddle around with 12 source code excerpts to make it work as a whole for various reasons.

  • You know how to do something for general Eclipse plug-ins but not where to hook it up to your GEF editor.
  • You know how to do something in SWT/Draw2D but not where to hook it up to your GEF editor.
  • You get burned by random conventions that aren’t clearly defined
  • Some file you would think would be included is ignored by your build script.
    In any case, I’ll post a few technical details on some of the issues we had to solve, hoping it will help someone out in the future!

Scenario: You are using a WizardDialog in your application, but the ? button does nothing.

Or: How do you set up contextual help on a WizardDialog in Eclipse?

Step 1) Create a help_contexts.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<?NLS TYPE=""?>
   <context  id="help_general" >
        <topic label="test" href=""/>


  • The name of the XML file is not important.
  • Important: DO NOT include your plug-in name in the context id (here: “help_general”)
  • Important: DO NOT include any periods in the context id (here: “help_general”, not “help.general”)
  • You may reference local help files – they don’t need to be external.


    Step 2) Reference the contexts file from your plugin.xml

    <extension point="">
             <contexts file="help_contexts.xml">
  • The contexts element has an optional plugin-id parameter. Leave this empty unless you want to contribute help contexts to another plug-in.

Step 3) Ensure help_contexts.xml is packaged with your application

  • Edit your file to ensure it includes help_contexts.xml (bin.includes = …, help_contexts.xml, …)
  • Note the Bundle-SymbolicName in your Manifest.MF (also visible in your plugin.xml editor). If none found, note Bundle-Name.  Example: com.domain.myplugin

Step 4) Set the context id in the WizardPage

public class MyWizardPage extends WizardPage
    public void createControl(Composite parent) {
        PlatformUI.getWorkbench.getHelpSystem.setHelp(parent, "com.domain.myplugin.help_general");
  • You must add this in each WizardPage, not the WizardDialog.
    Hope this helped!

YUI Compressor for Visual Studio

clock May 11, 2009 14:44 by author EtienneT

Although you don't want this for all things in life, you do want to ensure that your JavaScript and CSS files are as small as possible.  As a web programmer, a script minifier is a useful application that should be a part of your toolbelt. This article presents a simple way to hook up a popular minifer inside Visual Studio.

First you need to download YUI Compressor from Yahoo and unzip its contents anywhere.

External Tool

Now you want to make a new external tool in Visual Studio. (Tools -> External Tools...)


Then add a new tool and name it YUI Compressor.  You want to put the following values in the inputs.  Adjust the jar location depending of where you unzipped YUI Compressor.  It also assumes that java.exe is in your path.


  1. Title: Yui Compressor
  2. Command: java.exe
  3. Arguments: -jar "E:\yuicompressor-2.4.2\build\yuicompressor-2.4.2.jar" $(ItemPath) --charset "UTF8" --type js -o $(ItemFileName).min$(ItemExt)
  4. Initial Directory: $(ItemDir)
  5. Check “Use Output Window”

You can already test this.  Any problems will be presented in the Output window.  First you need to select your *.js file in the Solution Explorer.  Then in the Tool menu, select Yui Compressor.  Refresh the directory that contains your *.js file and you should have a file named *.min.js with the same prefix as your *.js.

You could do the same thing to minimize CSS just by making a new external tool with the –type argument value changed to css instead of js.


You can easily make a toolbar to quickly do this when you need it.  Go to Tools->Customize.  Then New…


Then go to the Command Tab and select Tools.


Drag and Drop the External Command 1 (or the # corresponding to your external tool if you have more than one) to the created toolbar.  Hit close and now you can put your new toolbar wherever you want.

I hope you find this useful! Setting up tools this way inside Visual Studio is a good way to speed up the development process because you don't need to memorize numerous command line parameters: everything is done with one click!

kick it on

Gotchas: Migration from IIS6 + SQL 2005 (32-bit) to IIS7 + SQL 2008 (64-bit)

clock January 12, 2009 10:47 by author JKealey

LavaBlast Holiday Retreat First of all, let me wish you a Happy New Year!

Over the past couple weeks, we've been upgrading our server to a fresh install of Windows Server 2008 64-bit with more RAM and faster drives than our old Server 2003 32-bit. To make a long story short, this upgrade was much more painful than the one we did two years ago, which went flawlessly. I experienced so many pain points that, had I written them all down, you would have a 10 page blog post to read! I'll list a few things here that I remember off the top of my head (the main issues), hoping this will help some of you out there. 

IIS 6 32-bit to IIS 7 64-bit

  • Problem: Migrating the IIS configuration. Our metabase backups were of no use because we were upgrading IIS at the same time.
    • Solution: We used msdeploy to migrate our IIS.
    • Note: We didn't use msdeploy to migrate our files or databases, as we had already rsync'ed these over to the new server (~30 gigabytes).
  • Problem: Our application pools ended up being configured as 32-bit instead of 64-bit. Our sites wouldn't load.
  • Problem: Our applications pools ended up trying to use accounts from the old machine instead of the new one.
    • Solution: We changed these manually.
    • Note: Probably related to the same issue as above.
  • Problem: I had to install ASP.NET 1.1 for one of our customers. This and/or the previous tool ended up screwing up our IIS Handler mapping configuration. The applications were trying to use either the ASP.NET 1.1 or the 32-bit ASP.NET 2.0 dlls (can't remember which).
    • Solution: Not knowing of a better way, I ended up manually changing the defaults for the whole web server to (C:\Windows\Microsoft.NET\Framework64\v2.0.50727\aspnet_filter.dll) and using the "Revert to inherited" feature of IIS7 Manager for all our apps.
    • Note: I still don't know what the best practices are for installing ASP.NET 1.1 after a higher ASP.NET is installed, without breaking everything.
  • Problem: IIS7 screwed around with the formatting of my (dozens of) web.config files. We need to figure out what changed and bring these back into
  • Problem: Some web services that we use in our scripts were returning illegal protocol errors. We had to add the following code in our Web.config to re-enable GET and POST.
        <add name="HttpGet"/>       
        <add name="HttpPost"/>    

SQL Server 2005 32-bit to SQL Server 2008 64-bit

  • Problem: Reloading the databases on the new server via the command line.
  • Problem: ASP.NET could not access the imported databases.
    • Solution: We had to manually fix the security configuration for each database on the target machine so that ASP.NET could access it.
    • Note: I wonder if this is due to the way we are restoring our databases.
  • Problem: We had SQL Server Express installed. One of our demo applications used a connection string such as "Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\NORTHWND.MDF;Integrated Security=True;User Instance=True". The full-blown version of SQL Server doesn't support user instances.
    • Solution: We restored the database into SQL Server and are using it instead of the User Instance.
    • Note: I suppose you could also install the express version side-by-side if you wanted to.
  • Problem: We installed SQL Server Reporting Services (SSRS). By default, it binds to the ~/reports of all our websites, breaking a couple of our sites that were using the ~/reports subfolder. (It also binds to the ~/reportserver folder, but we weren't using that one).
    • Solution: We had to launch the SSRS manager to change the virtual directory that was used.
  • Problem: Some of our older applications used the SQLNCLI provider, but these now return "System.InvalidOperationException: The 'SQLNCLI' provider is not registered on the local machine.".
    • Solution: Installing the client (from the SQL Server 2008 DVD) only ends up creating a new provider "SQLNCLI10" that we can use in our connection strings instead of installing the older version.
    • Solution 2: We noticed that some code using SQLNCLI10 did not work properly. System.Data.OleDb.OleDbException: The fractional part of the provided time value overflows the scale of the corresponding SQL Server parameter or column. Increase bScale in DBPARAMBINDINFO or column scale to correct this error. We ended up changing providers, as this was too much of a pain.
  • Problem: We use automatic script generation in SQL Server Management Studio (SSMS). Tools ==> Options ==> Designers ==> Table and Database Designers ==> Auto Generate Change Scripts. However, the scripts that are now generated in our local SQL2k8 databases don't always work with the SQL2k5 databases we have deployed in the wild. We'd like the scripts to be compatible with both, so that we don't have to manually remove the problematic portions.
  • Problem: SQL Server 2005 cannot open our SQL Server 2008 backups, even if their compatibility mode appears to be 2005.
    • Solution: I don't know of a solution, do you? :)

Most of our other applications were transferred easily, except for TWiki which is always a pain to install on a Windows-based server. We still haven't resolved a CSS issue, but the tool now works! I hope this helps! Best of luck to you all in 2009! Feel free to comment about the problems you experienced during your recent migrations to these technologies.

kick it on

Software usability is like eggnog

clock November 27, 2008 01:15 by author JKealey

Yesterday, I had the chance to participate in a formal usability study at the University of Ottawa. Marconi Lanna’s master’s project is an improved code difference visualization tool. I had already played with the tool in the past and found it interesting but only saw its true value during the formal usability test where I was given specific tasks to perform. I will keep the details about this tool for a later post when it becomes publicly available, but participating in the study helped me realize a few things:

  1. Creating a proper usability study is hard.
    • There are so many ways you can introduce bias in a test, it isn't funny. For this particular test, I feel that it was very close to being unbiased and most of my initial concerns were addressed.
    • One concern that does remain is the fact that the test cases were crafted by the researcher and could have been manipulated to generate a specific set of results. However, crafted test cases are required for any kind of comparison between the users. I personally feel the test was unbiased as it presented potential flaws in both the default tool and the proposed tool.
    • This particular test was for a tool used by software developers. As you probably know, some developers demonstrate productivity levels an order of magnitude higher than others.  You can imagine that this makes it hard to analyze productivity results collected during a study.
  2. Don't forget to make the test subjects communicate
    • If a subject does a task and says nothing, you don't learn 10% of what you could have learned about their thought process. In usability studies, the thought process is the most important thing to understand.
    • Everyone is different, and having them express their concerns/procedures helps you analyze the results.
    • This is not a discussion. Because of the desire for unbiased results during the questions, you cannot ask specific questions (which may lead to bias) or comment on the answers (which will definitely lead to bias). However, after everything is done and recorded, having discussions with the subject can enhance your overall understanding of what they said while thinking aloud.
  3. Working on usability is a self-improvement task that you must work on every day.
    • It's easy to continuously postpone enhancements to the the non-functional aspects (performance, security, reliability, usability, etc.) of a software system when you're a small startup.
    • You should log even seemingly superficial non-functional concerns in your bug database just as with any other issue. Customers that complain are rare and you must take their comments seriously.

Following my own advice, I want to look at one of the usability issues that I filed in our bug system a few months ago, our bug #1377. I like revealing some internal details of the issues we face and appreciate your input on our strategies. Note: if you're not interested in a real-world example at a complex usability issue, skip to the conclusion of this post as this could be a snooze-fest!

A closer look at one of our usability issues

Drink too much eggnog and you too will have usability issues Usability is one of our core values when building software. However, we sometimes slip up, as with anything! Looking back at the comments received over the last year by our users, I can clearly identify the single most important usability problem we have with our wide range of software solutions for the franchise industry. The issue is related to our party/event/appointment booking module which is a part of our franchise point of sale (POS) software. I'd like to drill down into this feature and present the usability issue and how it came to surface.

Background information

A POS system basically lets you sell items in a retail store. Our is integrated with a frequent buyer program, and we register information concerning each member in the database. Depending on the franchise brand, different information is tracked. (We can do cool things when customers are willing to provide information such as their postal or email address, but as a general rule, all fields are optional). We also have a party booking module which allows parents to book their child's birthday party at a store. The feature accomplishes two things: it acts as a calendar system (to avoid booking overlaps) and is integrated with the sale module to support deposits. Obviously, when someone books an event at a store and pays a deposit, the system needs to remember who paid what, so that when the big day comes, the deposit can be applied to the transaction.

To make a long story short, we offer a number of configuration options in the background to support different scenarios used by different franchises. Here are some examples:

  • Book a child birthday party in one of multiple rooms.  You reserve the room for a period of time for your exclusive use.
    • This is akin to booking a meeting room.
  • Book a space in one of multiple play areas. You reserve your spot in the play room, but the room is shared up to its capacity.
  • Book a particular employee for your haircut or spa services.
    • Rooms are not the only resource that can be booked. This ties in to the employee management system.
  • Book a table, a pitcher of beer, and that hot new employee to serve at your table.
  • You're booking a particular resource and pay a: 
    • Flat fee (party package)
    • Flat fee per guest
    • The fee will depend on what is bought at a later date.
  • Stores have the possibility to ask for a deposit, and if they do, it can be of a particular % of the transaction or a fixed dollar amount.
    • The store decides if deposits are refundable. (in the event of a cancellation).

As you can see, there are numerous elements at play here that make it non-trivial in our context, where we design the booking system to meet each franchise's particular booking needs. What is important at a high-level, however, is we defined the following use cases when researching the requirements for this module.

  1. (Deposit is required) A member is in the store and wants to book an event.
  2. (Deposit is required) A member is on the phone, wants to book and event and will come by later this week to pay for the deposit.
  3. (Deposit is not required) A member wants to create a booking.
  4. Employees want to peruse the list of bookings for a particular date range.
  5. Employees want to add an additional deposit amount to a transaction (regardless of the fact that deposits are required or not)
  6. Employees want to finalize an event, and apply the total deposit amounts to the transaction.
  7. An employee books a "fairy princess" birthday party for another employee, without their knowledge or consent, and invites their friends and family. The birthday boy frantically tries to cancel the event as soon as possible, to avoid long-term trauma.
  8. (and a few more, less important scenarios).

We chose to divide our scenarios contextually

We decided that the best way to solve this problem was to create two ways to access the booking information, one which is contextually associated to a store member and one for higher-level tasks.

  1. From the existing sales page in the POS (for all scenarios that work with money)
    • The cashier first selects a member from the frequent member program (or create a new account)
    • The system displays a new button called bookings
    • The cashier selects bookings and progresses through the following menus (create a new booking, additional deposit, cancellation, finalize event).
    • The system then returns back to the sale page, where the cashier can process the sale/refund.
  2. From the events page in the POS (for all scenarios that don't deal with money)
    • The system displays an Outlook-like calendar of all the events, allowing employees to peruse the events
    • Employees can view availabilities, change rooms, change employees, change date&time, enter additional event information from this page.
    • Employees can create an event without a deposit, which the system presents as being in the "Deposit Required" state.
      • This is to support scenario #2 listed above. You'd create a tentative booking, which you'd be willing to double book if the person never comes by to pay for the deposit.
      • Also, this is used by franchises that don't require deposits instead of going to the sale page, and becomes similar to a paper calendar in a hair salon.

The usability problem

After being in use for about 18 months, I can state that we need to step back and re-evaluate this feature, to improve its usability. The system works fine when used as intended, but I have observed the following issues:

  • If deposits are not required, untrained cashiers book events in the event page and never finalize them.
    • They do open up the sale page, possibly select the member, and sell whatever was bought without associating the sale with the booking.
    • This is bad because we're left with open events and can't track cancellation rates, sales per member, etc.  (bad for the store)
  • If deposits are required, untrained cashiers assume they need to go to the events module to do anything related to events.
    • They book an event, but the system marks it as "Deposit Required" and doesn't let them change the deposit amount.
    • This is bad as they are under-using the system, not following store policies, and sending in support requests to have the system explained to them. (bad for the store and bad for LavaBlast)

As a side note, I am honestly always surprised when a cashier expects to book an event, accept money but not track any information to associate the deposit with the person making the payment. That's like their store manager expecting them to come in to work and not bothering to write their name on their timesheet.

Root cause

  • We've divided the scenarios in two groups: the ones that deal with money and the ones that do not.
  • Combine this with the following facts:
    • Some stores don't use deposits. Therefore, money is only involved when finalizing the event at which point the main task is selling products/services, not dealing with events.
    • Even if we use deposits, we allow the creation of temporary bookings (in the deposit required state).

To recap, to improve usability, we've defined processes that contextually lead the cashier to performing their tasks. However, since the core starting point (deposit amounts) varies completely from one franchise store to another, our contextual division doesn't make sense.

Possible Solutions

  • Possible solutionsSolution #1: In the store events page, after selecting an event, give the option to the cashier to add a deposit, cancel or finalize the event. This would bring the user from the events page to the sale page and allow them to follow our defined processes.
    • Possible issue: We're making them switch contexts implicitly. When they're done, they would wonder why there were in the sale module instead of the events module. (do we force them back to the events module when they are done?)
    • Possible issue: they want to finalize an event that has already occurred (to get rid of "open" events, caused by the described usability issue).
  • Solution #2: In the sale page, make the booking button available even when no member has been selected. Once clicked, prompt to select a member or offer to peruse the list of bookings. Selecting an event would auto-select the member, and bring us to the usual process.
    • Can be combined with the previous solution.
    • Possible issue: This allows cashiers to do anything at any time while in the sale module and loses the contextual value of the process we've defined. One main problem in the POS industry is the constant desire to put all options on the page, flooding the user with too many options and making the POS hard to use and increases training costs.
  • Solution #3: Do the previous solutions and get rid of the top level events page.
    • If everything can be done from the sale module (and in most cases the sale module is where the cashier spends most of their day), why bother having a separate mode, that adds confusion.
    • Possible issue: cashiers won't instinctively feel that the sale page is where they want to go for event bookings, they might not even discover that events exist!
  • Solution #4: Cut the feature set to attack the root causes directly
    • If deposits are required, get rid of the feature that allows us to create temporary bookings.
      • Possible issue: people will complain that they can't do this anymore. As deposits are required, this is a case of tough love.
    • If deposits are never required, don't integrate with the sale module at all.
      • Possible issue: decrease the perceived value of the event module and lose a few potential reports (which are not in use at this point).

At this point, we'll probably end up implementing #1, #2, and #4. One of our core technological themes is usability, therefore we're willing to live with cutting a few features for an improved process. However, the issue is still up for discussion and I'd love to hear your opinions on this specific matter or on similar problems you've experience in your software startups.


We've built a flexible core to support the varied needs of franchisors. Everything works, but are we offering too many features to our customers? Should we cut back to make it simpler on everyone? In our case, I think so. If I had to sum up the usability problem discussed today into a high level lesson learned, I'd state the following: Don't separate use case scenarios into different groups if a configuration option invalidates the grouping premise.

During this holiday season, I think I'll take some time to re-read a few of my favorite books on usability:

  1. Steve Krug - Don't Make Me Think
  2. Joel Spolsky - User Interface Design for Programmers

Although these are great reads, they deal with the low-level user interface design problems, not with higher-level software engineering feature interaction problems in software product lines. I'd be curious to know if there any good resources on this subject (other than overly academic conference papers/workshops). I'd love to hear more first-hand, real-world experiences from other bloggers.

Easy to use software is like eggnog.

  • Reaching the desired results takes time. (The best eggnog takes at least three weeks to make).
  • Users will get a kick out of it. (With as much bourbon as milk, eggnog will definitely get people talking.)
  • At a high level, anyone can claim to be a pro. However, the devil is in the details.
  • You need to listen to your users to make the recipe better in the future.
  • What would the holiday season be without it? (okay, that doesn't make sense!)
kick it on

Gotcha: ASP.NET and exceptions in asynchronous tasks

clock October 30, 2008 13:00 by author JKealey

Awesomeness Here's a little piece of information you might not know about how ASP.NET 2.0 and above operate. Simply put, if an unhandled exception occurs in a secondary thread which has been launched by your ASP.NET application, IIS will force your application to restart. This can mean lost sessions, bad server side state, slow reloads and, in general, is a bad thing. However, these kinds of exceptions are a pain to discover and resolve if you don't know what you're looking for because the built-in ASP.NET error handler does not get fired and your user is not redirected to an error page, unless something breaks after the web server restarts. (This article explains the cryptic messages you will see in the Event Log).

Here's a simple scenario based on the integration between our interactive kiosk created for The Code Factory co-working space and Twitter. We want our web application to post messages on Twitter when certain events occur (member check-in and member check out). Because we did not want to re-invent the wheel, we found a Twitter C# Library with a permissive software license. Noticing that posting to Twitter slowed down our application and because we didn't want our application to depend on Twitter's availability, we decided to run this code asynchronously. Being careless, we supposed that should anything bad occur in this second thread (timeouts, invalid login/password, etc.), the system will simply not be able to post, which was not a problem for us. We were wrong, and the whole web application restarted because of this. (This was not the case in ASP.NET 1.x and was previously discussed by others).

In any case, here's some sample code to run the Twitter library asynchronously, including a try/catch.

using System;
using System.Collections.Generic;
using System.Text;
namespace LavaBlast.Util.Twitter
    public class AsyncTwitter
        public void Update(string userName, string password, string status)
            TwitterDelegate caller = new TwitterDelegate(UpdateTwitter);
            caller.BeginInvoke(userName, password, status, new AsyncCallback(CallbackMethod), caller);
        protected delegate void TwitterDelegate(string userName, string password, string status);
        protected static void CallbackMethod(IAsyncResult ar)
            TwitterDelegate caller = (TwitterDelegate)ar.AsyncState;
        protected static void UpdateTwitter(string userName, string password, string status)
                Twitter t = new Twitter();
                t.Update(userName, password, status, Twitter.OutputFormatType.XML);
            catch (Exception ex)
                LavaBlast.ElectronicJournal.Error("Unable to post to twitter.", ex);
                // ignore. 

The lesson learned is unhandled exceptions in other threads can wreak havoc on your ASP.NET application. To help debug these kinds of errors when they occur, I do suggest you setup the HttpModule developed by Peter A. Bromberg that adds the actual exception in your Event Log. Peter describes the problem in more detail than I do, and is worth a read.

Have a Happy Halloween! (Halloween will be weird for us in 2008 because we were hit by the year's first snowstorm on Tuesday.)


kick it on

Month List


The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2017

Sign in