Regular Expressions in .NET are pretty easy to use (assuming you understand the Regex syntax which is beside the case) and certainly you can think of some useful extension methods for System.String that would allow you to quickly validate against a particular regular expression pattern. But regular expressions have another great feature that you maybe don’t use as much – that is the ability to capture subexpressions into groups so that you can pull out a piece of the match.

The way this is done using the Regex class is pretty straightforward.

// Find a word that starts with H and W
string input = "Hello World";
string pattern = @"(H\w+) (W\w+)";

Match m = Regex.Match(input, pattern);
if (m.Success) {
    string hWord = m.Groups[1].Value;
    string wWord = m.Groups[2].Value;
}

This is easy enough, but I am annnoyed by the code that accesses the groups. It seems like such a mundane detail to worry about Group and Match objects. What if the code could be simplified like the following:

string input = "Hello World";
string pattern = @"(H\w+) (W\w+)";

string hWord, wWord;
if (input.MatchInto(pattern, out hWord, out wWord)) ...

It may not look like much of a savings in terms of lines of code, but I find that it looks much cleaner and is a lot less explicit. The full code is below.

/// <summary>
/// Performs a regular expression match against the specified string, and places the captured groups into the output parameters.
/// </summary>
/// <param name="input">The input string to match against.</param>
/// <param name="pattern">The regular expression pattern which should contain grouping expressions.</param>
/// <param name="value1">Receives the value of the 1st capture group (Groups[1]) or null if no match was made.</param>
/// <param name="value2">Receives the value of the 2nd capture group (Groups[2]) or null if no match was made.</param>
/// <param name="value3">Receives the value of the 3rd capture group (Groups[3]) or null if no match was made.</param>
/// <param name="value4">Receives the value of the 4th capture group (Groups[4]) or null if no match was made.</param>
/// <param name="value5">Receives the value of the 5th capture group (Groups[5]) or null if no match was made.</param>
/// <returns>True if the pattern matched (not necessarily all groups) otherwise false.</returns>
public static bool MatchInto( this string input, string pattern, out string value1, out string value2, out string value3, out string value4, out string value5 )
{

    value1 = value2 = value3 = value4 = value5 = null;

    var match = Match( input, pattern );
    if ( match.Success ) {

        // Value1
        if ( match.Groups.Count > 1 && match.Groups[1].Success ) {
            value1 = match.Groups[1].Value;
        }   // if

        // Value2
        if ( match.Groups.Count > 2 && match.Groups[2].Success ) {
            value2 = match.Groups[2].Value;
        }   // if

        // Value3
        if ( match.Groups.Count > 3 && match.Groups[3].Success ) {
            value3 = match.Groups[3].Value;
        }   // if

        // Value4
        if ( match.Groups.Count > 4 && match.Groups[4].Success ) {
            value4 = match.Groups[4].Value;
        }   // if

        // Value5
        if ( match.Groups.Count > 5 && match.Groups[5].Success ) {
            value5 = match.Groups[5].Value;
        }   // if

        return true;

    }   // if

    return false;

}

/// <summary>
/// Performs a regular expression match against the specified string, and places the captured groups into the output parameters.
/// </summary>
/// <param name="input">The input string to match against.</param>
/// <param name="pattern">The regular expression pattern which should contain grouping expressions.</param>
/// <param name="value1">Receives the value of the 1st capture group (Groups[1]) or null if no match was made.</param>
/// <param name="value2">Receives the value of the 2nd capture group (Groups[2]) or null if no match was made.</param>
/// <param name="value3">Receives the value of the 3rd capture group (Groups[3]) or null if no match was made.</param>
/// <param name="value4">Receives the value of the 4th capture group (Groups[4]) or null if no match was made.</param>
/// <returns>True if the pattern matched (not necessarily all groups) otherwise false.</returns>
public static bool MatchInto( this string input, string pattern, out string value1, out string value2, out string value3, out string value4 )
{
    string value5;
    return MatchInto( input, pattern, out value1, out value2, out value3, out value4, out value5 );
}

/// <summary>
/// Performs a regular expression match against the specified string, and places the captured groups into the output parameters.
/// </summary>
/// <param name="input">The input string to match against.</param>
/// <param name="pattern">The regular expression pattern which should contain grouping expressions.</param>
/// <param name="value1">Receives the value of the 1st capture group (Groups[1]) or null if no match was made.</param>
/// <param name="value2">Receives the value of the 2nd capture group (Groups[2]) or null if no match was made.</param>
/// <param name="value3">Receives the value of the 3rd capture group (Groups[3]) or null if no match was made.</param>
/// <returns>True if the pattern matched (not necessarily all groups) otherwise false.</returns>
public static bool MatchInto( this string input, string pattern, out string value1, out string value2, out string value3 )
{
    string value4;
    string value5;
    return MatchInto( input, pattern, out value1, out value2, out value3, out value4, out value5 );
}

/// <summary>
/// Performs a regular expression match against the specified string, and places the captured groups into the output parameters.
/// </summary>
/// <param name="input">The input string to match against.</param>
/// <param name="pattern">The regular expression pattern which should contain grouping expressions.</param>
/// <param name="value1">Receives the value of the 1st capture group (Groups[1]) or null if no match was made.</param>
/// <param name="value2">Receives the value of the 2nd capture group (Groups[2]) or null if no match was made.</param>
/// <returns>True if the pattern matched (not necessarily all groups) otherwise false.</returns>
public static bool MatchInto( this string input, string pattern, out string value1, out string value2 )
{
    string value3;
    string value4;
    string value5;
    return MatchInto( input, pattern, out value1, out value2, out value3, out value4, out value5 );
}

/// <summary>
/// Performs a regular expression match against the specified string, and places the captured groups into the output parameters.
/// </summary>
/// <param name="input">The input string to match against.</param>
/// <param name="pattern">The regular expression pattern which should contain grouping expressions.</param>
/// <param name="value">Receives the value of the 1st capture group (Groups[1]) or null if no match was made.</param>
/// <returns>True if the pattern matched (not necessarily all groups) otherwise false.</returns>
public static bool MatchInto( this string input, string pattern, out string value )
{
    string value2;
    string value3;
    string value4;
    string value5;
    return MatchInto( input, pattern, out value, out value2, out value3, out value4, out value5 );
}

Here’s a helpful tip if you frequently find yourself wrestling with AssemblyInfo.cs (or AssemblyInfo.vb, etc.) when working with a solution with a large number of projects.

I find that most of the time, almost all the information except the AssemblyTitle, AssemblyDescription, and GUID are the same across all projects. Even the GUID you can ignore if you’re not worried about COM visibility.

Just add a SolutionInfo.cs file to the solution (not any project in particular) and put your common assembly details in there. See below for an example.

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[assembly: AssemblyCompany( "Einstein Technologies" )]
[assembly: AssemblyProduct( "My Product" )]
[assembly: AssemblyCopyright( "Copyright 2009 Einstein Technologies. All rights reserved." )]
[assembly: AssemblyTrademark( "" )]
[assembly: AssemblyCulture( "en-US" )]

[assembly: ComVisible( false )]

[assembly: AssemblyVersion( "1.0.*" )]

Next go to each project, right click –> add existing item. Browse to the SolutionInfo.cs file you created above and click the glyph next to the Add button and choose Add as Link.

Now you can reduce your AssemblyInfo.cs file in that project to the lines below and feel confident that everything else is consistent across the projects.

using System.Reflection;

[assembly: AssemblyTitle( "Plugin Framework" )]
[assembly: AssemblyDescription( "A class library that defines plugin interfaces and stuff." )]

Bonus tip: I tend to drag the linked SolutionInfo.cs file into the special “Properties” folder alongside AssemblyInfo.cs. But Visual Studio won’t let you add files directly to this folder via the UI.

I don’t know about you but I am sick and tired of the choice I have to make between pretty formatting of values and usability. This applies to pretty much all .NET technology and I assume most languages in general. What’s my problem? Well let’s say you list the size of all files by extension in a directory.

Dir . -Recurse | 
Group Extension | %{ 
    $_ | 
    Select Name,@{
        N='Size';
        E={($_.Group | Measure-Object Length -Sum).Sum }
    }
}

The nice thing is, you can pipe this out to Sort-Object to sort by length. The downside is, it looks like crap. It would be nice if it showed MB, KB, bytes, etc. But once you format as a string, you can no longer easily filter or sort the output.

So please, reap the benefits of my frustration and use the following C# class with Add-Type to cast your data size formatting problems into oblivion.

Add-Type -ReferencedAssemblies System.Xml -TypeDefinition @"
   ... the code that stupid Windows Live says is too long to post ...
"@

Dir . -Recurse | 
Group Extension | %{ 
    $_ | 
    Select Name,@{
        N='Size';
        E={[DataSize]($_.Group | Measure-Object Length -Sum).Sum }
    }
}

Source is below.

using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Xml.Serialization;

namespace Einstein
{

    /// <summary>
    /// Represents a file size or other data size in bytes, kilobytes, megabytes, etc.
    /// </summary>
    [Serializable]
    [ImmutableObject( true )]
    public struct DataSize : IFormattable, IComparable<DataSize>, IComparable, IConvertible, IEquatable<DataSize>, IXmlSerializable
    {

        #region Constants

        /// <summary>
        /// The size of a byte, in bytes. Always 1, provided for consistency only.
        /// </summary>
        public const long ByteSize = 1;

        /// <summary>
        /// The size of a kilobyte, in bytes. This structure defines a KB as 1,024 bytes.
        /// </summary>
        public const long KilobyteSize = 1024;

        /// <summary>
        /// The size of a megabyte, in bytes. This structure defines a MB as 1,024^2 or 1,048,576 bytes.
        /// </summary>
        public const long MegabyteSize = 1048576;

        /// <summary>
        /// The size of a gigabyte, in bytes. This structure defines a GB as 1,024^3 or 1,073,741,824 bytes.
        /// </summary>
        public const long GigabyteSize = 1073741824;

        /// <summary>
        /// The size of a terabyte, in bytes. This structure defines a TB as 1,024^4 or 1,099,511,627,776 bytes.
        /// </summary>
        public const long TerabyteSize = 1099511627776;

        /// <summary>
        /// The suffix appended to the end of a string represented as bytes.
        /// </summary>
        public const string ByteSuffix = "B";

        /// <summary>
        /// The suffix appended to the end of a string represented as kilobytes.
        /// </summary>
        public const string KilobyteSuffix = "KB";

        /// <summary>
        /// The suffix appended to the end of a string represented as megabytes.
        /// </summary>
        public const string MegabyteSuffix = "MB";

        /// <summary>
        /// The suffix appended to the end of a string represented as gigabytes.
        /// </summary>
        public const string GigabyteSuffix = "GB";

        /// <summary>
        /// The suffix appended to the end of a string represented as terabytes.
        /// </summary>
        public const string TerabyteSuffix = "TB";

        #endregion

        #region Fields

        /// <summary>
        /// Holds the value of the data size, in bytes.
        /// </summary>
        private long bytes;

        /// <summary>
        /// Regular expression used to pick apart the format string.
        /// </summary>
        private static readonly Regex formatRegex = new Regex( @"(?<unit>A|B|K|M|G|T)(?<precision>\d+)?(?<nosuffix>\*)?", RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.Singleline );

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new DataSize representing the specified number of bytes.
        /// </summary>
        public DataSize( long bytes )
        {
            this.bytes = bytes;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets the value in terabytes.
        /// </summary>
        public decimal TotalTerabytes
        {
            get
            {
                return bytes / (decimal)TerabyteSize;
            }
        }

        /// <summary>
        /// Gets the value in gigabytes.
        /// </summary>
        public decimal TotalGigabytes
        {
            get
            {
                return bytes / (decimal)GigabyteSize;
            }
        }

        /// <summary>
        /// Gets the value in megabytes.
        /// </summary>
        public decimal TotalMegabytes
        {
            get
            {
                return bytes / (decimal)MegabyteSize;
            }
        }

        /// <summary>
        /// Gets the value in kilobytes.
        /// </summary>
        public decimal TotalKilobytes
        {
            get
            {
                return bytes / (decimal)KilobyteSize;
            }
        }

        /// <summary>
        /// Gets the value in bytes.
        /// </summary>
        public decimal TotalBytes
        {
            get
            {
                return (decimal)bytes;
            }
        }

        /// <summary>
        /// Gets the value in bytes as a signed 64 bit integer.
        /// </summary>
        public long Bytes
        {
            get
            {
                return bytes;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Converts the String representation of a number to its DataSize equivalent. A return value indicates whether the conversion succeeded or failed. 
        /// </summary>
        /// <remarks>
        /// <para>
        /// The string can take on the format of:
        /// <list type="bullet">
        ///     <item>128 MB</item>
        ///     <item>40.00 KB</item>
        ///     <item>78 gigabytes</item>
        ///     <item>1 terabyte</item>
        /// </list>
        /// </para>
        /// <para>
        /// Lowercase representations will be accepted such as a b, kb, mb, etc but they will not be treated as "bits" they
        /// will be treated as "bytes". Their acceptance is simply to keep the function case insensitive.
        /// </para>
        /// </remarks>
        /// <param name="input">A String object containing a data size to convert.</param>
        /// <returns>A DataSize structure representing the specified string.</returns>
        public static DataSize Parse( string input )
        {

            if ( input == null )
                throw new ArgumentNullException( "input" );

            DataSize dataSize;
            if ( TryParse( input, out dataSize ) )
                return dataSize;
            else
                throw new FormatException( "Could not parse the specified string into a DataSize." );

        }

        /// <summary>
        /// Converts the String representation of a number to its DataSize equivalent. A return value indicates whether the conversion succeeded or failed.
        /// </summary>
        /// <param name="input">A String object containing a number to convert.</param>
        /// <param name="dataSize">When this method returns, contains DatSize equivalent to the numeric value or symbol contained
        /// in input, if the conversion succeeded, or zero if the conversion failed. The conversion fails if the input parameter is a null
        /// reference (Nothing in Visual Basic), is not a number in a valid format, or represents a number less than MinValue or greater than MaxValue. This
        /// parameter is passed uninitialized.</param>
        /// <returns>
        /// True if the parse succeeded, false otherwise.
        /// </returns>
        /// <remarks>
        ///     <para>
        /// The string can take on the format of:
        /// <list type="bullet">
        ///             <item>128 MB</item>
        ///             <item>40.00 KB</item>
        ///             <item>78 gigabytes</item>
        ///             <item>1 terabyte</item>
        ///         </list>
        ///     </para>
        ///     <para>
        /// Lowercase representations will be accepted such as a b, kb, mb, etc but they will not be treated as "bits" they
        /// will be treated as "bytes". Their acceptance is simply to keep the function case insensitive.
        /// </para>
        /// </remarks>
        public static bool TryParse( string input, out DataSize dataSize )
        {
            return TryParse( input, false, out dataSize );
        }

        /// <summary>
        /// Converts the String representation of a number to its DataSize equivalent. A return value indicates whether the conversion succeeded or failed.
        /// </summary>
        /// <param name="input">A String object containing a number to convert.</param>
        /// <param name="requireUnit">True if a data size unit is required for the parse to succeed, false otherwise.</param>
        /// <param name="dataSize">When this method returns, contains DatSize equivalent to the numeric value or symbol contained
        /// in input, if the conversion succeeded, or zero if the conversion failed. The conversion fails if the input parameter is a null
        /// reference (Nothing in Visual Basic), is not a number in a valid format, or represents a number less than MinValue or greater than MaxValue. This
        /// parameter is passed uninitialized.</param>
        /// <returns>
        /// True if the parse succeeded, false otherwise.
        /// </returns>
        /// <remarks>
        ///     <para>
        /// The string can take on the format of:
        /// <list type="bullet">
        ///             <item>128 MB</item>
        ///             <item>40.00 KB</item>
        ///             <item>78 gigabytes</item>
        ///             <item>1 terabyte</item>
        ///         </list>
        ///     </para>
        ///     <para>
        /// Lowercase representations will be accepted such as a b, kb, mb, etc but they will not be treated as "bits" they
        /// will be treated as "bytes". Their acceptance is simply to keep the function case insensitive.
        /// </para>
        /// </remarks>
        [SuppressMessage( "Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "The switch statement is throwing it off." )]
        public static bool TryParse( string input, bool requireUnit, out DataSize dataSize )
        {

            dataSize = default( DataSize );

            // Make sure we have a string
            if ( String.IsNullOrEmpty( input ) )
                return false;

            // Trim both ends of input
            input = input.Trim( );

            string numberPart = null;
            string unitPart = null;

            // Find the position of the space, if any
            int spacePos = input.IndexOf( ' ' );
            if ( spacePos > -1 ) {
                // A space exists in the string
                // First segment is the number such as 3.04
                // Second segment is assumed to be the unit such as KB
                numberPart = input.Substring( 0, spacePos ).Trim( );
                unitPart = input.Substring( spacePos + 1 ).Trim( );
            }   // if
            else {
                if ( requireUnit ) {
                    return false;
                }   // if
                else {
                    numberPart = input;
                    unitPart = "B";
                }   // else
            }   // else

            // Parse the number into a decimal. Allow separators and such
            // If the parse fails, an exception will be thrown which will
            // propagate out to the caller.
            decimal number = 0m;
            if ( Decimal.TryParse( numberPart, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign | NumberStyles.AllowThousands, null, out number ) ) {

                #region B.A.S.S. - Big Ass Switch Statement

                switch ( unitPart.ToUpperInvariant( ) ) {

                    case "B":
                    case "BYTE":
                    case "BYTES":
                    break;

                    case "K":
                    case "KB":
                    case "KBYTE":
                    case "KBYTES":
                    case "KILOBYTE":
                    case "KILOBYTES":
                    number *= KilobyteSize;
                    break;

                    case "M":
                    case "MB":
                    case "MEG":
                    case "MEGS":
                    case "MBYTE":
                    case "MBYTES":
                    case "MEGABYTE":
                    case "MEGABYTES":
                    number *= MegabyteSize;
                    break;

                    case "G":
                    case "GB":
                    case "GIG":
                    case "GIGS":
                    case "GBYTE":
                    case "GBYTES":
                    case "GIGABYTE":
                    case "GIGABYTES":
                    number *= GigabyteSize;
                    break;

                    case "T":
                    case "TB":
                    case "TBYTE":
                    case "TBYTES":
                    case "TERABYTE":
                    case "TERABYTES":
                    number *= TerabyteSize;
                    break;

                    default:
                    return false;

                }   // switch

                #endregion

                dataSize = new DataSize( (long)number );
                return true;

            }   // if

            return false;

        }

        /// <summary>
        /// Compares two DataSize structures and returns a value indicating whether one is less 
        /// than, equal to, or greater than the other.
        /// </summary>
        /// <param name="x">The first DataSize to compare.</param>
        /// <param name="y">The second DataSize to compare.</param>
        /// <returns>
        /// Less than zero, x is less than y. Zero, x equals y. Greater than zero, x is greater than y.
        /// </returns>
        [SuppressMessage( "Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "x" )]
        [SuppressMessage( "Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "y" )]
        public static int Compare( DataSize x, DataSize y )
        {
            return x.CompareTo( y );
        }

        /// <summary>
        /// Determines of two DataSize values are equal.
        /// </summary>
        /// <param name="x">The first DataSize to compare.</param>
        /// <param name="y">The second DataSize to compare.</param>
        /// <returns>True if the values are equal, false otherwise.</returns>
        [SuppressMessage( "Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "x" )]
        [SuppressMessage( "Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "y" )]
        public static bool Equals( DataSize x, DataSize y )
        {
            return x.Equals( y );
        }

        #endregion

        #region Object Overrides

        /// <summary>
        /// Indicates whether this instance and a specified object are equal.
        /// </summary>
        /// <param name="obj">Another object to compare to.</param>
        /// <returns>
        /// true if obj and this instance are the same type and represent the same value; otherwise, false.
        /// </returns>
        public override bool Equals( object obj )
        {

            if ( obj is DataSize )
                return Equals( (DataSize)obj );
            else
                return false;

        }

        /// <summary>
        /// Returns the hash code for this instance.
        /// </summary>
        /// <returns>
        /// A 32-bit signed integer that is the hash code for this instance.
        /// </returns>
        public override int GetHashCode( )
        {
            return bytes.GetHashCode( );
        }

        /// <summary>
        /// Returns a string representation of this DataSize using the default format.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.String"></see> representing this data size.
        /// </returns>
        public override string ToString( )
        {
            return ToString( "A", CultureInfo.CurrentCulture );
        }

        /// <summary>
        /// Returns a string representation of this DataSize using the default format.
        /// </summary>
        /// <param name="format">The format.</param>
        /// <returns>
        /// A <see cref="T:System.String"></see> representing this data size.
        /// </returns>
        /// <remarks>
        /// <para>
        /// The format specifier takes the form of: A99*
        /// </para>
        /// <para>
        /// Where A is any of the following characters:
        /// <list type="table">
        /// <listheader><term>Format Character</term></listheader>
        /// <item><term>A</term><description>Automatic. The unit of measurement will be the largest unit that is greater than or equal to 1.</description></item>
        /// <item><term>B</term><description>Bytes (B). No decimal digits will ever be displayed.</description></item>
        /// <item><term>K</term><description>Kilobytes (KB)</description></item>
        /// <item><term>M</term><description>Megabytes (MB)</description></item>
        /// <item><term>G</term><description>Gigabytes (GB)</description></item>
        /// <item><term>T</term><description>Terabytes (TB)</description></item>
        /// </list>
        /// </para>
        /// <para>
        /// The 99 represent a number from 0-99 that indicates the number of decimal places that will be
        /// included in the string. If bytes are specified as the unit of measurement, no decimal places will
        /// ever be used and this part of the format string will be ignored. If the precision is missing, up
        /// to two decimal places will be used.
        /// </para>
        /// <para>
        /// The asterisk (*) is an optional indicator that supresses the suffix. For example, if the value 39 KB
        /// is formatted as "K2*" then the output string would be "39.00" instead of "39.00 KB" because of the
        /// asterisk.
        /// </para>
        /// </remarks>
        public string ToString( string format )
        {
            return ToString( format, CultureInfo.CurrentCulture );
        }

        #endregion

        #region Operator Overloads

        /// <summary>
        /// Equality operator.
        /// </summary>
        /// <param name="left">The first DataSize to compare.</param>
        /// <param name="right">The second DataSize to compare.</param>
        /// <returns>True if <paramref name="left"/> and <paramref name="right"/> are equal, false otherwise.</returns>
        public static bool operator ==( DataSize left, DataSize right )
        {
            return left.bytes == right.bytes;
        }

        /// <summary>
        /// Inequality operator.
        /// </summary>
        /// <param name="left">The first DataSize to compare.</param>
        /// <param name="right">The second DataSize to compare.</param>
        /// <returns>False if <paramref name="left"/> and <paramref name="right"/> are equal, true otherwise.</returns>
        public static bool operator !=( DataSize left, DataSize right )
        {
            return left.bytes != right.bytes;
        }

        /// <summary>
        /// Greater than operator.
        /// </summary>
        /// <param name="left">The first DataSize to compare.</param>
        /// <param name="right">The second DataSize to compare.</param>
        /// <returns>True if <paramref name="left"/> is greater than <paramref name="right"/>, false otherwise.</returns>
        public static bool operator >( DataSize left, DataSize right )
        {
            return left.bytes > right.bytes;
        }

        /// <summary>
        /// Less than operator.
        /// </summary>
        /// <param name="left">The first DataSize to compare.</param>
        /// <param name="right">The second DataSize to compare.</param>
        /// <returns>True if <paramref name="left"/> is less than <paramref name="right"/>, false otherwise.</returns>
        public static bool operator <( DataSize left, DataSize right )
        {
            return left.bytes < right.bytes;
        }

        /// <summary>
        /// Greater than or equals operator.
        /// </summary>
        /// <param name="left">The first DataSize to compare.</param>
        /// <param name="right">The second DataSize to compare.</param>
        /// <returns>True if <paramref name="left"/> is greater than or equals <paramref name="right"/>, false otherwise.</returns>
        public static bool operator >=( DataSize left, DataSize right )
        {
            return left.bytes >= right.bytes;
        }

        /// <summary>
        /// Less than or equals operator.
        /// </summary>
        /// <param name="left">The first DataSize to compare.</param>
        /// <param name="right">The second DataSize to compare.</param>
        /// <returns>True if <paramref name="left"/> is less than or equals <paramref name="right"/>, false otherwise.</returns>
        public static bool operator <=( DataSize left, DataSize right )
        {
            return left.bytes <= right.bytes;
        }

        /// <summary>
        /// Unary plus operator.
        /// </summary>
        /// <param name="operand">The operand on which the operator operates.</param>
        /// <returns>The <paramref name="operand"/>.</returns>
        public static DataSize operator +( DataSize operand )
        {
            return operand;
        }

        /// <summary>
        /// Unary negation operator.
        /// </summary>
        /// <param name="operand">The operand on which the operator operates.</param>
        /// <returns>The <paramref name="operand"/>, negated.</returns>
        public static DataSize operator -( DataSize operand )
        {
            return new DataSize( -( operand.bytes ) );
        }

        /// <summary>
        /// Unary increment operator.
        /// </summary>
        /// <param name="operand">The operand on which the operator operates.</param>
        /// <returns>The <paramref name="operand"/> plus one.</returns>
        public static DataSize operator ++( DataSize operand )
        {
            return new DataSize( operand.bytes + 1 );
        }

        /// <summary>
        /// Unary decrement operator.
        /// </summary>
        /// <param name="operand">The operand on which the operator operates.</param>
        /// <returns>The <paramref name="operand"/>, minus one.</returns>
        public static DataSize operator --( DataSize operand )
        {
            return new DataSize( operand.bytes - 1 );
        }

        /// <summary>
        /// Addition operator.
        /// </summary>
        /// <param name="left">A DataSize to which <paramref name="right"/> will be added.</param>
        /// <param name="right">A second DataSize to add to <paramref name="left"/>.</param>
        /// <returns>A datasize that is the sum of <paramref name="left"/> and <paramref name="right"/>.</returns>
        public static DataSize operator +( DataSize left, DataSize right )
        {
            return new DataSize( left.bytes + right.bytes );
        }

        /// <summary>
        /// Subtraction operator.
        /// </summary>
        /// <param name="left">A DataSize to which <paramref name="right"/> will be subtracted.</param>
        /// <param name="right">A second DataSize to subtract from <paramref name="left"/>.</param>
        /// <returns>A datasize that is the difference of <paramref name="left"/> and <paramref name="right"/>.</returns>
        public static DataSize operator -( DataSize left, DataSize right )
        {
            return new DataSize( left.bytes - right.bytes );
        }

        /// <summary>
        /// Multiplication operator.
        /// </summary>
        /// <param name="left">A DataSize which will be multiplied by <paramref name="right"/>.</param>
        /// <param name="right">A DataSize by which <paramref name="left"/> will be multiplied.</param>
        /// <returns>A datasize that is the result of <paramref name="left"/> multiplied by <paramref name="right"/>.</returns>
        public static DataSize operator *( DataSize left, DataSize right )
        {
            return new DataSize( left.bytes * right.bytes );
        }

        /// <summary>
        /// Division operator.
        /// </summary>
        /// <param name="left">A DataSize which will be divided by <paramref name="right"/>.</param>
        /// <param name="right">A DataSize by which <paramref name="left"/> will be divided.</param>
        /// <returns>A datasize that is the result of <paramref name="left"/> divided by <paramref name="right"/>.</returns>
        public static DataSize operator /( DataSize left, DataSize right )
        {
            return new DataSize( left.bytes / right.bytes );
        }

        #endregion

        #region Conversion Operators

        /// <summary>
        /// Implicitly converts the specified <paramref name="operand"/> to <see cref="Int64"/>.
        /// </summary>
        /// <param name="operand">The DataSize to convert.</param>
        /// <returns>An <see cref="Int64"/> value that is the number of bytes represented by
        /// <paramref name="operand"/>.</returns>
        public static implicit operator long( DataSize operand )
        {
            return operand.bytes;
        }

        /// <summary>
        /// Implicitly converts the specified <paramref name="operand"/> to <see cref="Decimal"/>.
        /// </summary>
        /// <param name="operand">The DataSize to convert.</param>
        /// <returns>A <see cref="Decimal"/> value that is the number of bytes represented by
        /// <paramref name="operand"/>.</returns>
        public static implicit operator decimal( DataSize operand )
        {
            return operand.TotalBytes;
        }

        /// <summary>
        /// Explicitly converts the specified <paramref name="operand"/> to <see cref="DataSize"/>.
        /// </summary>
        /// <param name="operand">The <see cref="Int64"/> to convert.</param>
        /// <returns>A <see cref="DataSize"/> value that represents <paramref name="operand"/>
        /// number of bytes.</returns>
        [SuppressMessage( "Microsoft.Interoperability", "CA1406:AvoidInt64ArgumentsForVB6Clients", Justification = "VB 6 can't use overloaded operators." )]
        public static explicit operator DataSize( long operand )
        {
            return new DataSize( operand );
        }

        /// <summary>
        /// Explicitly converts the specified <paramref name="operand"/> to a string.
        /// </summary>
        /// <param name="operand">The <see cref="DataSize"/> to convert.</param>
        /// <returns>
        /// A string value that is the same as <see cref="M:DataSize.ToString"/>.
        /// </returns>
        public static explicit operator string( DataSize operand )
        {
            return operand.ToString( );
        }

        #endregion

        #region IFormattable Members

        /// <summary>
        /// Formats the value of the current instance using the specified format.
        /// </summary>
        /// <param name="format">The <see cref="T:System.String"></see> specifying the format to use.-or- null to use the default format defined for the type of the <see cref="T:System.IFormattable"></see> implementation.</param>
        /// <param name="formatProvider">The <see cref="T:System.IFormatProvider"></see> to use to format the value.-or- null to obtain the numeric format information from the current locale setting of the operating system.</param>
        /// <returns>
        /// A <see cref="T:System.String"></see> containing the value of the current instance in the specified format.
        /// </returns>
        public string ToString( string format, IFormatProvider formatProvider )
        {

            if ( String.IsNullOrEmpty( format ) )
                format = "A";

            Match formatMatch = formatRegex.Match( format );
            if ( formatMatch.Success ) {

                decimal value;
                int precision;
                string suffix;

                // Parse the precision specifier which determines how many decimal digits
                // will be displayed in the number portion of the return value.
                if ( formatMatch.Groups["precision"].Success ) {
                    // Try to parse the precision specifier and if we can't, default to zero
                    if ( Int32.TryParse( formatMatch.Groups["precision"].Value, out precision ) ) {
                        if ( precision > 99 || precision < 0 )
                            throw new FormatException( "Invalid format specifier." );
                    }   // if
                    else {
                        throw new FormatException( "Invalid format specifier." );
                    }   // else
                }   // if
                else {
                    precision = 2;
                }   // else

                switch ( formatMatch.Groups["unit"].Value.ToUpperInvariant( ) ) {

                    case "A":
                    // Automatic based on the size of the value
                    if ( Math.Truncate( Math.Abs( TotalTerabytes ) ) >= 1 )
                        goto case "T";
                    else if ( Math.Truncate( Math.Abs( TotalGigabytes ) ) >= 1 )
                        goto case "G";
                    else if ( Math.Truncate( Math.Abs( TotalMegabytes ) ) >= 1 )
                        goto case "M";
                    else if ( Math.Truncate( Math.Abs( TotalKilobytes ) ) >= 1 )
                        goto case "K";
                    else
                        goto case "B";

                    case "B":
                    value = TotalBytes;
                    suffix = ByteSuffix;
                    precision = 0;  // bytes cannot be fractional
                    break;

                    case "K":
                    value = TotalKilobytes;
                    suffix = KilobyteSuffix;
                    break;

                    case "M":
                    value = TotalMegabytes;
                    suffix = MegabyteSuffix;
                    break;

                    case "G":
                    value = TotalGigabytes;
                    suffix = GigabyteSuffix;
                    break;

                    case "T":
                    value = TotalTerabytes;
                    suffix = TerabyteSuffix;
                    break;

                    default:
                    throw new FormatException( "Invalid format specifier." );

                }   // switch

                if ( formatMatch.Groups["nosuffix"].Success ) {
                    // They want the value without the suffix
                    return String.Format( formatProvider, "{0:N" + precision + "}", value );
                }   // if
                else {
                    // They want the value with the sufffix
                    return String.Format( formatProvider, "{0:N" + precision + "} {1}", value, suffix );
                }   // else

            }   // if
            else {
                throw new FormatException( "Invalid format specifier." );
            }   // else

        }

        #endregion

        #region IComparable<DataSize> Members

        /// <summary>
        /// Compares the current object with another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// A 32-bit signed integer that indicates the relative order of the objects being compared. The return value has the following meanings: Value Meaning Less than zero This object is less than the other parameter.Zero This object is equal to other. Greater than zero This object is greater than other.
        /// </returns>
        public int CompareTo( DataSize other )
        {
            return bytes.CompareTo( other.bytes );
        }

        #endregion

        #region IComparable Members

        /// <summary>
        /// Compares the current instance with another object of the same type.
        /// </summary>
        /// <param name="obj">An object to compare with this instance.</param>
        /// <returns>
        /// A 32-bit signed integer that indicates the relative order of the objects being compared. The return value has these meanings: Value Meaning Less than zero This instance is less than obj. Zero This instance is equal to obj. Greater than zero This instance is greater than obj.
        /// </returns>
        /// <exception cref="T:System.ArgumentException">obj is not the same type as this instance. </exception>
        public int CompareTo( object obj )
        {

            if ( obj is DataSize )
                return CompareTo( (DataSize)obj );
            else
                throw new ArgumentException( "Compared value is not a DataSize.", "obj" );

        }

        #endregion

        #region IConvertible Members

        /// <summary>
        /// Returns the <see cref="T:System.TypeCode"></see> for this instance.
        /// </summary>
        /// <returns>
        /// The enumerated constant that is the <see cref="T:System.TypeCode"></see> of the class or value type that implements this interface.
        /// </returns>
        TypeCode IConvertible.GetTypeCode( )
        {
            return TypeCode.Int64;
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent Boolean value using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// A Boolean value equivalent to the value of this instance.
        /// </returns>
        bool IConvertible.ToBoolean( IFormatProvider provider )
        {
            return ( bytes != 0 );
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent 8-bit unsigned integer using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// An 8-bit unsigned integer equivalent to the value of this instance.
        /// </returns>
        byte IConvertible.ToByte( IFormatProvider provider )
        {
            return (byte)bytes;
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent Unicode character using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// A Unicode character equivalent to the value of this instance.
        /// </returns>
        char IConvertible.ToChar( IFormatProvider provider )
        {
            throw new InvalidCastException( "Cannot convert from Einstein.DataSize to System.Char." );
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent <see cref="T:System.DateTime"></see> using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// A <see cref="T:System.DateTime"></see> instance equivalent to the value of this instance.
        /// </returns>
        DateTime IConvertible.ToDateTime( IFormatProvider provider )
        {
            throw new InvalidCastException( "Cannot convert from Einstein.DataSize to System.DateTime." );
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent <see cref="T:System.Decimal"></see> number using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// A <see cref="T:System.Decimal"></see> number equivalent to the value of this instance.
        /// </returns>
        decimal IConvertible.ToDecimal( IFormatProvider provider )
        {
            return (decimal)bytes;
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent double-precision floating-point number using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// A double-precision floating-point number equivalent to the value of this instance.
        /// </returns>
        double IConvertible.ToDouble( IFormatProvider provider )
        {
            return (double)bytes;
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent 16-bit signed integer using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// An 16-bit signed integer equivalent to the value of this instance.
        /// </returns>
        short IConvertible.ToInt16( IFormatProvider provider )
        {
            return (short)bytes;
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent 32-bit signed integer using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// An 32-bit signed integer equivalent to the value of this instance.
        /// </returns>
        int IConvertible.ToInt32( IFormatProvider provider )
        {
            return (int)bytes;
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent 64-bit signed integer using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// An 64-bit signed integer equivalent to the value of this instance.
        /// </returns>
        long IConvertible.ToInt64( IFormatProvider provider )
        {
            return bytes;
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent 8-bit signed integer using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// An 8-bit signed integer equivalent to the value of this instance.
        /// </returns>
        sbyte IConvertible.ToSByte( IFormatProvider provider )
        {
            return (sbyte)bytes;
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent single-precision floating-point number using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// A single-precision floating-point number equivalent to the value of this instance.
        /// </returns>
        float IConvertible.ToSingle( IFormatProvider provider )
        {
            return (float)bytes;
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent <see cref="T:System.String"></see> using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// A <see cref="T:System.String"></see> instance equivalent to the value of this instance.
        /// </returns>
        string IConvertible.ToString( IFormatProvider provider )
        {
            return ToString( "A", provider );
        }

        /// <summary>
        /// Converts the value of this instance to an <see cref="T:System.Object"></see> of the specified <see cref="T:System.Type"></see> that has an equivalent value, using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="conversionType">The <see cref="T:System.Type"></see> to which the value of this instance is converted.</param>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// An <see cref="T:System.Object"></see> instance of type conversionType whose value is equivalent to the value of this instance.
        /// </returns>
        object IConvertible.ToType( Type conversionType, IFormatProvider provider )
        {

            IConvertible convertible = this;

            if ( conversionType == typeof( string ) )
                return convertible.ToString( provider );
            else if ( conversionType == typeof( DataSize ) )
                return this;
            else if ( conversionType == typeof( uint ) )
                return convertible.ToUInt32( provider );
            else if ( conversionType == typeof( int ) )
                return convertible.ToInt32( provider );
            else if ( conversionType == typeof( ulong ) )
                return convertible.ToUInt64( provider );
            else if ( conversionType == typeof( long ) )
                return convertible.ToInt64( provider );
            else if ( conversionType == typeof( float ) )
                return convertible.ToSingle( provider );
            else if ( conversionType == typeof( double ) )
                return convertible.ToDouble( provider );
            else if ( conversionType == typeof( decimal ) )
                return convertible.ToDecimal( provider );
            else if ( conversionType == typeof( byte ) )
                return convertible.ToByte( provider );
            else if ( conversionType == typeof( sbyte ) )
                return convertible.ToSByte( provider );
            else if ( conversionType == typeof( ushort ) )
                return convertible.ToUInt16( provider );
            else if ( conversionType == typeof( short ) )
                return convertible.ToInt16( provider );
            else if ( conversionType == typeof( bool ) )
                return convertible.ToBoolean( provider );
            else if ( conversionType == typeof( DateTime ) )
                return convertible.ToDateTime( provider );
            else if ( conversionType == typeof( char ) )
                return convertible.ToChar( provider );
            else
                throw new InvalidCastException( "Unable to convert DataSize to the requested type." );

        }

        /// <summary>
        /// Converts the value of this instance to an equivalent 16-bit unsigned integer using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// An 16-bit unsigned integer equivalent to the value of this instance.
        /// </returns>
        ushort IConvertible.ToUInt16( IFormatProvider provider )
        {
            return (ushort)bytes;
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent 32-bit unsigned integer using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// An 32-bit unsigned integer equivalent to the value of this instance.
        /// </returns>
        uint IConvertible.ToUInt32( IFormatProvider provider )
        {
            return (uint)bytes;
        }

        /// <summary>
        /// Converts the value of this instance to an equivalent 64-bit unsigned integer using the specified culture-specific formatting information.
        /// </summary>
        /// <param name="provider">An <see cref="T:System.IFormatProvider"></see> interface implementation that supplies culture-specific formatting information.</param>
        /// <returns>
        /// An 64-bit unsigned integer equivalent to the value of this instance.
        /// </returns>
        ulong IConvertible.ToUInt64( IFormatProvider provider )
        {
            return (ulong)bytes;
        }

        #endregion

        #region IEquatable<DataSize> Members

        /// <summary>
        /// Indicates whether the current object is equal to another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// true if the current object is equal to the other parameter; otherwise, false.
        /// </returns>
        public bool Equals( DataSize other )
        {
            return bytes == other.bytes;
        }

        #endregion

        #region IXmlSerializable Members

        /// <summary>
        /// This method is reserved and should not be used. When implementing the IXmlSerializable interface, you should return null (Nothing in Visual Basic) from this method, and instead, if specifying a custom schema is required, apply the <see cref="T:System.Xml.Serialization.XmlSchemaProviderAttribute"/> to the class.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Xml.Schema.XmlSchema"/> that describes the XML representation of the object that is produced by the <see cref="M:System.Xml.Serialization.IXmlSerializable.WriteXml(System.Xml.XmlWriter)"/> method and consumed by the <see cref="M:System.Xml.Serialization.IXmlSerializable.ReadXml(System.Xml.XmlReader)"/> method.
        /// </returns>
        System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema( )
        {
            return null;
        }

        /// <summary>
        /// Generates an object from its XML representation.
        /// </summary>
        /// <param name="reader">The <see cref="T:System.Xml.XmlReader"/> stream from which the object is deserialized.</param>
        void IXmlSerializable.ReadXml( System.Xml.XmlReader reader )
        {
            this.bytes = reader.ReadElementContentAsLong( );
        }

        void IXmlSerializable.WriteXml( System.Xml.XmlWriter writer )
        {
            writer.WriteValue( this.bytes );
        }

        #endregion

    }   // class

}   // namespace

I am really big on consistent user interfaces. I like it when things integrate well with a host and I like when the lines are blurred as to where the host ends and the application begins. That’s the approach I took with Tablet Enhancements for Outlook when I went with an Office look and feel for the TEO forms. In version 2.0 it was… eh. But in 3.0 I think I did a pretty good job of mimicking the Outlook forms. Of course, I still have to make it look and feel like Outlook 2007 now, but that’s another story.

So anyway, while working on a new product, I decided I was going to go for the Vista look-and-feel. Since the application’s main interface is supposed to function basically the same as the Control Panel, I used the Windows Vista Control Panel as my inspiration. But in doing so, I wound up with a few useful byproducts.

  • A custom renderer for ToolStrip that nearly perfectly reproduces the Windows Vista toolbar and menus.
  • A base form that lets Aero glass creep into the client area with Vista-looking back/forward buttons, breadcrumb box, and search box.
  • Another base form (subclass of the previous base form) that provides the Control Panel’s background bitmap and the enhanced status bar.

In upcoming posts, I will detail these three reusable components and even share the source code to my Vista-style ToolStrip renderer which can be added to your applications as easily as:

myToolStrip.Renderer = new VistaToolStripRenderer();

But for now, here’s a screen shot of what it all looks like (bugs in the glass part so that’s why there’s no text).

SNAG_MEDIA_20061018T134205

SNAG_MEDIA_20061018T135426

Sep 052006

Do you want Time Zone support in System.DateTime? No, you don’t. Trust me. Every now and then I come across a blog posting or article about how Microsoft is investigating ways to extend System.DateTime to meet customer demand for time zone support. Basically, customers are asking Microsoft to make System.DateTime be able to represent a given date/time and UTC offset. This is bad for a number of reasons, but the biggest reason is that time zones SUCK!

Maybe you remember my quest to eliminate all of the evil that time zones have inflicted on me by converting my life to UTC while the world around me continued ignorantly using Eastern Standard Time? Well that didn’t go over too well because interacting with other people was just too difficult. Fortunately, computers don’t have that problem. If you follow these simple rules, you can live a happy pain-free coexistence with DateTime.

  • Never, ever, ever store a DateTime in its local time. By storing all DateTimes in UTC, you guarantee that you can always accurately convert it to any time zone quickly and issues such as daylight savings crossovers and politics won’t cause issues. (Tip, use GETUTCDATE() in MSSQL and DateTime.UtcNow in .NET)
  • Never transfer DateTimes over the network in local time. Always use UTC because you don’t want to get into a situation where your application’s architecture won’t scale geographically because it expects the client and server to be in the same time zone.
  • Only use local times in the user interface when presenting a DateTime to a human or accepting a DateTime as input from a human. When you read the time, assume it is local time zone (unless your UI has a mechanism for specifying the time zone like Outlook finally does) and then promptly convert it to UTC using DateTime.ToUniversalTime() for storage or transport. When you’re displaying a DateTime from the database for a human to read in a report or window, use DateTime.ToLocalTime().
  • Note that I haven’t mentioned the DateTimeKind enumeration. I find this extension to DateTime to be completely unnecessary. If you followed the above rules, then there is no need to tell whether a date is local or utc. Also, DateTimeKind is a total hack and does not protect you from making multiple calls to ToLocalTime() or ToUniversalTime() which will keep offsetting the date. If you follow the rules above, there is no need to protect against this.

So does this mean I think .NET is completely sufficient when it comes to dates and times? Not by a long shot. But I don’t think the DateTime structure itself needs to be modified. In fact, I think it’s the TimeZone class that needs to be enhanced. Here’s what I’d like to see added to TimeZone.

  • static TimeZone[] TimeZone.GetAllTimeZones()
  • static TimeZone TimeZone.FromName(string name)
  • string TimeZone.Rfc822Name { get; }
  • string DateTime.ToRfc822String(TimeZone tz)
  • DateTime DateTime.FromRfc822String(string rfc822String)

Currently, only the local time zone is supported by the TimeZone class. I would like to see all the same time zones in the Windows registry (as seen in the date/time control panel applet) supported. Second in my list is a way to get a TimeZone from a name such as “Eastern Standard Time” or “EST”. This way you could parse out the time zone or UTC offset often found in internet email messages (see RFC 822). Third on my list is basically the reverse of #2, that is, the ability to get the standard name for the time zone. Fourth on the list is the ability to convert a DateTime to a localized time zone for an internet message. But if you follow my recommendations at the beginning of this post, you should be sending all your dates as UTC!! Finally, the ability for DateTime to parse a RFC 822 date header. The return value would always be in UTC, but unlike the current implementation, it wouldn’t ignore the offset. So if the date was: Thu, 31 Aug 2006 06:23:59 -0400, then it would be parsed as a UTC DateTime with the value 8/31/2006 10:23:59.

So anyway, what I’m basically saying is leave DateTime alone and fix TimeZone. :)

This was a real pain in the ass to figure out and searching around the internet, I couldn’t really find any straight answers. But I finally figured out how to use nested content with an ASP.NET User Control without resorting to a custom control. (Hey I’m a design-time guy.)

So let’s say you want to do this:

<uc1:Box runat="server" Title="This is cool!">
  <p>This would be child content of the box control.</p>
  <asp:Button runat="server" Text="This works too" />
</uc1:Box>

In Box.ascx, put this:

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="Box.ascx.cs" Inherits="Box" %>
<div id="BoxDiv" runat="server" class="box">
  <h3><asp:Literal id="TitleLiteral" runat="server"/></h3>
  <asp:PlaceHolder id="ChildPlaceHolder" runat="server" />
</div>

In Box.ascx.cs put this:

[ParseChildren( true, "ChildControls" )]
public partial class Box : System.Web.UI.UserControl
{

    public string Title
    {
        get
        {
            return TitleLiteral.Text;
        }
        set
        {
            TitleLiteral.Text = value;
        }
    }

    /// <summary>
    /// A collection of child controls nested in the user control's tags.
    /// </summary>
    public ControlCollection ChildControls
    {
        get
        {
            return ChildPlaceHolder.Controls;
        }
    }

}

Make your own Outlook! That’s what I did. Well not really. But I am working on a new project (not TEO, not even tablet for that matter) that requires extensive Outlook UI customization and creating user interface prototypes in Outlook was becoming a major drag. So much code just to create a button or menu. And when you need to start getting hierarchical, forget it.

So I took some time writing a dummy Outlook client. It doesn’t actually DO anything, but it looks almost identical to Outlook except for a few incorrect icons. But this one is much easier to customize in a WYSIWYG designer and allows me to turn out UI concepts much more quickly. I thought it was cool so I’d post a screen shot.

SNAG-20060423_005155

I’m not sure who reads my blog, but if anyone can give me the answer to this question I’d love to know. In .NET, how can I tell if my object is in COM heaven? Meaning it’s been separated from it’s RCW and can no longer be called. I know if you call Marshal.ReleaseComObject() it will return the reference count but what if I just want that count without decrementing it? I don’t like trapping InvalidComObjectException if I can avoid it.

Oh and I should mention that it doesn’t matter that I’m still holding a managed reference to it. The problem appears to be stemming from Windows Forms when it recursively disposes controls, it takes a different path for disposing ActiveX controls. So somewhere in there it’s doing a FinalReleaseComObject on the object even though I still have a reference to the RCW.

When you have complicated UI requirements, sometimes you might forget that all of your controls are initialized in your form’s constructor in Windows Forms. This can lead to dramatic performance problems, especially when COM interop is involved. Take for example the MapPoint integration in TEO 3.0.

In TEO 3.0, rather than just calling out to MapPoint and bringing back an image, there is a live MapPoint instance embedded in a tab that you can work with just as if you were using MapPoint. Drag/zoom/etc. However, MapPoint is an out-of-process COM server which means that when the control is created, MapPoint.exe is launched invisibly in the background to serve this functionality. While I’m proud of the plugin, it’s probably one of those things that you won’t use every time you open a contact window. Yet all that initialization is still done in the forms constructor, contributing several seconds to load times. Or at least it would have been.

If you have lots of complicated user interface components tucked away in a tab or invisible panel, consider using this control wrapper I made called DelayLoadedControl(Of T). To use it, you basically pass it a type of a control (must derive from System.Windows.Forms.Control) and add this in place of your control and none of the construction or initialization will be performed until the DelayLoadedControl becomes visible. What this means in TEO’s case is that MapPoint (and probably other tabs as well) won’t be invoked until that tab is selected, saving you valuable form launch time without complicating your design surface too much. Just stick your controls in a UserControl. The code for the control wrapper is below.

/// <summary>
/// A panel that allows for delay-loaded instantiation of controls by waiting for
/// the time that the panel is first shown.
/// </summary>
/// <remarks>
/// The type that is passed into this control instance must be a control with a 
/// default constructor. This constructor will be used to create the control when
/// it is added to the panel. This control is very useful for crowded UI's that
/// take up valuable seconds when a form is constructed, but might not be shown
/// right away such as in a tab page that might never be used. This allows the
/// time spent creating that control to be deferred until the user actually
/// requests the UI.
/// </remarks>
/// <typeparam name="T">Your control's type which must expose a default public
/// constructor.</typeparam>
public class DelayLoadedPanel<T> : Panel where T : Control, new( )
{

    /// <summary>
    /// Holds the control instance that is hosted inside this control 
    /// </summary>
    private T _innerControl;

    /// <summary>
    /// Event that is raised when the control is created on the fly inside the
    /// delay loaded panel.
    /// </summary>
    public event EventHandler<ControlCreatedEventArgs<T>> InnerControlCreated;

    /// <summary>
    /// Initializes a new instance of the <see cref="DelayLoadedPanel"/> class.
    /// </summary>
    public DelayLoadedPanel( )
    {
    }

    /// <summary>
    /// Releases the unmanaged resources used by the <see cref="T:Control"/> and
    /// its child controls and optionally releases the managed resources.
    /// </summary>
    /// <param name="disposing">true to release both managed and unmanaged
    /// resources; false to release only unmanaged resources.</param>
    protected override void Dispose( bool disposing )
    {
        if ( IsDisposed )
            return;
        if ( disposing )
        {
            if ( _innerControl != null )
            {
                _innerControl.Dispose( );
                _innerControl = null;
            }   // if
        }   // if
        // Calling base last. This is how I think it should be done anyway.
        base.Dispose( disposing );
    }

    /// <summary>
    /// Returns the control that is hosted inside this panel, or null if the
    /// control is not yet created.
    /// </summary>
    public T InnerControl
    {
        get
        {
            if ( IsDisposed )
                throw new ObjectDisposedException( );
            return _innerControl;
        }
    }

    /// <summary>
    /// Forces the creation of the inner control even though the panel may
    /// not have been displayed yet.
    /// </summary>
    public void ForceInnerControlCreation( )
    {
        if ( _innerControl == null )
        {
            _innerControl = new T( );
            _innerControl.Dock = DockStyle.Fill;
            Controls.Add( _innerControl );
            if ( InnerControlCreated != null )
            {
                InnerControlCreated( this, new ControlCreatedEventArgs<T>( _innerControl ) );
            }   // if
            _innerControl.Visible = true;
        }   // if
    }

    /// <summary>
    /// Raises the <see cref="E:Control.VisibleChanged"/> event.
    /// </summary>
    /// <remarks>
    /// Overridden to provide on-demand instantiation of the control type
    /// supplied in the 
    /// </remarks>
    /// <param name="e">An <see cref="T:EventArgs"></see> that contains the
    /// event data.</param>
    protected override void OnVisibleChanged( EventArgs e )
    {
        base.OnVisibleChanged( e );
        if ( Visible )
        {
            ForceInnerControlCreation( );
        }   // if
    }

}   // class

/// <summary>
/// Event arguments passed to the <see cref="E:DelayLoadedPanel.InnerControlCreated"/>
/// event of the <see cref="T:DelayLoadedPanel"/> class.
/// </summary>
public class ControlCreatedEventArgs<T> : EventArgs where T : Control, new( )
{

    /// <summary>
    /// The control instance that was created.
    /// </summary>
    private T _innerControl;

    /// <summary>
    /// Initializes a new instance of the <see cref="T:ControlCreatedEventArgs"/>
    /// class.
    /// </summary>
    /// <param name="innerControl">The inner control that was instantiated.</param>
    public ControlCreatedEventArgs( T innerControl )
    {
        if ( innerControl == null )
            throw new ArgumentNullException( "innerControl" );
        _innerControl = innerControl;
    }

    /// <summary>
    /// The control that was created inside the <see cref="T:DelayLoadedPanel"/>.
    /// </summary>
    public T InnerControl
    {
        get
        {
            return _innerControl;
        }
    }

}   // class

When you see how fast TEO 3 is (yes even in the first beta) you can thank the folks over at AutomatedQA. This profiling tool is very intuitive and is probably the best performance profiler I’ve ever seen. I took a few hours to tune up TEO 3.0 tonight and made about an 85% speed improvement opening new and existing items.

Sorry I haven’t posted much. Been busy with Christmas, TEO, and sick as a dog for the last week. I have a pretty big announcement to make (about my personal life) on Saturday so stay tuned.