Wednesday, July 29, 2009

Assembly Resolution with AppDomain.AssemblyResolve

Plugin frameworks often rely on runtime assembly resolution, and sometimes the assemblies to be resolved don't lie within the directory structure of the executable, nor are they located in the GAC. In these cases, you can use the AppDomain.AssemblyResolve event to resolve assemblies at runtime. The key with the AssemblyResolve method is that it must be put into place before attempting to instantiate any of the classes where an assembly needs to be loaded. Sample code exists on the MSDN Documentation for System.Assembly.AssemblyResolve.

Before implementing runtime assembly resolution, it's important to understand how methods are loaded at runtime.

When a method is first called, that method is JITted, or "Just-In-Time Compiled". Essentially, all the IL code for that method is loaded and then compiled into CPU instructions. Any types that are used within that method are also resolved when the method is JIT compiled, in order to ensure that the entire method is "safe" for execution. Essentially, what this means to Assembly Resolution using the AppDomain.AssemblyResolve event is that the attachment to the event handler cannot reside within the same method that calls the type that will have to be resolved using the event handler. The reason for this is simple... the event handler must be attached before the type is resolved. If the event handler is attached within the same method, then the JIT compiler will attempt to resolve the type before executing the first instruction in the method, including the instruction to attach the event handler to the AssemblyResolve event, causing the whole application to fail.

Thus, a good example of attaching an assembly at runtime would be the following. The Widget class is the class to load, and it does not reside within the same directory as the executable. Instead, it resides in a directory that is only identified with a relative path. This relative path can be searched at runtime to find the specific assembly that is needed to load.


using System;
using System.IO;
using System.Reflection;
using ClassLibrary1;

namespace ConsoleApplication6
{
internal class Program
{
private const string ResolutionPath = @"..\..\..\ClassLibrary1\bin\debug\";

private static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolve;
RunWidget();
}

private static void RunWidget()
{
var widget = new WidgetInOtherAssembly();
Console.WriteLine(widget);
}

private static Assembly CurrentDomainAssemblyResolve(object sender, ResolveEventArgs args)
{
Assembly[] currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();

for (int i = 0; i < currentAssemblies.Length; i++)
{
if (currentAssembliesi.FullName == args.Name)
{
return currentAssembliesi;
}
}

return FindAssembliesInDirectory(args.Name, ResolutionPath);
}

private static Assembly FindAssembliesInDirectory(string assemblyName, string directory)
{
foreach (string file in Directory.GetFiles(directory))
{
Assembly assm;

if (TryLoadAssemblyFromFile(file, assemblyName, out assm))
return assm;
}

return null;
}

private static bool TryLoadAssemblyFromFile(string file, string assemblyName, out Assembly assm)
{
try
{
// Convert the filename into an absolute file name for
// use with LoadFile.
file = new FileInfo(file).FullName;

if (AssemblyName.GetAssemblyName(file).FullName == assemblyName)
{
assm = Assembly.LoadFile(file);
return true;
}
}
catch
{
/* Do Nothing */
}
assm = null;
return false;
}
}
}


Changes To ResolveEventArgs in .NET 4.0

In .NET 4.0, the ResolveEventArgs class has a new property called "RequestingAssembly", which returns the name of the assembly requesting resolution. This has been done in order to enable the ability to execute specific code based on which assembly was requesting the resolution.

You can see more information on these changes here.

Tuesday, July 28, 2009

On Method Overloading Best Practices

C# allows method overloading. An overload is defined as a method that has the same name as another method, and differs only in the number and/or type of parameters it receives. (Technically, .NET allows creation of overloads based only on the return type but this construct is not allowed in C#).

The .NET Framework uses method overloading with several of it's methods. For example, the Activator.CreateInstance method has no fewer than 14 distinct overloads for it's usage. Each of these overloads provides the developer flexibility with how he/she chooses to call this method, and can greatly abstract out some of the more menial tasks that may have to be done by the developer, such as casting from one type to another before making a call, or having to research default parameters for a method in order to ensure the proper parameters are passed into the method.

When to Use Method Overloading

Method overloading should be used to allow your callers greater type and parameter options when calling a specific method. Good uses of method overloading are as follows:

Default values

When a method has parameters for which "acceptable defaults" can be defined, an overload of that method that omits the defaulted parameter can be created which would subsequently call the method that contains the parameter for which there is a default, passing in the default value. An example of this might be the Int32.TryParse method, which has two overloads:


bool TryParse(string s, out int result);
bool TryParse(string s, NumberStyles style, IFormatProvider provider, out int result);


Both of these methods ultimately route to the same method, namely the internal static method Number.TryParseInt32. The difference is that the first one passes NumberStyles.Integer and NumberFormatInfo.CurrentInfo as the default values for style and provider, whereas the second overload passes the user defined values. Therefore, the first one is provided for convenience for default values.

Type conversion convenience

If a piece of data can be represented using multiple types, then overloads can be created to abstract type conversions away from the client. An example might be a fictional "CreateInstance" method.


T CreateInstance();
object CreateInstance(Type type);
object CreateInstance(string typeName);


These three methods all accomplish the same purpose, namely, creating an instance from a representation of a type. Two of them are overloads. In the first method, the representation is passed via a generic type parameter, T. The next method passes the type via an instance of the Type class, and the third method (which is an overload of the second method) passes the type via it's full name. It's important to note that these three parameter types are intrinsically the same piece of data only represented in different ways. They all three represent a single type, which is then used to create an instance. If I wanted to then create an instance of a class based on another piece of data other than the type, I should not create an overload, but should create a new method with a new method name.

When Not to Use Overloading

There are a couple of instances where method overloads would not be appropriate.

To clean up Intellisense

Designing anything within an object model simply to cater to ease of use when it comes to a specific technology is almost always a mistake. Cleaning up Intellisense is not related to having a clear code base, and catering to Intellisense in this situation can muddle the true purpose of your methods.

To take advantage of the type differences of intrinsically different pieces of data

An example of this could be overloads like the following:


Product GetProduct(Guid id);
Product GetProduct(string name);


The first issue here would be that the name of the methods, GetProduct, does not accurately or completely describe the actions that the method would take in order to accomplish it's goal. In both situations, a Product instance is retrieved, but both situations accomplish this task based on intrinsically different data. An id is not a name, and cannot be converted to a name without having more information. Creating this kind of overload makes compiler resolutions dependent upon whatever type the data happens to be at the moment.

Also, potential inconsistencies in naming can occur in the situation where a third overload is to be added. Suppose there is a client reference number for each product, and a third overload needs to be created to manage fetching products by this number. Suppose, furthermore, that the client reference number is of the type string. The preferred overload would look like this:


Product GetProduct(string clientRef);


Unfortunately, since overloads are only distinguished based on the name and the incoming parameter types, the above method would cause a compiler error when combined with the previous two methods, leaving the developer with only two choices: rename all the methods for consistency, which would also force callers to change their code, or else use a different name for the new method, causing an inconsistency, and learn to deal with it. Of course, either option is no good.

In the end, the best option is to only use method overloading when the methods to be overloaded accomplish the exact same task given the same data where portions of the data either have acceptable default values, or else are already the same bit of data only represented in different formats.

Monday, July 27, 2009

Automatically Find the Namespace of a Class and Add a Using Statement in Visual Studio

This is an all-too-common question on the forums for me not to address it here. This post concerns a very basic C# concept.

Sometimes, when adding a code snippet, it's possible you might get an error that says something to the effect of "The name "xxx" does not exist in the current context".

At compile time, a namespace reference must be found in order to resolve classes that will be used within an application. Classes can be resolved to their namespaces either with the using directive, or by using the fully qualified class name when referring to a class.

Employing the using directive would look like this:


using System;
using System.Reflection;

namespace MyNamespace
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine(Assembly.GetExecutingAssembly().GetName().Version);
}
}
}


While employing a fully qualified namespace reference would look like this:


namespace MyNamespace
{
public class Program
{
public static void Main(string[] args)
{
System.Console.WriteLine(System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
}
}
}


Typically, the using directive is preferred, as it usually creates cleaner, and easier to read code, but sometimes the fully qualified class name makes more sense, especially when two or more namespaces contain different classes with the same name.

If both of these above constructs are missing from the code file, the error described above will be the result.

Most of the time, this situation can be done by following the following simple steps:

1. Place the text caret in the middle of the class name that is causing the error. Alternatively, you can simply double click on the error in the Errors window.

2. Press Shift+Alt+F10. If the class can be found, a set of options will appear to add the namespace reference to the class. The options that start with "using" will add the using directive to the top of the code file, while the options that simply have the namespace name will change the class reference in code to use the fully qualified class name.

Occasionally, however, it's necessary to add a reference to the assembly containing the class. You can do this by right clicking the "References" folder within the project, and selecting "Add Reference....". Select the applicable tab:

.NET - For .NET Assemblies located in the GAC.

COM - For older COM assemblies registered in the system.

Projects - For references to projects that reside within the same solution as the current project.

Browse - To browse the file system for a specific file.

Recent - For references that have recently been added.

Finally, select the assembly you need to reference, and click OK.

After a reference has been added, you can go back to step 1 above, and ensure the using directive or the fully qualified class name results in the proper resolution of the class.

Wednesday, July 15, 2009

IOException While Trying to Delete a Folder?!

Make sure you don't step on your own toes. The following code illustrates...


using System;
using System.IO;

internal class Program
{
private static void Main(string[] args)
{
string directory = @"C:\atemporaryfolder\";
Directory.CreateDirectory(directory);
Environment.CurrentDirectory = directory;
Directory.Delete(directory);
}
}


Who's using the folder? You are. Whenever Environment.CurrentDirectory is set in an application, it prevents anything else from deleting the folder. Note that the Environment.CurrentDirectory can also be set through a variety of other means, including the FolderBrowserDialog, OpenFileDialog and SaveFileDialog.

What's the solution? Simple. Set Environment.CurrentDirectory to something else. @"C:\" works just fine.

Tuesday, July 14, 2009

C# 3.0 Syntax of the Day

Can't believe I missed this one...


var items = new string[] { "David", "Jennifer" };


versus:


var items = new[] { "David", "Jennifer" };


Yep, that's right! "new[]" is okay. The compiler will infer the type of the array based on the type of the items within the squigglies.

Monday, July 13, 2009

Truncation and Rounding in Conversion

Just found this. Feels angry:


decimal d = 1.7M;
Console.WriteLine((long)d);
Console.WriteLine(Convert.ToInt64(d));


Yields:

1
2

Ouch.

Remember, kids, casting syntax truncates, while using the Convert class rounds.

Friday, July 10, 2009

A WebBrowser Implementation That Actually Raises Events!

A question that comes up rather frequently on the Forums is "Why doesn't the WebBrowser event ___________ ever get raised?" Typically, the blank is filled in with "MouseClick" and "KeyDown" and other input events. The reason for this is the fact that the WebBrowser control is an ActiveX-based control. Essentially, all of the events that would normally be raised by the control itself, are instead captured inside the document that resides within the control. In order to get the events to be raised at the control level, it's important to attach the event handlers to the document, and not the control. Unfortunately, the default implementation of WebBrowser doesn't expose the document's events at this level, so you're left with no way of tracking what's happening within the WebBrowser control, as far as input is concerned. Well, no more. Here's an implementation of WebBrowser, called ActiveWebBrowser, that can handle these events, and exposes them publicly at the WebBrowser level, without interfering with the existing events on the page.

This code will require you to reference the COM library called "Microsoft HTML Object Library" in your code, which should be available through the COM tab in the Add References dialog.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.Runtime.InteropServices.ComTypes;
using mshtml;


namespace WindowsFormsApplication1
{
public class MSHtmlEventArgs : EventArgs
{
public MSHtmlEventArgs(mshtml.IHTMLEventObj obj)
{
EventObj = obj;
}

public mshtml.IHTMLEventObj EventObj { get; private set; }
}


public class ActiveWebBrowser : WebBrowser
{
private IConnectionPoint icp;
private int cookie = -1;

public event EventHandler<MSHtmlEventArgs> DocumentActivate;
public event EventHandler<MSHtmlEventArgs> DocumentAfterUpdate;
public event EventHandler<MSHtmlEventArgs> DocumentBeforeActivate;
public event EventHandler<MSHtmlEventArgs> DocumentBeforeDeactivate;
public event EventHandler<MSHtmlEventArgs> DocumentBeforeEditFocus;
public event EventHandler<MSHtmlEventArgs> DocumentBeforeUpdate;
public event EventHandler<MSHtmlEventArgs> DocumentCellChange;
public event EventHandler<MSHtmlEventArgs> DocumentClick;
public event EventHandler<MSHtmlEventArgs> DocumentContextMenu;
public event EventHandler<MSHtmlEventArgs> DocumentControlSelect;
public event EventHandler<MSHtmlEventArgs> DocumentDataAvailable;
public event EventHandler<MSHtmlEventArgs> DocumentDataSetChanged;
public event EventHandler<MSHtmlEventArgs> DocumentDataSetComplete;
public event EventHandler<MSHtmlEventArgs> DocumentDoubleClick;
public event EventHandler<MSHtmlEventArgs> DocumentDeactivate;
public event EventHandler<MSHtmlEventArgs> DocumentDragStart;
public event EventHandler<MSHtmlEventArgs> DocumentErrorUpdate;
public event EventHandler<MSHtmlEventArgs> DocumentFocusIn;
public event EventHandler<MSHtmlEventArgs> DocumentFocusOut;
public event EventHandler<MSHtmlEventArgs> DocumentHelp;
public event EventHandler<MSHtmlEventArgs> DocumentKeyDown;
public event EventHandler<MSHtmlEventArgs> DocumentKeyPress;
public event EventHandler<MSHtmlEventArgs> DocumentKeyUp;
public event EventHandler<MSHtmlEventArgs> DocumentMouseDown;
public event EventHandler<MSHtmlEventArgs> DocumentMouseMove;
public event EventHandler<MSHtmlEventArgs> DocumentMouseUp;
public event EventHandler<MSHtmlEventArgs> DocumentMouseOut;
public event EventHandler<MSHtmlEventArgs> DocumentMouseOver;
public event EventHandler<MSHtmlEventArgs> DocumentMouseWheel;
public event EventHandler<MSHtmlEventArgs> DocumentPropertyChange;
public event EventHandler<MSHtmlEventArgs> DocumentReadyStateChange;
public event EventHandler<MSHtmlEventArgs> DocumentRowEnter;
public event EventHandler<MSHtmlEventArgs> DocumentRowExit;
public event EventHandler<MSHtmlEventArgs> DocumentRowsDelete;
public event EventHandler<MSHtmlEventArgs> DocumentRowsInserted;
public event EventHandler<MSHtmlEventArgs> DocumentSelectionChange;
public event EventHandler<MSHtmlEventArgs> DocumentSelectStart;
public event EventHandler<MSHtmlEventArgs> DocumentStop;

protected override void OnDocumentCompleted(WebBrowserDocumentCompletedEventArgs e)
{
base.OnDocumentCompleted(e);

IConnectionPointContainer icpc;
icpc = (IConnectionPointContainer)this.Document.DomDocument;
Guid guid = typeof(HTMLDocumentEvents2).GUID;
icpc.FindConnectionPoint(ref guid, out icp);
icp.Advise(new HandleWebBrowserDHTMLEvents(this), out cookie);
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
if (-1 != cookie) icp.Unadvise(cookie);
cookie = -1;
}
base.Dispose(disposing);
}

class HandleWebBrowserDHTMLEvents : mshtml.HTMLDocumentEvents2
{
private ActiveWebBrowser _webBrowser;

public HandleWebBrowserDHTMLEvents(ActiveWebBrowser webBrowser)
{
_webBrowser = webBrowser;
}

public void onactivate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentActivate != null)
_webBrowser.DocumentActivate(_webBrowser, new MSHtmlEventArgs(e));
}

public void onafterupdate(mshtml.IHTMLEventObj e) {
if (_webBrowser.DocumentAfterUpdate != null)
_webBrowser.DocumentAfterUpdate(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onbeforeactivate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentBeforeActivate != null)
_webBrowser.DocumentBeforeActivate(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public bool onbeforedeactivate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentBeforeDeactivate != null)
_webBrowser.DocumentBeforeDeactivate(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onbeforeeditfocus(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentBeforeEditFocus != null)
_webBrowser.DocumentBeforeEditFocus(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onbeforeupdate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentBeforeUpdate != null)
_webBrowser.DocumentBeforeUpdate(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void oncellchange(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentCellChange != null)
_webBrowser.DocumentCellChange(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onclick(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentClick != null)
_webBrowser.DocumentClick(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public bool oncontextmenu(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentContextMenu != null)
_webBrowser.DocumentContextMenu(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public bool oncontrolselect(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentControlSelect != null)
_webBrowser.DocumentControlSelect(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void ondataavailable(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDataAvailable != null)
_webBrowser.DocumentDataAvailable(_webBrowser, new MSHtmlEventArgs(e));
}

public void ondatasetchanged(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDataSetChanged != null)
_webBrowser.DocumentDataSetChanged(_webBrowser, new MSHtmlEventArgs(e));
}

public void ondatasetcomplete(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDataSetComplete != null)
_webBrowser.DocumentDataSetComplete(_webBrowser, new MSHtmlEventArgs(e));
}

public bool ondblclick(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDoubleClick != null)
_webBrowser.DocumentDoubleClick(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void ondeactivate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDeactivate != null)
_webBrowser.DocumentDeactivate(_webBrowser, new MSHtmlEventArgs(e));
}

public bool ondragstart(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentDragStart != null)
_webBrowser.DocumentDragStart(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public bool onerrorupdate(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentErrorUpdate != null)
_webBrowser.DocumentErrorUpdate(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onfocusin(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentFocusIn != null)
_webBrowser.DocumentFocusIn(_webBrowser, new MSHtmlEventArgs(e));
}

public void onfocusout(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentFocusOut != null)
_webBrowser.DocumentFocusOut(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onhelp(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentHelp != null)
_webBrowser.DocumentHelp(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onkeydown(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentKeyDown != null)
_webBrowser.DocumentKeyDown(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onkeypress(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentKeyPress != null)
_webBrowser.DocumentKeyPress(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onkeyup(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentKeyUp != null)
_webBrowser.DocumentKeyUp(_webBrowser, new MSHtmlEventArgs(e));
}

public void onmousedown(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseDown != null)
_webBrowser.DocumentMouseDown(_webBrowser, new MSHtmlEventArgs(e));
}

public void onmousemove(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseMove != null)
_webBrowser.DocumentMouseMove(_webBrowser, new MSHtmlEventArgs(e));
}

public void onmouseout(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseOut != null)
_webBrowser.DocumentMouseOut(_webBrowser, new MSHtmlEventArgs(e));
}

public void onmouseover(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseOver != null)
_webBrowser.DocumentMouseOver(_webBrowser, new MSHtmlEventArgs(e));
}

public void onmouseup(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseUp != null)
_webBrowser.DocumentMouseUp(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onmousewheel(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentMouseWheel != null)
_webBrowser.DocumentMouseWheel(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onpropertychange(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentPropertyChange != null)
_webBrowser.DocumentPropertyChange(_webBrowser, new MSHtmlEventArgs(e));
}

public void onreadystatechange(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentReadyStateChange != null)
_webBrowser.DocumentReadyStateChange(_webBrowser, new MSHtmlEventArgs(e));
}

public void onrowenter(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentRowEnter != null)
_webBrowser.DocumentRowEnter(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onrowexit(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentRowExit != null)
_webBrowser.DocumentRowExit(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public void onrowsdelete(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentRowsDelete != null)
_webBrowser.DocumentRowsDelete(_webBrowser, new MSHtmlEventArgs(e));
}

public void onrowsinserted(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentRowsInserted != null)
_webBrowser.DocumentRowsInserted(_webBrowser, new MSHtmlEventArgs(e));
}

public void onselectionchange(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentSelectionChange != null)
_webBrowser.DocumentSelectionChange(_webBrowser, new MSHtmlEventArgs(e));
}

public bool onselectstart(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentSelectStart != null)
_webBrowser.DocumentSelectStart(_webBrowser, new MSHtmlEventArgs(e));
return true;
}

public bool onstop(mshtml.IHTMLEventObj e)
{
if (_webBrowser.DocumentStop != null)
_webBrowser.DocumentStop(_webBrowser, new MSHtmlEventArgs(e));
return true;
}
}
}
}

Wednesday, July 1, 2009

I'm an MVP!

So, I received an email from Microsoft this morning. Apparently I've been awarded Microsoft's Most Valuable Professional (MVP) Award in Visual C#! Thanks Microsoft!

I plan to just "keep on keepin' on" and continue helping people on the MSDN Forums as best as I know how, and to continue providing the best content I know how to give here on my blog.