2008-05-15

Refactoring a big if block into a simple command processor using attributes

Recently someone had a problem where they had some massive control block full of if statements looking at a string, dispatching one of a variety of functions. The if block was massive. Hundreds of if statments, hundreds of magic strings.

Interestingly all the functions had the same signature... So I gave him this example of how to use attributes on the methods to specify the corresponding token, then we use Reflection to scan the assembly for all the functions with that attribute, then create a function table keyed by thier token, to privde fast lookup. This example shows how to creat an object instance and then invoke the method via reflection, but this could be made much simpler if the methods were all static and the function protoype was part of an interface instead of just a unspoken convention.

Here's the "Before" example from the original question...


string tag;
string cmdLine;
State state;
string outData;

...

if (token == "ABCSearch") {
ABC abc = new ABC();
abc.SearchFor(tag, state, cmdLine, ref outData);
}
else if (token == "JklmDoSomething") {
JKLM jklm = new JKLM();
jklm.Dowork1(tag, state, cmdLine, ref outData);
}


A couple of notes:

  • There is no correlation between the token and the class name (ABC, JKLM, ...) or the method (SearchFor, Dowork1).
  • The methods do have the same signature:
    void func(string tag, State state, string cmdLine, ref string outData)
  • The if ()... block is 500+ lines and growing



And here is my example command processor (as a console app):


using System;
using System.Collections.Generic;
using System.Reflection;

namespace ConsoleApplication2
{
public class Program
{
static void Main(string[] args)
{
while(true)
{
Console.Write("[e(x)ecute, (t)okens, (q)uit] -> ");
string s = Console.ReadKey().KeyChar.ToString().ToLower();
Console.WriteLine();

switch (s)
{
case "q":
Console.WriteLine("Finished.");
return;

case "t":
Console.WriteLine("Known tokens:");
foreach (string tokenName in CommandProcessor.GetTokens())
{
Console.WriteLine(tokenName);
}
break;

case "x":
string token = string.Empty;
string tag = string.Empty;
string cmdLine = string.Empty;
string state = string.Empty;

Console.Write("token: ");
token = Console.ReadLine();
Console.Write("tag: ");
tag = Console.ReadLine();
Console.Write("cmdLine: ");
cmdLine = Console.ReadLine();
Console.Write("state: ");
state = Console.ReadLine();

try
{
string output = CommandProcessor.DoCommand(token, tag, cmdLine, State.GetStateFromString(state));
Console.WriteLine("Output:");
Console.WriteLine(output);
}
catch (TokenNotFoundException ex)
{
Console.WriteLine(ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Unknown error occured during execution. Exception was: " + ex.Message);
}
break;

default:
Console.WriteLine("Unknown command: {0}", s);
break;
}
}
}
}

public class CommandProcessor
{
// our dictionary of method calls.
internal static Dictionary availableFunctions = new Dictionary();

static CommandProcessor()
{
SetupMethodCallDictionary();
}

private static void SetupMethodCallDictionary()
{
// get the current assembly.
Assembly assembly = Assembly.GetExecutingAssembly();

// cycle through the types in the assembly
foreach (Type type in assembly.GetTypes())
{
// cycle through the methods on each type
foreach (MethodInfo method in type.GetMethods())
{
// look for Token attributes on the methods.
object[] tokens = method.GetCustomAttributes(typeof(TokenAttribute), true);

if (tokens.Length > 0)
{
// cycle through the token attributes (allowing multiple attributes
// leaves room for backwards compatibility if you change your tokens
// or consolidate functionality of the methods. etc.
foreach (TokenAttribute token in tokens)
{
// look for the token in the dictionary, if it's not there add it..
MethodInfo foundMethod = default(MethodInfo);
if (availableFunctions.TryGetValue(token.TokenName, out foundMethod))
{
// if there is more than one function registered for the same
// token, just keep the last one found.
availableFunctions[token.TokenName] = method;
}
else
{
// add to the table.
availableFunctions.Add(token.TokenName, method);
}
}
}
}
}
}

public static string DoCommand(string token, string tag, string cmdLine, State state)
{
// the data returned from the command
string outData = string.Empty;
MethodInfo method = default(MethodInfo);

// see if we have a method for that token
if (availableFunctions.TryGetValue(token, out method))
{
// if so, create an instance of the object, and then execute the method,
// unless it's static.. in which case just execute the method.
object instance = null;
if (!method.IsStatic)
{
// this just invokes the default constructor... if you need to pass
// parameters use one of the other overloads.
instance = Activator.CreateInstance(method.ReflectedType);
}

object[] args = new object[] { tag, state, cmdLine, outData };

method.Invoke(instance, args);
outData = (string)args[3];
}
else
{
throw new TokenNotFoundException(string.Format("Token {0} not found. Cannot execute.", token));
}
return outData;
}

public static IEnumerable GetTokens()
{
foreach (KeyValuePair entry in availableFunctions)
{
yield return entry.Key;
}
}
}

public class State
{
public State(string text)
{
_text = text;
}

private string _text;

public string Text
{
get { return _text; }
set { _text = value; }
}

public static State GetStateFromString(string state)
{
// implement parsing of string to build State object here.
return new State(state);
}
}

[AttributeUsage(AttributeTargets.Method)]
public class TokenAttribute : Attribute
{
public TokenAttribute(string tokenName)
{
_tokenName = tokenName;
}

private string _tokenName;

public string TokenName
{
get { return _tokenName; }
set { _tokenName = value; }
}
}

[global::System.Serializable]
public class TokenNotFoundException : Exception
{
//
// For guidelines regarding the creation of new exception types, see
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
// and
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
//
public TokenNotFoundException() { }
public TokenNotFoundException(string message) : base(message) { }
public TokenNotFoundException(string message, Exception inner) : base(message, inner) { }
protected TokenNotFoundException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context)
: base(info, context) { }
}

public class ABC
{
[Token("ABCSearch")]
public void SearchFor(string tag, State state, string cmdLine, ref string outData)
{
// do some stuff.
outData =
string.Format("You called ABC.SearchFor. Parameters were [tag: {0}, state: {1}, cmdLine: {2}]", tag, state.Text, cmdLine);

}
}

public class JKLM
{
[Token("JklmDoSomething")]
public void Dowork1(string tag, State state, string cmdLine, ref string outData)
{
// do some other stuff.
outData =
string.Format("You called JKLM.Dowork1. Parameters were [tag: {0}, state: {1}, cmdLine: {2}]", tag, state.Text, cmdLine);
}
}
}

How to get information about your current culture.

Instead of doing a college survery and asking a bunch of probing questions about the lives of twenty-somethings, there's an easier way to get information about your current culture. Just look at CultureInfo.CurrentCulture.

Here's a quick program that explains how to do that. This can be very useful in debugging and troubleshooting how your program behaves on machines that are setup for other laungages or regions.


using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
CultureInfo currentCulture = CultureInfo.CurrentCulture;

Console.WriteLine("CultureInfo");
Console.WriteLine("-----------");
Console.WriteLine("DisplayName: {0}", currentCulture.DisplayName);
Console.WriteLine("Name: {0}", currentCulture.Name);
Console.WriteLine("LCID: {0}", currentCulture.LCID);
Console.WriteLine();

Console.WriteLine("NumberFormatInfo");
Console.WriteLine("----------------");
Console.WriteLine("Decimal Seperator: {0}", currentCulture.NumberFormat.NumberDecimalSeparator);
Console.Write("Digits: ");

foreach (string s in currentCulture.NumberFormat.NativeDigits)
{
Console.Write(s + " ");
}

Console.WriteLine();
}
}
}




Base output should look like:


CultureInfo
-----------
DisplayName: English (United States)
Name: en-US
LCID: 1033



NumberFormatInfo
----------------
Decimal Seperator: .
Digits: 0 1 2 3 4 5 6 7 8 9

Filtering a network stream using a wrapper

So not that long ago, someone posted a question asking how to deal with a certain situation. The situation is such that there is a network file stream coming from somewhere, that has certain data you want to keep, and certain data you don't want to keep. Control blocks, extra header information, weirdo protocol, too much data coming back form an API, etc..

My suggestion was to create a simple container object (aka wrapper) to the existing network stream, that operates the same as the network stream, but does the necessary filtering.

Here's an example of how you'd use it, and and example base class implementation of for the filters follows it. In the actual problem case example, he was dealing with a NetworkStream that contained Xml data in irregular chunks, with control blocks as fixed headers. Each header indicates how much XmlData follows. The filter will remove the headers as needed, presenting a simple stream of Xml data to the XmlReader to parse.

I've left out the concrete implementation that actually parses the stream, and here you just have the FilteredNetworkStream base class and an idea of how to use it once you implement it. All that's left for the implementer is to override the abstract method FilterBeforeRead, which contains the customized filtering logic for the particular situation.



using (NetworkStream inputStream = GetNetworkStreamFromSomewhere())
using (StreamWriter outputStream = new StreamWriter(@"C:\Path\To\File.xml", false))
{

XmlReader reader = XmlReader.Create(new FilteredNetworkStream(inputStream));
while (reader.Read())
{

// method returns empty string if current data is discardable
string outputData = GetDesiredDataFromReader(reader);

if (!string.IsNullOrEmpty(outputData))
{

// save desired data to local file
outputStream.Write(outputData);
}
}
}


Here's the base class:


public abstract class FilteredNetworkStream : Stream
{
public FilteredNetworkStream(NetworkStream baseStream)
{
_baseStream = baseStream;
}

protected NetworkStream _baseStream;
public abstract void FilterBeforeRead();

#region Stream Implementation

public override bool CanRead
{
get { return _baseStream.CanRead; }
}

public override bool CanSeek
{
get { return _baseStream.CanSeek; }
}

public override bool CanWrite
{
get { return _baseStream.CanWrite; }
}

public override void Flush()
{
_baseStream.Flush();
}

public override long Length
{
get { return _baseStream.Length; }
}

public override long Position
{
get
{
return _baseStream.Position;
}
set
{
_baseStream.Position = value;
}
}

public override int Read(byte[] buffer, int offset, int count)
{
this.FilterBeforeRead();
return _baseStream.Read(buffer, offset, count);
}

public override long Seek(long offset, SeekOrigin origin)
{
return _baseStream.Seek(offset, origin);
}

public override void SetLength(long value)
{
_baseStream.SetLength(value);
}

public override void Write(byte[] buffer, int offset, int count)
{
_baseStream.Write(buffer, offset, count);
}

#endregion
}

2008-04-29

Getting the field list returned from an ad-hoc Sql query

So, recently I needed to make an application that allowed a user to enter an arbitrary Sql query, and elsewhere in the UI I needed to display a drop-down with the fields that this arbitrary query returned.

This poses a small problem. It's very simple if the user is doing simple queries, that don't take long to execute. You could just run the query, then, take the first result, and get the list of fields. Well.. This works for simple queries that return small result sets, but we needed to put in queries that potentially return as many as 48 million results, using complex queries including joins between multi-million rowed tables, aggregates, and that sort of thing..

In other words, the queries are slow. Really slow. They create a lot of UI lag when I go to get the field names for the drop down box.

My first attempt was to take the query and wrap it up like this:

SELECT TOP(1) * FROM ( // original query here // ) fieldNamesTable

My thinking was that if I specified that I only wanted the first record it's be really quick, even with a complex query. This is true. It's must faster, but it's still slow. Too slow. A lot of UI lag still remained.

So, my second attempt worked much better. I wrapped the query again, but now it looks like:

SELECT * FROM ( // original query here // ) fieldNamesTable WHERE 1 = 0

Instead of specifying I wanted the first record, I put a phrase in the WHERE clause that will always be false. The Sql Server's query execution engine realizes that, and so it knows that the query will never be able to return data. So it immediately returns with 0 results. But I get the field names!! This is SUPER fast!

Enjoy,
Troy

2008-03-24

An amusing bought with the DataGridView control

As usual, WinForms GUI programming is a terrible PIA. Even worse is the flagship of all controls the great beast known as the DataGridView. Working with the DataGridView bends your mind like offensive cutlery at Uri Geller's dinner table.

During my most recent encounter with this control of the third kind, I needed to use a ComboBox column, and that column needed to have an effect on the contents of the other controls. Normally, you could just hook up a cellvaluechanged event or something of that nature, but that doesn't work out on a ComboBox control in a DataGridView column... There's no event that would fire when a user selected an item in the dropdown. Only after the user selected the item, then refocused on some other control.

That was annoying. Too many clicks for the user! When I select the item in the dropdown, the row should react, I shouldn't have to click elsewhere.

So here's a quick example of what I did to get that working. In the example, we handle the datagridview's EditControlShowing event, then grab a reference to the combobox, then unwire any previous events we may have hooked up to SelectionChangeCommitted, then wire the event. In the SelectionChangeCommitted event we call _dataGridView.EndEdit() to effect the other rows.

Enjoy!



public class Example
{
///
/// Constructor
///

public Example()
{
_dataGridView = new DataGridView();

// setup the datagridview here.
DataGridViewComboBoxColumn fooColumn = new DataGridViewComboBoxColumn();
fooColumn.Name = "Foo";
fooColumn.ValueType = typeof(String);
fooColumn.HeaderText = "Foo";
fooColumn.Items.Add("Bar");
fooColumn.Items.Add("Baz");
fooColumn.Items.Add("Fizz");
fooColumn.Items.Add("Buzz");
fooColumn.Items.Add("FizzBuzz");
fooColumn.DefaultCellStyle.NullValue = "Bar";

_dataGridView.Columns.Add(fooColumn);

// hook up editing control showing event
_dataGridView.EditingControlShowing += new DataGridViewEditingControlShowingEventHandler(_dataGridView_EditingControlShowing);

// create a delegate for the method that will handle the event
_comboBoxSelectDelegate = new EventHandler(combo_SelectionChangeCommitted);
}

private DataGridView _dataGridView;
private EventHandler _comboBoxSelectDelegate;

void _dataGridView_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
// get the control from the event args.

ComboBox combo = e.Control as ComboBox;


if (combo != null)
{
// remove the event subscription if it exists.
combo.SelectionChangeCommitted -= comboSelectDelegate;

// add a subscription to the event
combo.SelectionChangeCommitted += comboSelectDelegate;
}
}

void combo_SelectionChangeCommitted(object sender, EventArgs e)
{
// handle the event, and end edit mode
_dataGridView.EndEdit();
}
}

2008-03-07

Black Box OMG it's addictive.

BlackBox - A simple puzzle game where you shoot rays of light into a black box, and determine the location of atoms inside the box based on the entry and exit points of the ray.

Seriously addicting.

Here is a Wiki article about it and an online-playable Flash version.

2008-02-13

Sharing Menu Items between ToolStrips on a Windows Form

So, you may have found yourself building a nice user-friendly, somewhat complicated Windows Forms application, that had lots of drop-down menus and right click context menus, and what not.. You may have naively assumed that you could *share* your menu items, so that you have a consistent set of options, icons, and more importantly event handlers for a particular menu item or set of menu items.


Well, I did... I had a Tools drop-down menu with some basic functions that I wanted to also be accessible from a right-click content menu on a treeview. Redundant? Sure... Convenient? Definately.

There's a little annoying detail that says that a ToolStripMenuItem can't be "owned" by more than one ToolStrip. In other words, it can only be in one place at a time. So, when you do something like this:

toolsToolStripMenuItem.DropDownItems.Add(myMenuItem);
treeNodeContextToolStrip.Items.Add(myMenuItem);

The menu item in question suddenly disappears from the tools menu, and appears in the content menu... Hmm...

So, in order to share the menu item, I came up with this hackish solution....I handled the Opening event for the two menu strips and in each one, "took ownership" of the menuitem. So, through sleight-of-hand it seems to exist in one place at a time. We can only get away with this because, ToolStrips, being modal, only show one at a time.

So, here's a simple sample:

Hot-Swap Menu Item Sample

private void treeNodeContextMenuStrip_Opening(object sender, CancelEventArgs e)
{
treeNodeContextMenuStrip.Items.Insert(3, myToolStripMenuItem);
}

private void toolsToolStripMenuItem_DropDownOpening(object sender, EventArgs e)
{
this.toolsToolStripMenuItem.DropDownItems.Insert(5, this.myToolStripMenuItem);
}