The blog of dlaa.me

Posts tagged "Silverlight"

The proverbial "one line fix" [ComputeFileHashes works around a troublesome Silverlight-on-Mac issue]

When I achieved cross-platform parity by adding MD5 support to the Silverlight version of ComputeFileHashes, I thought I was done for a while. But then I got an email from a coworker reporting that the Silverlight version of ComputeFileHashes running on a Mac under Safari presented an "Add Files" dialog that did not actually let the user select any files. Ouch, that's no good...

I started investigating with a quick web search; the top hit for "OpenFileDialog Mac" showed that others had experienced similar problems and the Silverlight team confirmed a bug. So at least my application wasn't totally broken. :) I wanted to understand the scenario better, but I don't own a Mac (which is why this problem escaped my notice in the first place). Fortunately, I found one at work that I could borrow some cycles on and I wrote a simple test application to invoke the OpenFileDialog with a few different values for the Filter property. ComputeFileHashes was initially passing the value "All Files (*)|*" - effectively just "*" - which was intended to match all files. And, indeed, it does so in WPF and Silverlight/PC. However, on Silverlight/Mac that value seems to match no files. Someone suggested "*.*", but to me that matches all files with a '.' in their name and I didn't want to exclude files that don't happen to have an extension. So I tried "" instead, and that did exactly what I wanted on Silverlight/Mac and Silverlight/PC. I thought I'd found the solution - until I tried the new value on WPF and it caused an exception...

At this point I was tired of cross-platform trial-and-error, and I decided I was inviting trouble by passing any filter string at all! The default behavior of OpenFileDialog is to allow the selection of all files, so I wasn't really adding much value by passing a custom filter that did the same thing. Well, I was providing more explicit filter text in the drop-down of the dialog, but it wasn't worth the compatibility problems I was dealing with. So I removed the line of code that set the Filter property, recompiled, republished, and called it done. :)

The latest version of ComputeFileHashes is now 2009-01-30. I've updated all the binaries in order to avoid version number confusion, but the only real change here is the filter string and the improvement is only visible on Silverlight/Mac. (Note: I did not update the screenshots below, so the versions shown there are out of date.)

  • If you're using Silverlight to run ComputeFileHashes, you'll automatically get the new version next time you run ComputeFileHashes.
  • If you're using ClickOnce to run ComputeFileHashes, the application will automatically update itself after you run it a couple of times.
  • If you're using the WPF or command-line versions, you'll need to download the new binaries and update manually.

Please refer to the original release announcement for more information about supported platforms, source code, implementation, etc..

 

ClickOnce ComputeFileHashes

Click here or on the image below to run the Silverlight version of ComputeFileHashes in your browser.

Silverlight ComputeFileHashes

Click here or on the image below to download the command-line and WPF versions of ComputeFileHashes - along with the ClickOnce and Silverlight versions AND the complete source code for everything!

Command-line ComputeFileHashes

 

Seamless cross-platform support is a tricky matter that's usually best left to others who have the time and resources to do it right. I didn't realize I was introducing a platform dependency by specifying a filter string, but I was... and I got burned by it. That's why it's important to test an application on all the supported configurations: you never know what problem might show up where you least expect it! That said, I'm probably not going to run out and buy myself a Mac just because of this incident - so please accept my apologies in advance should I fall victim to a similar problem in the future. :)

Thank goodness for reference implementations [Low-overhead .NET MD5 implementation (source code and tests) works great on Silverlight!]

In yesterday's post announcing ComputeFileHashes's new support for MD5 on Silverlight, I promised to share some details about my experience getting an MD5 HashAlgorithm implementation for Silverlight. Recall that an MD5 class is available in the desktop .NET Framework, but is not part of Silverlight 2's subset of the .NET Framework. (Probably in order to save space by excluding one of the less-secure cryptographic hash functions - a completely sensible tradeoff.) Because I didn't want to write my own code for MD5 (it's a non-trivial algorithm), the challenge was to find something freely available that I could just drop in and take advantage of. So I was very interested when I found out about Reid Borsuk's managed implementation of an MD5 HashAlgorithm for Silverlight because it sounded perfect for my needs.

The first step of incorporating something like this is to check the license: this code is under Ms-PL, so there were no problems there. The next step is to skim the code and get a general feel for how it works - and it was while doing this that I realized I wouldn't be able to use this code as-is...

To understand why, a little background is required:

The HashAlgorithm abstract class requires that derived classes implement the following methods: Initialize, HashCore, and HashFinal. Initialize gets called once at the start of hashing, then HashCore is called many times (being passed a different block of data each time), and then HashFinal is called once at the end of hashing to finalize any computations and return the computed hash value. It's a straightforward model and is flexible enough to accommodate a wide variety of checksum algorithms. Other than maintaining a few bytes of internal state across calls, there's no need for the hash algorithm to allocate anything: the data flows in from the user, gets processed, and is immediately forgotten about.

Or at least that's how it's supposed to work...

What I found when I looked at the aforementioned MD5 implementation was that it would allocate an internal buffer during Initialize, repeatedly re-allocate that buffer and append new data to the end of it during every call to HashCore, and then process the entire buffer all at once in HashFinal. While this approach works fine for fairly small inputs, it was completely impractical for ComputeFileHashes which is expected to process multi-gigabyte files as a matter of course. All those reallocations and the large internal buffer would quickly exhaust the physical memory of virtually any system in use today on something like the Windows 7 Beta ISO images I've been using for my examples. (In fact, it's a bit more dire than it initially seems: this technique requires twice the memory of the original data: that last call to HashCore needs to copy the nearly-full-sized buffer into a new full-sized buffer.)

Okay, so if I couldn't use the code as-is; the next step is to see what it would take to modify it so that it would work for my scenario. Well, this implementation uses a small HashAlgorithm wrapper class around a core MD5 implementation class, I wondered if it would be a simple matter of changing the way the wrapper called into the core. But looking at things a bit more closely, it seemed the core was not structured for that - and separating things like I wanted might not be trivial.

It's decision time: Do I start changing the structure of this code to work the way I need it to, or do I investigate other options? I considered the implications of both approaches, but it was something a coworker said that convinced me to spend a bit of time looking elsewhere. He asked, "If you don't like a fundamental part of the implementation and feel the need to fix it, why do you think you won't be compelled to make changes to the rest of it as well?" That question expressed my concerns pretty well, so I decided to look into other options for a bit. After all, I could always come back to this if nothing panned out.

The obvious place to start was the MD5 specification: RFC1321, The MD5 Message-Digest Algorithm. The body of this document describes the algorithm in great detail and would be a great place to start writing my own implementation if I was willing to spend a considerable amount of time developing and testing. But the real gem is in the appendix: a reference implementation of MD5 written in C! Fortunately, C's not so different from C# - and I've ported things before - so I had a decent idea what to expect. And it sure is hard to beat the reference implementation from the point of view of obtaining an accurate, (typically) bug-free, chunk of code. There is an accompanying license, but it's open (this is a public specification, after all) and primarily requires that derivative works identify themselves as being "derived from the RSA Data Security, Inc. MD5 Message-Digest Algorithm." (like I just did). So things seemed promising!

I decided to spend a bus ride porting the reference implementation and see how far I got. As it happens (and I'm sure this is no accident), the reference implementation uses exactly the Initialize/HashCore/HashFinal pattern that HashAlgorithm expects. Consequently, each of my own HashAlgorithm wrapper methods simply makes a single call into the ported reference implementation - and all of a sudden concerns about memory exhaustion are a thing of the past! By the end of the bus ride, I had successfully ported the reference implementation to C# and had it passing the seven test cases that are part of the specification.

My mind was pretty much made up at this point: I'd use my port of the MD5 reference implementation for the Silverlight version of ComputeFileHashes. This was code from a reliable source, code I had become familiar with, and code that I'd feel comfortable debugging or tuning if necessary. I beefed up the test cases a bit by exercising all of them for all the possible chunk sizes, addressed a couple of code analysis warnings, and had something ready in a jiffy. I added the MD5Managed class to the Silverlight build of ComputeFileHashes and - yep - it just worked. :)

So here's a(nother) completely managed MD5 implementation that anyone is free to use (subject to the reference implementation's license). I haven't spent time optimizing it - but that was kind of the point (see the class comments below for more). I'd started out trying to avoid writing my own MD5 implementation and I only partly succeeded - but I'm glad with how this worked out and maybe some of you can benefit from what I've done. Even if all you do is run the Silverlight version of ComputeFileHashes from time to time, I feel like my relatively minimal investment was worthwhile! :)

 

[Click here to download a Visual Studio solution containing the source code for a Silverlight-ready managed implementation of MD5 along with the simple test cases discussed above.]

 

using System;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;

namespace Delay
{
    /// <summary>
    /// MD5Managed: A HashAlgorithm implementation that acts as a thin wrapper
    /// around a C# translation of the MD5 reference implementation. The C code
    /// has been translated as closely as possible so that most of the original
    /// structure remains and comparisons between the two are straightforward.
    /// </summary>
    /// <remarks>
    /// Derived from the RSA Data Security, Inc. MD5 Message-Digest Algorithm.
    /// 
    /// Specification:
    /// RFC1321 - The MD5 Message-Digest Algorithm
    /// http://www.faqs.org/rfcs/rfc1321.html
    /// 
    /// Original license:
    /// Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
    /// rights reserved.
    /// 
    /// License to copy and use this software is granted provided that it
    /// is identified as the "RSA Data Security, Inc. MD5 Message-Digest
    /// Algorithm" in all material mentioning or referencing this software
    /// or this function.
    /// 
    /// License is also granted to make and use derivative works provided
    /// that such works are identified as "derived from the RSA Data
    /// Security, Inc. MD5 Message-Digest Algorithm" in all material
    /// mentioning or referencing the derived work.
    /// 
    /// RSA Data Security, Inc. makes no representations concerning either
    /// the merchantability of this software or the suitability of this
    /// software for any particular purpose. It is provided "as is"
    /// without express or implied warranty of any kind.
    /// 
    /// These notices must be retained in any copies of any part of this
    /// documentation and/or software.
    /// </remarks>
    public class MD5Managed : HashAlgorithm
    {
        // Current context
        private readonly MD5_CTX _context = new MD5_CTX();
        // Last hash result
        private readonly byte[] _digest = new byte[16];
        // True if HashCore has been called
        private bool _hashCoreCalled;
        // True if HashFinal has been called
        private bool _hashFinalCalled;

        /// <summary>
        /// Initializes a new instance.
        /// </summary>
        public MD5Managed()
        {
            InitializeVariables();
        }

        /// <summary>
        /// Initializes internal state.
        /// </summary>
        public override void Initialize()
        {
            InitializeVariables();
        }

        /// <summary>
        /// Initializes variables.
        /// </summary>
        private void InitializeVariables()
        {
            MD5Init(_context);
            _hashCoreCalled = false;
            _hashFinalCalled = false;
        }

        /// <summary>
        /// Updates the hash code with the data provided.
        /// </summary>
        /// <param name="array">Data to hash.</param>
        /// <param name="ibStart">Start position.</param>
        /// <param name="cbSize">Number of bytes.</param>
        protected override void HashCore(byte[] array, int ibStart, int cbSize)
        {
            if (null == array)
            {
                throw new ArgumentNullException("array");
            }

            if (_hashFinalCalled)
            {
                throw new CryptographicException("Hash not valid for use in specified state.");
            }
            _hashCoreCalled = true;

            MD5Update(_context, array, (uint)ibStart, (uint)cbSize);
        }

        /// <summary>
        /// Finalizes the hash code and returns it.
        /// </summary>
        /// <returns></returns>
        protected override byte[] HashFinal()
        {
            _hashFinalCalled = true;
            MD5Final(_digest, _context);
            return Hash;
        }

        /// <summary>
        /// Returns the hash as an array of bytes.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Justification = "Matching .NET behavior by throwing here.")]
        [SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes", Justification = "Matching .NET behavior by throwing NullReferenceException.")]
        public override byte[] Hash
        {
            get
            {
                if (!_hashCoreCalled)
                {
                    throw new NullReferenceException();
                }
                if (!_hashFinalCalled)
                {
                    // Note: Not CryptographicUnexpectedOperationException because that can't be instantiated on Silverlight 4
                    throw new CryptographicException("Hash must be finalized before the hash value is retrieved.");
                }

                return _digest;
            }
        }

        // Return size of hash in bits.
        public override int HashSize
        {
            get
            {
                return _digest.Length * 8;
            }
        }

        ///////////////////////////////////////////////
        // MD5 reference implementation begins here. //
        ///////////////////////////////////////////////

        /* MD5 context. */
        private class MD5_CTX
        {
            public readonly uint[] state;   /* state (ABCD) */
            public readonly uint[] count;   /* number of bits, modulo 2^64 (lsb first) */
            public readonly byte[] buffer;  /* input buffer */

            public MD5_CTX()
            {
                state = new uint[4];
                count = new uint[2];
                buffer = new byte[64];
            }

            public void Clear()
            {
                Array.Clear(state, 0, state.Length);
                Array.Clear(count, 0, count.Length);
                Array.Clear(buffer, 0, buffer.Length);
            }
        }

        /* Constants for MD5Transform routine. */
        private const int S11 = 7;
        private const int S12 = 12;
        private const int S13 = 17;
        private const int S14 = 22;
        private const int S21 = 5;
        private const int S22 = 9;
        private const int S23 = 14;
        private const int S24 = 20;
        private const int S31 = 4;
        private const int S32 = 11;
        private const int S33 = 16;
        private const int S34 = 23;
        private const int S41 = 6;
        private const int S42 = 10;
        private const int S43 = 15;
        private const int S44 = 21;

        private static byte[] PADDING;

        [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "More compact this way")]
        static MD5Managed()
        {
            PADDING = new byte[64];
            PADDING[0] = 0x80;
        }

        /* F, G, H and I are basic MD5 functions. */
        private static uint F(uint x, uint y, uint z) { return (((x) & (y)) | ((~x) & (z))); }
        private static uint G(uint x, uint y, uint z) { return (((x) & (z)) | ((y) & (~z))); }
        private static uint H(uint x, uint y, uint z) { return ((x) ^ (y) ^ (z)); }
        private static uint I(uint x, uint y, uint z) { return ((y) ^ ((x) | (~z))); }

        /* ROTATE_LEFT rotates x left n bits. */
        private static uint ROTATE_LEFT(uint x, int n) { return (((x) << (n)) | ((x) >> (32 - (n)))); }

        /* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4.
           Rotation is separate from addition to prevent recomputation. */
        private static void FF(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
        {
            (a) += F((b), (c), (d)) + (x) + (uint)(ac);
            (a) = ROTATE_LEFT((a), (s));
            (a) += (b);
        }
        private static void GG(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
        {
            (a) += G((b), (c), (d)) + (x) + (uint)(ac);
            (a) = ROTATE_LEFT((a), (s));
            (a) += (b);
        }
        private static void HH(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
        {
            (a) += H((b), (c), (d)) + (x) + (uint)(ac);
            (a) = ROTATE_LEFT((a), (s));
            (a) += (b);
        }
        private static void II(ref uint a, uint b, uint c, uint d, uint x, int s, uint ac)
        {
            (a) += I((b), (c), (d)) + (x) + (uint)(ac);
            (a) = ROTATE_LEFT((a), (s));
            (a) += (b);
        }

        /* MD5 initialization. Begins an MD5 operation, writing a new context. */
        private static void MD5Init(MD5_CTX context)  /* context */
        {
            context.count[0] = context.count[1] = 0;

            /* Load magic initialization constants. */
            context.state[0] = 0x67452301;
            context.state[1] = 0xefcdab89;
            context.state[2] = 0x98badcfe;
            context.state[3] = 0x10325476;
        }

        /* MD5 block update operation. Continues an MD5 message-digest
           operation, processing another message block, and updating the
           context. */
        private static void MD5Update(MD5_CTX context,  /* context */
                                      byte[] input,     /* input block */
                                      uint inputIndex,  // Starting index for input block
                                      uint inputLen)    /* length of input block */
        {
            /* Compute number of bytes mod 64 */
            uint index = (uint)((context.count[0] >> 3) & 0x3F);

            /* Update number of bits */
            if ((context.count[0] += ((uint)inputLen << 3)) < ((uint)inputLen << 3))
            {
                context.count[1]++;
            }
            context.count[1] += ((uint)inputLen >> 29);

            uint partLen = 64 - index;

            /* Transform as many times as possible. */
            uint i = 0;
            if (inputLen >= partLen)
            {
                Buffer.BlockCopy(input, (int)inputIndex, context.buffer, (int)index, (int)partLen);
                MD5Transform(context.state, context.buffer, 0);

                for (i = partLen; i + 63 < inputLen; i += 64)
                {
                    MD5Transform(context.state, input, inputIndex + i);
                }

                index = 0;
            }

            /* Buffer remaining input */
            Buffer.BlockCopy(input, (int)(inputIndex + i), context.buffer, (int)index, (int)(inputLen - i));
        }

        /* MD5 finalization. Ends an MD5 message-digest operation, writing the
           the message digest and zeroizing the context. */
        private static void MD5Final(byte[] digest,    /* message digest */
                                     MD5_CTX context)  /* context */
        {
            byte[] bits = new byte[8];

            /* Save number of bits */
            Encode(bits, context.count, 8);

            /* Pad out to 56 mod 64. */
            uint index = (uint)((context.count[0] >> 3) & 0x3f);
            uint padLen = (index < 56) ? (56 - index) : (120 - index);
            MD5Update(context, PADDING, 0, padLen);

            /* Append length (before padding) */
            MD5Update(context, bits, 0, 8);

            /* Store state in digest */
            Encode(digest, context.state, 16);

            /* Zeroize sensitive information. */
            context.Clear();
        }

        /* MD5 basic transformation. Transforms state based on block. */
        private static void MD5Transform(uint[] state,
                                         byte[] block,
                                         uint blockIndex)
        {
            uint a = state[0], b = state[1], c = state[2], d = state[3];
            uint[] x = new uint[16];

            Decode(x, block, blockIndex, 64);

            /* Round 1 */
            FF(ref a, b, c, d, x[0],  S11, 0xd76aa478); /* 1 */
            FF(ref d, a, b, c, x[1],  S12, 0xe8c7b756); /* 2 */
            FF(ref c, d, a, b, x[2],  S13, 0x242070db); /* 3 */
            FF(ref b, c, d, a, x[3],  S14, 0xc1bdceee); /* 4 */
            FF(ref a, b, c, d, x[4],  S11, 0xf57c0faf); /* 5 */
            FF(ref d, a, b, c, x[5],  S12, 0x4787c62a); /* 6 */
            FF(ref c, d, a, b, x[6],  S13, 0xa8304613); /* 7 */
            FF(ref b, c, d, a, x[7],  S14, 0xfd469501); /* 8 */
            FF(ref a, b, c, d, x[8],  S11, 0x698098d8); /* 9 */
            FF(ref d, a, b, c, x[9],  S12, 0x8b44f7af); /* 10 */
            FF(ref c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */
            FF(ref b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */
            FF(ref a, b, c, d, x[12], S11, 0x6b901122); /* 13 */
            FF(ref d, a, b, c, x[13], S12, 0xfd987193); /* 14 */
            FF(ref c, d, a, b, x[14], S13, 0xa679438e); /* 15 */
            FF(ref b, c, d, a, x[15], S14, 0x49b40821); /* 16 */

            /* Round 2 */
            GG(ref a, b, c, d, x[1],  S21, 0xf61e2562); /* 17 */
            GG(ref d, a, b, c, x[6],  S22, 0xc040b340); /* 18 */
            GG(ref c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */
            GG(ref b, c, d, a, x[0],  S24, 0xe9b6c7aa); /* 20 */
            GG(ref a, b, c, d, x[5],  S21, 0xd62f105d); /* 21 */
            GG(ref d, a, b, c, x[10], S22, 0x02441453); /* 22 */
            GG(ref c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */
            GG(ref b, c, d, a, x[4],  S24, 0xe7d3fbc8); /* 24 */
            GG(ref a, b, c, d, x[9],  S21, 0x21e1cde6); /* 25 */
            GG(ref d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */
            GG(ref c, d, a, b, x[3],  S23, 0xf4d50d87); /* 27 */
            GG(ref b, c, d, a, x[8],  S24, 0x455a14ed); /* 28 */
            GG(ref a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */
            GG(ref d, a, b, c, x[2],  S22, 0xfcefa3f8); /* 30 */
            GG(ref c, d, a, b, x[7],  S23, 0x676f02d9); /* 31 */
            GG(ref b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */

            /* Round 3 */
            HH(ref a, b, c, d, x[5],  S31, 0xfffa3942); /* 33 */
            HH(ref d, a, b, c, x[8],  S32, 0x8771f681); /* 34 */
            HH(ref c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */
            HH(ref b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */
            HH(ref a, b, c, d, x[1],  S31, 0xa4beea44); /* 37 */
            HH(ref d, a, b, c, x[4],  S32, 0x4bdecfa9); /* 38 */
            HH(ref c, d, a, b, x[7],  S33, 0xf6bb4b60); /* 39 */
            HH(ref b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */
            HH(ref a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */
            HH(ref d, a, b, c, x[0],  S32, 0xeaa127fa); /* 42 */
            HH(ref c, d, a, b, x[3],  S33, 0xd4ef3085); /* 43 */
            HH(ref b, c, d, a, x[6],  S34, 0x04881d05); /* 44 */
            HH(ref a, b, c, d, x[9],  S31, 0xd9d4d039); /* 45 */
            HH(ref d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */
            HH(ref c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */
            HH(ref b, c, d, a, x[2],  S34, 0xc4ac5665); /* 48 */

            /* Round 4 */
            II(ref a, b, c, d, x[0],  S41, 0xf4292244); /* 49 */
            II(ref d, a, b, c, x[7],  S42, 0x432aff97); /* 50 */
            II(ref c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */
            II(ref b, c, d, a, x[5],  S44, 0xfc93a039); /* 52 */
            II(ref a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */
            II(ref d, a, b, c, x[3],  S42, 0x8f0ccc92); /* 54 */
            II(ref c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */
            II(ref b, c, d, a, x[1],  S44, 0x85845dd1); /* 56 */
            II(ref a, b, c, d, x[8],  S41, 0x6fa87e4f); /* 57 */
            II(ref d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */
            II(ref c, d, a, b, x[6],  S43, 0xa3014314); /* 59 */
            II(ref b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */
            II(ref a, b, c, d, x[4],  S41, 0xf7537e82); /* 61 */
            II(ref d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */
            II(ref c, d, a, b, x[2],  S43, 0x2ad7d2bb); /* 63 */
            II(ref b, c, d, a, x[9],  S44, 0xeb86d391); /* 64 */

            state[0] += a;
            state[1] += b;
            state[2] += c;
            state[3] += d;

            /* Zeroize sensitive information. */
            Array.Clear(x, 0, x.Length);
        }

        /* Encodes input (UINT4) into output (unsigned char). Assumes len is
           a multiple of 4. */
        private static void Encode(byte[] output,
                                   uint[] input,
                                   uint len)
        {
            for (uint i = 0, j = 0; j < len; i++, j += 4)
            {
                output[j] = (byte)(input[i] & 0xff);
                output[j + 1] = (byte)((input[i] >> 8) & 0xff);
                output[j + 2] = (byte)((input[i] >> 16) & 0xff);
                output[j + 3] = (byte)((input[i] >> 24) & 0xff);
            }
        }

        /* Decodes input (unsigned char) into output (UINT4). Assumes len is
           a multiple of 4. */
        private static void Decode(uint[] output,
                                   byte[] input,
                                   uint inputIndex,
                                   uint len)
        {
            for (uint i = 0, j = 0; j < len; i++, j += 4)
            {
                output[i] = ((uint)input[inputIndex + j]) |
                    (((uint)input[inputIndex + j + 1]) << 8) |
                    (((uint)input[inputIndex + j + 2]) << 16) |
                    (((uint)input[inputIndex + j + 3]) << 24);
            }
        }
    }
}

 

Updated 2009-02-16: Call MD5Init from the constructor for consistency with the Framework's HashAlgorithm classes where a call to Initialize is not necessary for a newly constructed instance.

Updated 2010-12-06: Added missing inputIndex offset to MD5Update method.

Cross-platform feature parity: achieved [Silverlight version of ComputeFileHashes now includes MD5!]

I was very happy with last week's release of ComputeFileHashes supporting the command-line, WPF, Silverlight, *and* ClickOnce. Only one thing bothered me: the Silverlight version didn't do MD5 due to the lack of support for that type of checksum by Silverlight 2. Recall that I'd fairly happily implemented my own CRC-32 class because none of the platforms supported it. [Also, it was relatively simple and had a good reference implementation. :) ] But because MD5 is a more complex algorithm and was only missing on Silverlight, I was reluctant to do the same thing for MD5...

What I really wanted was a freely available, Silverlight compatible HashAlgorithm-based MD5 implementation that I could trivially drop into my code and use on Silverlight. So I was excited when kind reader (and teammate!) Jeff Wilcox left a comment pointing to something that sounded perfect for my needs. I told Jeff I'd add MD5 for Silverlight and mentally breathed a sigh of relief that all four of ComputeFileHashes's supported platforms would provide the same set of checksums.

As it turns out, after a bit of research I decided not to use that MD5 implementation. (I'll explain why in my next post.) However, now that I'd fully bought in to the idea of MD5 on Silverlight, I was reluctant to let it go... So I spent some time working on an alternate solution and developed something I'm quite happy with. So I'm able to release an update to ComputeFileHashes that offers MD5 support on Silverlight!

The latest version of ComputeFileHashes is now 2009-01-26. I've updated all the binaries in order to avoid version number confusion - but the only real change here is the addition of MD5 for Silverlight. (FYI, I only updated the screenshot of the Silverlight version below.)

  • If you're using Silverlight to run ComputeFileHashes, you'll automatically get the new version next time you run ComputeFileHashes.
  • If you're using ClickOnce to run ComputeFileHashes, the application will automatically update itself after you run it a couple of times.
  • If you're using the WPF or command-line versions, you'll need to download the new binaries and update manually.

Please refer to the original release announcement for more information about supported platforms, source code, implementation, etc..

 

ClickOnce ComputeFileHashes

Click here or on the image below to run the Silverlight version of ComputeFileHashes in your browser.

Silverlight ComputeFileHashes

Click here or on the image below to download the command-line and WPF versions of ComputeFileHashes - along with the ClickOnce and Silverlight versions AND the complete source code for everything!

Command-line ComputeFileHashes

 

I've said that "ComputeFileHashes is a simple tool intended to make verifying checksums easy for anyone.". And in some ways, I think the Silverlight version is the easiest option of all because there's no need to install it on your machine and it runs everywhere Silverlight 2 does (PC, Mac, (Linux soon!), Internet Explorer, Firefox, Safari, ...). So I'm really glad to add MD5 support to ComputeFileHashes for Silverlight - I hope you enjoy the new functionality!

Gratuitous platform support [ComputeFileHashes works on the command-line, on WPF, on Silverlight, and via ClickOnce!]

Last week, I released the ComputeFileHashes tool for calculating file checksums. (To read more about what checksums are and why they're useful, please refer to that post.) ComputeFileHashes is a fairly simple .NET command-line application for calculating the MD5, SHA-1, and CRC-32 hashes of one or more files. It takes advantage of the multi-processing capabilities of today's hardware to complete that task quickly - roughly on par with native-code implementations. ComputeFileHashes works quite well and I happily used it to verify the recently released Windows 7 Beta ISO images I'd downloaded.

Because not everybody is a fan of command-line tools, I thought it would be nice to use WPF to create a more user-friendly version of ComputeFileHashes. Once I'd done that, I knew it would be a trivial matter to publish the WPF version via ClickOnce to enable an absurdly easy install scenario. From there, porting to Silverlight would be straightforward and would offer an install-free, completely web-based solution with cross-platform (ex: PC/Mac), cross-browser (ex: IE/Firefox/Safari) appeal. What's more, because all of these platforms are built on .NET, so I expected to be able to take significant advantage of code sharing!

 

ClickOnce ComputeFileHashes

Click here or on the image below to run the Silverlight version of ComputeFileHashes in your browser.

Silverlight ComputeFileHashes

Click here or on the image below to download the command-line and WPF versions of ComputeFileHashes - along with the ClickOnce and Silverlight versions AND the complete source code for everything!

Command-line ComputeFileHashes

 

Implementation notes:

  • The command-line version of ComputeFileHashes is a standard .NET 2.0 application and should work pretty much everywhere. The Silverlight version requires Silverlight 2 which is tiny and can be completely installed in less than a minute start-to-finish. The WPF/ClickOnce versions are a little more advanced and require .NET 3.5 SP1 (conveniently pre-installed on all Windows 7 machines!). If you don't already have .NET 3.5 SP1 (and you may not because Windows Update still doesn't seem to offer it), you can get the .NET 3.5 SP1 installer from here. Unfortunately, the only indication of .NET 3.5 SP1 not being installed seems to be an application crash immediately after starting the stand-alone WPF version. :( Fortunately, the ClickOnce version knows about the .NET 3.5 SP1 prerequisite and should offer to install it automatically if it's not already present.
  • None of the computation or file processing is performed on the main user interface thread under WPF or Silverlight, so ComputeFileHashes remains responsive even when working on a large file. Additional files can be queued for processing or the application/browser can be closed without the user having to wait.
  • As I hoped, I was able to achieve a very high degree of code sharing. By refactoring the original ComputeFileHashes code slightly, I pulled the core implementation out into a common class/file that everything shares. Then I put nearly all of the user interface functionality into another class/file that the WPF and Silverlight implementations both share. The XAML for the WPF and Silverlight versions is separate, but very similar. (There are enough slight differences between the two versions that I deliberately did not attempt to share the same XAML file.)
  • The source code structure looks like this:
    ComputeFileHashesCore.cs Core implementation of the file hashing code shared by all implementations. Makes use of multiple threads to perform hash calculations in parallel.
    ComputeFileHashesUI.cs User interface code shared by the WPF and Silverlight implementations. Makes use of a worker thread to push all computation off of the user interface thread and keep the application responsive. Defers to ComputeFileHashesCore for hashing functionality.
    CRC32.cs
    WaitingRoom.cs
    Custom CRC-32 HashAlgorithm implementation and synchronization object shared by all implementations.
    HashFileInfo.cs
    BlockingQueue.cs
    Data object for tracking state and custom Queue subclass that are shared by the WPF and Silverlight implementations.
    ComputeFileHashesCL\
       ComputeFileHashesCL.cs
    Command-line interface for handling arguments and displaying progress. Defers to ComputeFileHashesCore for hashing functionality.
    ComputeFileHashesWPF\
       Window1.xaml
       Window1.xaml.cs
    WPF definition of the application window. Defers to ComputeFileHashesUI for nearly all functionality.
    ComputeFileHashesSL\
       Page.xaml
       Page.xaml.cs
    Silverlight definition of the application window. Defers to ComputeFileHashesUI for nearly all functionality.
  • A look at the screenshots above reveals a few differences between the WPF and Silverlight implementations:
    • The DataGrids look different. On WPF, ComputeFileHashes makes use of the WPFToolkit's DataGrid; on Silverlight it uses the DataGrid in the SDK. The two are very similar to use from a developer perspective, but they draw themselves differently and have some slightly user-level functionality changes due to platform differences. This was actually my first experience with either DataGrid and I was happy to find that they both worked the same - and pretty much the way I expected them to!
    • The Silverlight version does not calculate the MD5 hash. This is because Silverlight's .NET doesn't implement the MD5 HashAlgorithm subclass while the desktop's .NET does. MD5 is not trivial to write, so I wasn't too interested in developing my own implementation like I did for CRC-32 (which isn't supported on any .NET platform).
    • The Silverlight version includes a "Details" column that's not present on the WPF version. On WPF, it's trivial to create a ToolTip for a DataGrid column, but on Silverlight the ToolTipService class must be used and my attempts to set its attached property with a Style were met with ... resistance. So if an exception is thrown when processing a file, the WPF version will show the exception text in a ToolTip while the Silverlight version shows it in the Details column.
    • The Silverlight version does not support drag-and-drop from the Windows Explorer. Running within a browser imposes certain limitations on Silverlight; an inability to integrate quite as richly with the operating system is one of them.
    • The hyperlinks aren't quite the same. Silverlight ships with HyperlinkButton which is exactly the right control for the job here. WPF doesn't have that control, so the similar Hyperlink control is made to behave as desired with a small bit of code.

 

In the original release announcement, I wrote that "ComputeFileHashes is a simple tool intended to make verifying checksums easy for anyone.". Well, that's still the case - and adding support for WPF, ClickOnce, and Silverlight should make it even easier for everyone to use. Just decide what kind of user interface you prefer, and start using that version of ComputeFileHashes for all your checksumming needs! :)

Yummier pies! [A technique for more flexible gradient styling of Silverlight Toolkit pie charts]

One of the goals of Charting for the Silverlight Toolkit is to enable rich, flexible styling by designers. (Background and overviews for Charting are available here and here.) And there are already some great resources for chart design: Designer’s Guide to Styling Silverlight Toolkit Charting Controls, Styling the Charts in the Silverlight Toolkit.

However, pie charts differ from the other common chart types in some significant ways and that makes the task of styling them a bit challenging. In particular, because styling happens at the DataPoint level (in the case of PieSeries, that's PieDataPoint: the visual representation of one of the slices of the pie), it initially seemed difficult to create a unified style for an entire pie because the sizes and positions of the individual slices can vary so dramatically for different data...

Working on something unrelated one day, I came across the GradientBrush.MappingMode property and realized the BrushMappingMode.Absolute enumeration was ideal for the pie styling problem. I created a simple demonstration and shared it with some folks on the Silverlight Toolkit team. That demo got passed around a bit and eventually made its way to Pete Brown; my sample shows up in his post about pie styling as what he calls the "Rainbow Brite" example.

Using Absolute mode is obviously a big win, but there's still a significant limitation: the coordinates it uses are expressed in pixels and therefore are closely tied to the size of the pie. What looks good for a pie at one size looks silly for that same pie when it is made a little bigger or smaller. I called this limitation out with my initial demo and Pete mentions it in his post as well...

Knowing that Absolute mode works well to enable cohesive styling for fixed-size pies (just look at Pete's great second example!), it seemed to me that the sizing limitation could be overcome with some fairly simple code. So when I got a chance, I wrote that code and put together the following proof-of-concept demonstration using the existing examples from Pete's post (with only a few trivial tweaks to the XAML to adjust gradient offsets). What's great is that now both pies can both be resized dynamically and the styling looks good at any size!

PieDataPointMappingModeUpdater sample application

[The complete sample code/project is available in PieDataPointMappingModeUpdater.zip as an attachment to this post.]

 

The principle here is simple: just update the relevant pixel coordinates of each gradient based on the current size of the PieSeries. What's more, a wrapper function abstracts out most of the complexity, so all that's necessary for the user is to pass in a simple helper method that's tailored to whatever specific pie design has been used. The helper is given the rectangular bounds of the pie and performs whatever gradient adjustments are necessary. It's that easy!

Here's what it looks like in code; the following is from the sample application's Page.xaml.cs:

/// <summary>
/// Initializes an instance of the Page class.
/// </summary>
public Page()
{
    InitializeComponent();

    // Hook up to Example1's PieSeries
    var pieSeries1 = Example1.Series[0] as PieSeries;
    PieDataPointMappingModeUpdater.UpdatePieSeries(pieSeries1, PieSeries1Updater, true);

    // Hook up to Example2's PieSeries
    var pieSeries2 = Example2.Series[0] as PieSeries;
    PieDataPointMappingModeUpdater.UpdatePieSeries(pieSeries2, PieSeries2Updater, true);
}

/// <summary>
/// Updates the gradients for Example1's PieSeries.
/// </summary>
private void PieSeries1Updater(PieDataPoint pieDataPoint, Rect pieBounds)
{
    var brush = pieDataPoint.Background as LinearGradientBrush;
    if (null != brush)
    {
        brush.StartPoint = new Point(pieBounds.Left, pieBounds.Top);
        brush.EndPoint = new Point(pieBounds.Right, pieBounds.Bottom);
    }
}

/// <summary>
/// Updates the gradients for Example2's PieSeries.
/// </summary>
private void PieSeries2Updater(PieDataPoint pieDataPoint, Rect pieBounds)
{
    var brush = pieDataPoint.Background as RadialGradientBrush;
    if (null != brush)
    {
        var center = new Point(
            pieBounds.Left + ((pieBounds.Right - pieBounds.Left) / 2),
            pieBounds.Top + ((pieBounds.Bottom - pieBounds.Top) / 2));
        brush.Center = center;
        brush.GradientOrigin = center;
        var radius = (pieBounds.Right - pieBounds.Left) / 2;
        brush.RadiusX = radius;
        brush.RadiusY = radius;
    }
}

Note that the constructor simply hooks things up and that each helper method is specific to the pie design it will be updating. All each updater does is nudge the pixel coordinates of its gradient to match up with the pie's size. So it seems reasonable to assume that other - potentially more complex - pie designs can be updated just as easily.

Here's the proof-of-concept wrapper method that does the bulk of the work:

/// <summary>
/// Updates the PieDataPoints of a PieSeries by applying the specified action to each.
/// </summary>
/// <param name="pieSeries">PieSeries instance to update.</param>
/// <param name="updater">Action to run for each PieDataPoint.</param>
/// <param name="keepUpdated">true to attach to the SizeChanged event of the PieSeries's PlotArea.</param>
public static void UpdatePieSeries(PieSeries pieSeries, Action<PieDataPoint, Rect> updater, bool keepUpdated)
{
    // Apply template to ensure visual tree containing PlotArea is created
    pieSeries.ApplyTemplate();
    // Find PieSeries's PlotArea element
    var children = Traverse<FrameworkElement>(
        pieSeries,
        e => VisualTreeChildren(e).OfType<FrameworkElement>(),
        element => null == element as Chart);
    var plotArea = children.OfType<Panel>().Where(e => "PlotArea" == e.Name).FirstOrDefault();
    // If able to find the PlotArea...
    if (null != plotArea)
    {
        // Calculate the diameter of the pie (0.95 multiplier is from PieSeries implementation)
        var diameter = Math.Min(plotArea.ActualWidth, plotArea.ActualHeight) * 0.95;
        // Calculate the bounding rectangle of the pie
        var leftTop = new Point((plotArea.ActualWidth - diameter) / 2, (plotArea.ActualHeight - diameter) / 2);
        var rightBottom = new Point(leftTop.X + diameter, leftTop.Y + diameter);
        var pieBounds = new Rect(leftTop, rightBottom);
        // Call the provided updater action for each PieDataPoint
        foreach (var pieDataPoint in plotArea.Children.OfType<PieDataPoint>())
        {
            updater(pieDataPoint, pieBounds);
        }
        // If asked to keep the gradients updated, hook up to PlotArea.SizeChanged as well
        if (keepUpdated)
        {
            plotArea.SizeChanged += delegate
            {
                UpdatePieSeries(pieSeries, updater, false);
            };
        }
    }
}

Attentive readers may have noticed that I'm using the same Traverse<T> implementation that ImplicitStyleManager uses - though I've got a custom getChildNodes implementation that works with the visual tree:

/// <summary>
/// Implementation of getChildNodes parameter to Traverse based on the visual tree.
/// </summary>
/// <param name="reference">Object in the visual tree.</param>
/// <returns>Stream of visual children of the object.</returns>
private static IEnumerable<DependencyObject> VisualTreeChildren(DependencyObject reference)
{
    var childrenCount = VisualTreeHelper.GetChildrenCount(reference);
    for (var i = 0; i < childrenCount; i++)
    {
        yield return VisualTreeHelper.GetChild(reference, i);
    }
}

 

And that's all there is to it! Now, not only do you know how to create appealing, holistic designs for fixed-size pie charts, you've got an easy way to keep those designs looking sharp for dynamically-sized pie charts as well. So go forth with that knowledge - and make even tastier pies! :)

[PieDataPointMappingModeUpdater.zip]

Expanded access to Silverlight 2's generic.xaml resources [SilverlightDefaultStyleBrowser updated for better compatibility!]

Kind reader (and fellow Silverlight Charting blogger!) Pete Brown contacted me recently to report that my SilverlightDefaultStyleBrowser application (background reading available here, here, here, here, and here) didn't seem to be working for assemblies from the Silverlight Toolkit or Telerik's RadControls for Silverlight. Specifically, he found that SilverlightDefaultStyleBrowser would work successfully with the controls in the Silverlight runtime and in the Silverlight SDK - but when he used the "Add Assembly" button to add assemblies from the Toolkit or RadControls, no new controls appeared in the list. This was unexpected and I started investigating...

Silverlight Toolkit: This scenario was particularly troubling because I'm on the Toolkit team and I know it's not doing anything weird that should break SilverlightDefaultStyleBrowser. So I stepped through the code for parsing generic.xaml and discovered that the root ResourceDictionary wasn't getting loaded. A bit more debugging revealed that this was because the Silverlight Toolkit's generic.xaml uses a different XAML namespace than SilverlightDefaultStyleBrowser was looking for (and than nearly every other Silverlight assembly I've seen). But the Toolkit is not at fault here - it turns out that Silverlight supports two different XAML namespaces! So I added support for the second namespace to SilverlightDefaultStyleBrowser, and now it happily parses assemblies from the Silverlight Toolkit. :)

RadControls: I'd assumed the issue here was the same and expected the RadControls to "just work" now that I'd added support for the second XAML namespace - but I was wrong. :( So I stepped through SilverlightDefaultStyleBrowser's generic.xaml parsing code again (recall that generic.xaml is a public entry point for Silverlight assemblies) and discovered that SilverlightDefaultStyleBrowser was finding and parsing the RadControls generic.xaml just fine all along - except that there weren't any Style elements in it. So SilverlightDefaultStyleBrowser's behavior was "correct" in the first place! What's different here is that the Telerik assemblies expose a generic.xaml containing references to custom elements that work at run time (when generic.xaml is parsed and executed by Silverlight), but which do not work with the simple XML-level parsing that SilverlightDefaultStyleBrowser performs. I'm guessing it would be fairly straightforward to modify SilverlightDefaultStyleBrowser to successfully expose the Telerik Styles, but I'm not going to add that custom code until/unless I hear from someone at Telerik that this is something they're okay with. So (for now, at least), SilverlightDefaultStyleBrowser doesn't find any Styles in the RadControls assemblies; that behavior is correct and "by design".

Having investigated the problem report and fixed what I could, I updated the public SilverlightDefaultStyleBrowser application and the downloadable source code. And started writing this post! :)

 

The version number of SilverlightDefaultStyleBrowser appears in the window's title and the latest release number is 1.0.3268.34946. (Note: I haven't updated the screen shot below which shows the introductory version number.) If installed via ClickOnce, the application should automatically prompt you to upgrade when it detects the update (which typically happens after running the application once or twice). If you're using the standalone EXE, you'll need to update manually.

SilverlightDefaultStyleBrowser Application

Click here or on the image above to install SilverlightDefaultStyleBrowser via ClickOnce with automatic updating.

Click here to download the standalone SilverlightDefaultStyleBrowser executable and source code in a ZIP file.

 

SilverlightDefaultStyleBrowser was written to do one thing and to do it simply. As is often the case when trying to duplicate existing behavior, there tend to be a few surprises along the way where things turn out not to work quite as expected. This was one of those surprises and I'm glad for the opportunity to fix this and make SilverlightDefaultStyleBrowser just a little more useful for everyone!

Great Silverlight charts are still just a click away [ChartBuilder sample and source code updated for Charting's December 08 release]

In yesterday's announcement of the December 08 release of Charting for Silverlight, I outlined some of the new Charting features and mentioned a forthcoming update to my ChartBuilder tool to show off the new features. (Background reading for ChartBuilder: Introduction and "user's guide", Updates.)

The new ChartBuilder is now live on the web - and I've even updated my sample screenshot! :)

You can click this text or the image below to run the latest ChartBuilder in your browser.

ChartBuilder

[And click here to download the complete ChartBuilder source code.]

Release notes:

  • Added support for new series type BubbleSeries. To keep thing simple and avoid needing to specify another set of values, the bubble size is bound to the dependent value of the series.
  • Added support for the new Independent(Range|Category)Axis and Dependent(Range|Category)Axis properties of the Column/Bar/Line/Scatter/Bubble series types. Off by default, either axis can be enabled by checking the relevant box in the settings panel for a series - then customized as always.
  • Added a ToolTip for the "Axes" header label to display the current value of the Chart.ActualAxes property in pseudo-XAML. More of a diagnostic aid than anything, this information can help understand the interaction between axes and series - and what's really going on behind the scenes.
  • Increased the maximum number of stand-alone Axis instances from 2 to 4 now that multiple axes are supported.
  • Various other improvements.
  • Updated the version to 2008-12-07.

ChartBuilder continues to be tremendously useful to me as an interactive Charting test application. The plethora of knobs, dials, and switches that represent a user experience designer's worst nightmare [ :) ] enable a degree of scenario testing that has helped find and fix a wide variety of issues. Furthermore, the interactive nature of the tool - and the ease with which it enables mocking up samples for answers on the Silverlight Controls and Silverlight Toolkit forum - has really seemed to help people understand Charting more easily.

My hope is that ChartBuilder can be just as useful to you - so if you've got ideas after using it, please let me know!

Silverlight Charting gets a host of improvements [Silverlight Toolkit December 08 release now available!]

The December 08 release of the Silverlight Toolkit was published a short while ago. Just about every control in the Toolkit got some love and attention for this release and I encourage you to have a look, download it, and enjoy!

That said, readers of my blog know that I'm all about the Charting. :) So just like my original Charting announcement and overview post, here's an announcement of the December 08 Charting release and an overview of some of the high points. By now, I assume folks have had time to play around with Charting and are starting to get into some more advanced scenarios, so the samples are going to be a little more technical than before. If you want a refresher on basic Charting concepts, please have a look at my original overview - all of those concepts still apply.

Though the Charting team was down to just two of us for this release, Jafar and I have done our best to deliver some pretty compelling new features. It's important to note that there are a few breaking changes from the November 08 release, but I hope you'll agree that the new functionality is worth the minor inconvenience of updating existing Charting code. As it happens, if you don't explicitly use the Axis class, you probably won't need to change anything at all!

 

I wrote the following summary for the release notes, and it seems like a good way to begin:

Notable Changes

Support has been added for arbitrary numbers of Axes for a Chart. A key and significant consequence of this change is that it is now possible to mix previously incompatible Series in the same chart (example: Column and Bar). The former "automatically share axes when possible" behavior remains present and can be used to render two series with a shared independent axis and different dependent axes in order to display related-but-differently-scaled values (example: an engine's torque and RPM).

The Column/Bar/Line/Scatter Series classes expose two new (optional) properties which specify a particular Axis instance to be used by the Series (rather than letting the Series choose from the Chart's Axes collection as it does by default). These properties allow very specific customization of each Series' axes in situations where maximum control is desired.

A design-time assembly for Charting has been created which is automatically used by Blend and Visual Studio to enhance the design-time experience. Most class properties are now categorized into custom "Data Visualization" and "Data Visualization Styles" categories to make them easier to find - while particularly common properties are now found in the "Common" category. ToolTips identify the purpose of each control and property, and Toolbox icons are automatically associated with each class. (Note: Some of these features are only supported by one of the design tools.)

The design-time behavior of DataPoints has been enhanced: DataPoints are now visible by default when dropped onto the design surface in Blend, so styling them is easier. PieDataPoint now creates a default Geometry (that can be customized interactively with the ActualRatio and ActualOffsetRatio properties) which makes it possible to see and understand relevant styling changes. DataPoints in a Chart show up in Visual Studio without a refresh of the design surface.

The Chart class is now decorated with ContentPropertyAttribute("Series") which means that it is no longer necessary to explicitly wrap Series with <charting:Chart.Series> ... </charting:Chart.Series> in XAML. Additionally, this enables Blend to display a Chart's Series in the "Objects and Timeline" pane and makes accessing the properties of a Series considerably easier. Example of simplified XAML syntax:

    <charting:Chart>
        <charting:PieSeries ... />
    </charting:Chart>

The behavior of the Axis classes during animations that expand or shrink the range of displayed data has been changed so that the animation of the size change is smooth (vs. jumping between axis intervals). This significantly increases the ease with which dynamic data changes can be observed and understood by the viewer.

The DataPointSeries class (a subclass of Series) has been unsealed to make the task of writing a custom Series considerably easier. While this change doesn't expose the entire hierarchy on which the "in-box" Series are built, it is a significant benefit to developers because DataPointSeries implements many of the key Series notions: the ItemsSource property, DataPoint creation, dynamic data detection, change animation, show/hide transitions, DataPoint selection, and more.

A new Series type, BubbleSeries, has been added (along with its associated BubbleDataPoint class). BubbleSeries is similar in nature to ScatterSeries, but conveys additional information by using the size of the data points to display an additional dependent value for each of the data points.

In scenarios where multiple columns/bars (of ColumnSeries/BarSeries) share the same category, all such data points will now be automatically displayed overlapping so that smaller values will no longer be obscured from view by larger values.

Breaking Changes

The Axis.AxisType property has been replaced by a corresponding hierarchy of Axis classes. The concrete classes map directly to the AxisType values that were removed: LinearAxis, DateTimeAxis, CategoryAxis. In addition to improving overall API consistency, a consequence of this refactoring is that the LinearAxis.Minimum/Maximum properties are now strongly typed as double and the corresponding properties on DateTimeAxis are strongly typed as DateTime - both of which improve development-time and design-time usability.

The Axis.ShouldIncludeZero property has been removed; this property is has no meaning for some axis types and is typically not needed because the same thing can be accomplished by setting Minimum or Maximum accordingly. A consequence of this is that Column/Bar charts will usually not include the 0 value by default (and therefore the "beginnings" of the columns/bars can be truncated). This behavior is consistent with Excel (demonstrated by creating a column chart of the values 10, 11, and 12 in Excel), though truncation is more likely because Charting's heuristics are more aggressive with regard to excluding 0.

Other Changes

Various UI improvements and bug fixes.

Whew - there's a lot of good stuff in there! :)

 

So now that we have an idea what's new, let's start by looking at the support for multiple axes with a common scenario: charting two quantities that are related, but measured in different units and/or on different scales. For the purposes of this demonstration, let's chart the performance characteristics of an imaginary engine:

Chart with multiple axes

[Note: Complete source code for all of the sample charts here can be found in the ChartingIntroduction.zip file attached to this post.]

The XAML for this Chart is straightforward (and discussed below):

<charting:Chart Title="Engine Performance">
    <!-- Power curve -->
    <charting:LineSeries
        Title="Power"
        ItemsSource="{StaticResource EngineMeasurementCollection}"
        IndependentValueBinding="{Binding Speed}"
        DependentValueBinding="{Binding Power}"
        MarkerWidth="5"
        MarkerHeight="5">
        <!-- Vertical axis for power curve -->
        <charting:LineSeries.DependentRangeAxis>
            <charting:LinearAxis
                Orientation="Vertical"
                Title="Power (hp)"
                Minimum="0"
                Maximum="250"
                Interval="50"
                ShowGridLines="True"/>
        </charting:LineSeries.DependentRangeAxis>
    </charting:LineSeries>
    <!-- Torque curve -->
    <charting:LineSeries
        Title="Torque"
        ItemsSource="{StaticResource EngineMeasurementCollection}"
        IndependentValueBinding="{Binding Speed}"
        DependentValueBinding="{Binding Torque}"
        MarkerWidth="5"
        MarkerHeight="5">
        <!-- Vertical axis for torque curve -->
        <charting:LineSeries.DependentRangeAxis>
            <charting:LinearAxis
                Orientation="Vertical"
                Title="Torque (lb-ft)"
                Minimum="50"
                Maximum="300"
                Interval="50"/>
        </charting:LineSeries.DependentRangeAxis>
    </charting:LineSeries>
    <charting:Chart.Axes>
        <!-- Shared horizontal axis -->
        <charting:LinearAxis
            Orientation="Horizontal"
            Title="Speed (rpm)"
            Interval="1000"
            ShowGridLines="True"/>
    </charting:Chart.Axes>
</charting:Chart>

We start with the usual Chart object to contain the series - but now we can put the series directly inside the Chart tags instead of inside nested Chart.Series tags (though that syntax still works and is fully supported). There are two LineSeries here (one for torque and one for power) and each starts out as you'd expect by hooking up to the data and doing a bit of customization. But then there's something new: the LineSeries.DependentRangeAxis property is used to identify a specific LinearAxis. Every series type (other than PieSeries which doesn't have axes) now exposes a Dependent???Axis property and an Independent???Axis property (where "???" is "Range" or "Category" depending on which series it is).

Normally, a series will search the Chart.Axis collection to find an axis that it can use - but when these new properties are set, it always uses the specified axis. So in this example we're providing a specific LinearAxis for each LineSeries to use and we're customizing it slightly to get the chart looking just how we want. Having specified both LineSeries in this manner, a third LinearAxis is added to the Chart.Axis collection where it will be found - and used - by both LineSeries when they acquire an independent value axis.

 

Moving on, here's a chart using the new BubbleSeries in a financial scenario (for a fictional stock ticker symbol). It's the usual "stock price by date" chart we've all seen before - but this chart is also showing the trading volume each day by varying the size the data points:

Bubble chart

The XAML for that Chart is what you'd expect:

<charting:Chart Title="Stock Performance">
    <!-- Stock price and volume -->
    <charting:BubbleSeries
        Title="ABCD"
        ItemsSource="{StaticResource StockDataCollection}"
        IndependentValueBinding="{Binding Date}"
        DependentValueBinding="{Binding Price}"
        SizeValueBinding="{Binding Volume}"
        DataPointStyle="{StaticResource CustomBubbleDataPointStyle}"/>
    <charting:Chart.Axes>
        <!-- Axis for custom labels -->
        <charting:DateTimeAxis
            Orientation="Horizontal">
            <charting:DateTimeAxis.AxisLabelStyle>
                <Style TargetType="charting:DateTimeAxisLabel">
                    <Setter Property="StringFormat" Value="{}{0:MMM d}"/>
                </Style>
            </charting:DateTimeAxis.AxisLabelStyle>
        </charting:DateTimeAxis>
    </charting:Chart.Axes>
</charting:Chart>

BubbleSeries is used just like a ScatterSeries, but it exposes an additional property SizeValueBinding that works just like Independent/DependentValueBinding to identify the source of the size values. The DataPointStyle property specifies a custom style for BubbleDataPoint (not shown here) where we've added the day's volume to the ToolTip of each bubble. This custom style was created in the usual manner - by starting from the default style for BubbleDataPoint and making the desired changes. (I did so in Visual Studio's XAML editor, but it could just as easily be done in Blend.)

Tweaking things just a little more, the independent value axis is customized by setting its AxisLabelStyle property and providing a style for the new DateTimeAxisLabel class. DateTime/NumericAxis/AxisLabel are the classes used to display an axis's labels. In this case we've provided a custom StringFormat so that the dates are all displayed in the friendly Month+Day pattern seen above. DateTimeAxisLabel offers specific StringFormat properties for each supported IntervalType - but in this case we've simply used the StringFormat property which conveniently overrides the other, more specific types.

 

Next up is a fairly typical column chart showing made-up bowling scores for some of the people on the Toolkit team. But there's twist because there are two "Shawn"s on the team:

Chart with overlapping columns

By now, the XAML is probably kind of boring in its predictability:

<charting:Chart Title="Bowling Scores">
    <!-- Scores -->
    <charting:ColumnSeries
        Title="Score"
        ItemsSource="{StaticResource ScoreDataCollection}"
        IndependentValueBinding="{Binding Player}"
        DependentValueBinding="{Binding Score}">
        <charting:ColumnSeries.IndependentCategoryAxis>
            <!-- Axis for automatic sorting -->
            <charting:CategoryAxis
                Orientation="Horizontal"
                SortOrder="Ascending"/>
        </charting:ColumnSeries.IndependentCategoryAxis>
    </charting:ColumnSeries>
</charting:Chart>

This is a standard ColumnSeries, which is specifying a IndependentCategoryAxis so the new SortOrder property of CategoryAxis can be used to automatically sort the category names. This data set contains two items with the independent value "Shawn". Previously, both would have been displayed in the same category slot with the same width which means the column in front could have obscured the one in back and led viewers to believe that only four scores were being displayed. But now ColumnSeries and BarSeries automatically detect this situation and overlap columns/bars in the same category slot (after sorting them by size). This behavior makes it clear that five values are being shown and makes it easy to hover over either of the "Shawn"s to get a ToolTip with the associated score.

 

The previous chart is interesting for another reason, too: it's using the natural, simple way to display the data, but a purist might argue that it's also the "wrong" way to do so... [Jafar and I actually had this "argument" - I won't say who took which position. :) ] Categorizing by player name works well enough, but what if we also had an image for each player and we wanted to display that image instead of their name? Changing the ColumnSeries.IndependentValueBinding property might work, but it's become obvious that we're mixing display concerns and data concerns.

When maintaining developer/designer separation is critical, what's probably more appropriate is to categorize by the actual data object - and then display it however we want. Here's a chart that does just that:

Chart with unique categories

And here's the XAML - which is a bit more complicated than before, but still quite manageable:

<charting:Chart>
    <!-- Customized Title -->
    <charting:Chart.Title>
        <StackPanel>
            <TextBlock
                Text="Bowling Scores"
                HorizontalAlignment="Center"/>
            <TextBlock
                Text="(Alternate Approach)"
                FontSize="10"
                HorizontalAlignment="Center"/>
        </StackPanel>
    </charting:Chart.Title>
    <!-- Scores -->
    <charting:ColumnSeries
        Title="Score"
        ItemsSource="{StaticResource ScoreDataCollection}"
        IndependentValueBinding="{Binding}"
        DependentValueBinding="{Binding Score}">
        <charting:ColumnSeries.IndependentCategoryAxis>
            <!-- Axis for automatic sorting and custom labels -->
            <charting:CategoryAxis
                Orientation="Horizontal"
                SortOrder="Ascending">
                <charting:CategoryAxis.AxisLabelStyle>
                    <Style TargetType="charting:AxisLabel">
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="charting:AxisLabel">
                                    <TextBlock Text="{Binding Player}"/>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </charting:CategoryAxis.AxisLabelStyle>
            </charting:CategoryAxis>
        </charting:ColumnSeries.IndependentCategoryAxis>
    </charting:ColumnSeries>
</charting:Chart>

Now that we've set IndependentValueBinding to the data objects themselves, there are automatically five categories (one for each of the five objects in the data set). (Aside: CategoryAxis will still sort them for us because the data objects implement the IComparable interface.) Once the data is set up "properly", all that's left to do is customize how the objects are displayed - and this is done by providing a custom template for the AxisLabel class (something that's already been discussed).

One other thing to note above is how the Chart.Title property is set to a tree of UI elements instead of the usual string. Chart.Title (and other such properties in Charting) follows the ContentControl model because it makes simple things simple while also allowing more advanced scenarios. In this case, all we've done is provide a "subtitle" - but we could also have gone much further and created something very customized.

 

The last thing I wanted to highlight was some of the design-time enhancements for Charting in this release. Data points were previously a little tricky to customize because their default state is not visible (which allows the reveal/show animation to fade them in without flicker). Data points now check if they're being used in design-time and will automatically play their reveal/show animation so that they're visible on the design surface by default. PieDataPoint used to be extra tricky because its Geometry property is only set when it's actually being used by PieSeries. But now it knows about design-time and its ActualRatio/ActualOffsetRatio properties can be changed to interactively evaluate different styling approaches. Here's a stand-alone PieDataPoint in Blend showing off both of these improvements (along with the new "Data Visualization" property category):

PieDataPoint in Blend

 

I hope you're excited by some of the new functionality we've just looked at from the December 08 release of Charting! And naturally, we've made a some other improvements, too! :) You can browse the live Charting sample page to find out more - then download the December 08 Toolkit release and start playing around with the new stuff! As always, if you have questions about Charting, you can ask them in the Silverlight Controls forum. If you think you've found a bug, please report it with the Issue Tracker in the Toolkit's CodePlex site.

 

PS - If you're looking for an update to my ChartBuilder application (Background reading: Introduction, Update) to show off the new Charting functionality, please stay tuned because it will be available very soon! :)

PPS - I'm on vacation in December, so responses to blog comments and emails may be delayed. But please don't let that stop you from contacting me: I'll follow up on everything I get - I just might be a little slower than usual. :)

[ChartingIntroduction.zip]

Having problems with layout? Switch to Plan B! [LayoutTransformControl scenarios for WPF]

When I first wrote about adding full LayoutTransform fidelity to Silverlight with my LayoutTransformControl project, I mentioned that I'd found two scenarios where LayoutTransformControl's behavior differed from WPF's LayoutTransform. (Background reading for LayoutTransformControl: Motivation and introduction for Beta 1, Significant enhancements and update for Beta 2, Update for RTW, Fixes for two edge case bugs) These two differences were intentional because it seemed to me that the corresponding WPF behavior was incorrect. Now you can judge for yourself. :)

The sample WPF application shown here (note: complete source code is attached to this post) demonstrates two variants of the problematic scenarios; one rendered by WPF's LayoutTransform and the other by LayoutTransformControl. The top four cells demonstrate the first scenario and the bottom four cells demonstrate the second. Cells on the left show a variant that both implementations agree on while cells on the right show a small modification that demonstrates conflicting behavior. I've shaded the problematic sections red to help identify them. Here's how it looks:

WPF LayoutTransform issue demonstration

The XAML for top scenario's red quadrant is this:

<Grid Background="Orange">
    <Grid.LayoutTransform>
        <SkewTransform AngleX="-20"/>
    </Grid.LayoutTransform>
    <Grid MinHeight="75" MinWidth="75"/>
</Grid>

It's a simple skew on a free-sized Grid with arbitrary content (for simplicity, the content is another Grid with MinWidth/MinHeight, but this could be a Button or any other typical content). The behavior for positive skew angles makes sense to me, but the behavior for negative angles seems inconsistent and causes undesirable clipping of the content (which is really obvious when applied to a Button). The corresponding XAML for LayoutTransformControl's "correct" rendering is:

<l:LayoutTransformControl>
    <l:LayoutTransformControl.Transform>
        <SkewTransform AngleX="-20"/>
    </l:LayoutTransformControl.Transform>
    <Grid Background="Orange">
        <Grid MinHeight="75" MinWidth="75"/>
    </Grid>
</l:LayoutTransformControl>

The XAML for bottom scenario's red quadrant is this:

<Grid Width="75">
    <Grid Background="Orange">
        <Grid.LayoutTransform>
            <RotateTransform Angle="90"/>
        </Grid.LayoutTransform>
        <Grid MinHeight="125" MinWidth="125"/>
    </Grid>
</Grid>

It's a width-constrained, free-sized Grid with arbitrary content. The behavior for Angle=0 (or 180) makes sense to me, but the behavior for Angle=90 (or 270) does not; at this angle the content should be vertically unbounded and should stretch to the full 125 pixels of the inner Grid. Changing the nature of the outer constraint from width to height exhibits the same "doesn't stretch" behavior at Angle=90 (or 270). The corresponding XAML for LayoutTransformControl's "correct" rendering is:

<Grid Width="75">
    <l:LayoutTransformControl>
        <l:LayoutTransformControl.Transform>
            <RotateTransform Angle="90"/>
        </l:LayoutTransformControl.Transform>
        <Grid Background="Orange">
            <Grid MinHeight="125" MinWidth="125"/>
        </Grid>
    </l:LayoutTransformControl>
</Grid>

I reported both of these issues to the WPF team when I found them, but unfortunately they weren't able to address them for the .NET 3.5 SP1 release. Although it might put LayoutTransformControl out of a job, I'm optimistic that both will be fixed in a future update of WPF. :) For the time being, though, I humbly suggest giving LayoutTransformControl a try if you encounter unexpected behavior like this. It's probably the quickest, easiest patch you'll find!

[WpfLayoutTransformIssues.zip]

An unexceptional layout improvement [Two LayoutTransformControl fixes for Silverlight 2!]

I'd almost finished patting myself on the back for managing to implement WPF's LayoutTransform on Silverlight using just the RenderTransform available on that platform. (Background reading for LayoutTransformControl: Motivation and introduction for Beta 1, Significant enhancements and update for Beta 2, Update for RTW) Yes, everything was peachy - until I was contacted by kind reader Matthew Serbinski with a report of an InvalidOperationException being thrown by Silverlight when using LayoutTransformControl with a ScaleTransform with ScaleY=0... :(

You've probably already recognized ScaleY=0 as something of an edge case for layout: a value which collapses everything into nothingness. I looked into how WPF's LayoutTransform code handled this situation and discovered that it specifically detected circumstances corresponding to a transformation matrix without an inverse - and skipped performing the usual layout computations. My LayoutTransformControl implementation didn't look for this special case, ended up violating one of the rules of the layout system, and triggered the reported exception.

So I added a little bit of code to handle such input the same way WPF does. And while I was at it, I checked to see if maybe there were other special cases that LayoutTransformControl wasn't handling properly... Sure enough, I found one other scenario: that of needing to layout within an container having no width or height. In this case, LayoutTransformControl's behavior wasn't wrong enough to cause an exception (or any visible problem I noticed), but I made a similar tweak for consistency with WPF.

Changes in place, I modified the LayoutTransformControl sample application, its Silverlight test framework, and its WPF test framework to allow setting both ScaleX and ScaleY to 0. (I'd formerly limited ScaleX/ScaleY to positive values which is why I didn't realize there was a problem myself.) Then I spent some time playing around with the test apps: trying all kinds of things and looking for any other anomalous behavior. Nothing turned up, so I updated the LayoutTransformControl source code download and started writing this post... :)

LayoutTransformControl Sample Application

Changes to code always involve a certain amount of risk that a regression will be introduced. Fortunately, the changes here are small, self-contained, and easy to test - so I'm optimistic they won't cause problems for those of you already using LayoutTransformControl in your projects. Of course, if there are any new problems - or existing ones I don't know about yet! - please let me know and I'll look into them as quickly as I can.

Thank you for your help - happy LayoutTransform-ing!