Feeds:
Posts
Comments

Archive for August, 2008

The first rule of WPF

Avoid control inheritance.

I knew this rule.  I’ve read it several times from many different sources.  I’ve even given this advice to others.  However, I’m also guilty of forgetting this rule.

Recently, I had the need for a MaskedTextBox in WPF.  Marlon Grech, a fellow WPF Disciple, has a great article about how he created such a control for his AvalonControlsLibrary.  Having read that post, my first inclination was to create my own MaskedTextBox (not a matter of NIH syndrome, but the lesser evil of simply not wanting to deal with the legal paperwork necessary to use Marlon’s code at work).  I spent a few days perfecting this control (there’s a lot of corner cases you have to deal with, and I wanted full MaskedTextBox functionality, which makes it even more difficult).  Once I was done, a coworker pointed out to me that we didn’t pick up the Aero styling that our application uses.  It’s a simple fix at an application level, but not so easy to fix at a library level.  Not a huge deal, but this got me to thinking about how to address the problem.  Then it dawned on me… I shouldn’t have created a MaskedTextBox at all!

You see, if I’d followed the "first rule of WPF", I wouldn’t have had the styling issue.

So, what’s the better solution here, then?  Well, you simply use attached properties and behaviors instead!  Here’s a naive example of this concept.  Please beware that this is NOT a complete solution, and won’t begin to work for you as is.  It only is here for illustrative purposes.  I can’t publish a complete solution since I developed this during work hours.  However, with this illustration, it shouldn’t be too difficult to develop a complete solution.  Anyway, on to the code.

public class MaskedText : IDisposable
{
    private readonly TextBox _target;
    private MaskedTextProvider _provider;

    private MaskedText(TextBox target)
    {
        _target = target;
        _target.PreviewTextInput += OnPreviewTextInput;
        CreateProvider();
    }

    public void Dispose()
    {
        _target.PreviewTextInput -= OnPreviewTextInput;
    }

    private void OnPreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
    {
        e.Handled = true;
        ReplaceText(e.Text);
    }

    private void CreateProvider()
    {
        string text = _target.Text;
        if (_provider != null)
        {
            text = _provider.ToString(false, false);
        }

        _provider = new MaskedTextProvider(
            GetMask(_target),
            CultureInfo.CurrentCulture,
            true, // allow prompt as input
            GetPromptChar(_target),
            '',
            false); // Ascii only

        int testPosition;
        MaskedTextResultHint hint;
        _provider.Replace(text, 0, _provider.Length, out testPosition, out hint);
        SetText();
    }

    private void SetText()
    {
        int start = _target.SelectionStart;
        _target.Text = GetFormattedDisplayString();
        _target.SelectionStart = start;
        _target.SelectionLength = 0;
    }

    private string GetFormattedDisplayString()
    {
        bool flag;
        if (_target.IsReadOnly)
        {
            flag = false;
        }
        else if (DesignerProperties.GetIsInDesignMode(_target))
        {
            flag = true;
        }
        else
        {
            flag = !GetHidePromptOnLeave(_target) || _target.IsFocused;
        }

        return _provider.ToString(false, flag, true, 0, _provider.Length);
    }

    private void ReplaceText(string text)
    {
        int start = _target.SelectionStart;
        int end = start + _target.SelectionLength - 1;

        int testPosition;
        MaskedTextResultHint hint;
        if (end >= start)
        {
            _provider.Replace(text, start, end, out testPosition, out hint);
        }
        else
        {
            _provider.InsertAt(text, start, out testPosition, out hint);
        }

        if (hint == MaskedTextResultHint.Success ||
            hint == MaskedTextResultHint.CharacterEscaped ||
            hint == MaskedTextResultHint.NoEffect ||
            hint == MaskedTextResultHint.SideEffect)
        {
            SetText();
            _target.SelectionStart = testPosition + text.Length;
            _target.SelectionLength = 0;
        }
        else
        {
            //RaiseMaskInputRejectedEvent(hint, start);
        }
    }

    #region Instance

    private static readonly DependencyProperty InstanceProperty =
        DependencyProperty.RegisterAttached("Instance", typeof(MaskedText), typeof(MaskedText),
            new FrameworkPropertyMetadata((MaskedText)null));

    private static MaskedText GetInstance(DependencyObject d)
    {
        return (MaskedText)d.GetValue(InstanceProperty);
    }

    private static void SetInstance(DependencyObject d, MaskedText value)
    {
        d.SetValue(InstanceProperty, value);
    }

    #endregion

    #region Mask

    public static readonly DependencyProperty MaskProperty =
        DependencyProperty.RegisterAttached(
            "Mask",
            typeof(string),
            typeof(MaskedText),
            new FrameworkPropertyMetadata((string)null, new PropertyChangedCallback(OnMaskChanged), new CoerceValueCallback(CoerceMaskValue)),
            new ValidateValueCallback(IsMaskValid));

    public static string GetMask(DependencyObject d)
    {
        return (string)d.GetValue(MaskProperty);
    }

    public static void SetMask(DependencyObject d, string value)
    {
        d.SetValue(MaskProperty, value);
    }

    private static void OnMaskChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue == null)
        {
            Unattach(d);
        }
        else
        {
            Attach(d);
        }
    }

    private static object CoerceMaskValue(DependencyObject d, object value)
    {
        string mask = (string)value;
        if (string.IsNullOrEmpty(mask))
        {
            return null;
        }
        return value;
    }

    private static bool IsMaskValid(object value)
    {
        string mask = (string)value;
        if (string.IsNullOrEmpty(mask))
        {
            return true;
        }

        foreach (char ch in mask)
        {
            if (!MaskedTextProvider.IsValidMaskChar(ch))
            {
                return false;
            }
        }

        return true;
    }

    #endregion

    #region PromptChar

    public static readonly DependencyProperty PromptCharProperty =
        DependencyProperty.RegisterAttached(
            "PromptChar",
            typeof(char),
            typeof(MaskedText),
            new FrameworkPropertyMetadata('_', FrameworkPropertyMetadataOptions.None, new PropertyChangedCallback(OnPromptCharChanged)),
            new ValidateValueCallback(IsPromptCharValid));

    public static char GetPromptChar(DependencyObject d)
    {
        return (char)d.GetValue(PromptCharProperty);
    }

    public static void SetPromptChar(DependencyObject d, char value)
    {
        d.SetValue(PromptCharProperty, value);
    }

    private static void OnPromptCharChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MaskedText maskedText = GetInstance(d);
        if (maskedText != null)
        {
            maskedText._provider.PromptChar = (char)e.NewValue;
            maskedText.SetText();
        }
    }

    private static bool IsPromptCharValid(object value)
    {
        char ch = (char)value;
        return MaskedTextProvider.IsValidPasswordChar(ch);
    }

    #endregion

    #region IncludePrompt

    public static readonly DependencyProperty IncludePromptProperty =
        DependencyProperty.RegisterAttached(
            "IncludePrompt",
            typeof(bool),
            typeof(MaskedText),
            new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIncludePromptChanged)));

    public static bool GetIncludePrompt(DependencyObject d)
    {
        return (bool)d.GetValue(IncludePromptProperty);
    }

    public static void SetIncludePrompt(DependencyObject d, bool value)
    {
        d.SetValue(IncludePromptProperty, value);
    }

    private static void OnIncludePromptChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MaskedText maskedText = GetInstance(d);
        if (maskedText != null)
        {
            maskedText.CreateProvider();
        }
    }

    #endregion

    #region HidePromptOnLeave

    public static readonly DependencyProperty HidePromptOnLeaveProperty =
        DependencyProperty.RegisterAttached(
            "HidePromptOnLeave",
            typeof(bool),
            typeof(MaskedText),
            new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnHidePromptOnLeaveChanged)));

    public static bool GetHidePromptOnLeave(DependencyObject d)
    {
        return (bool)d.GetValue(HidePromptOnLeaveProperty);
    }

    public static void SetHidePromptOnLeave(DependencyObject d, bool value)
    {
        d.SetValue(HidePromptOnLeaveProperty, value);
    }

    private static void OnHidePromptOnLeaveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MaskedText maskedText = GetInstance(d);
        if (maskedText != null)
        {
            maskedText.SetText();
        }
    }

    #endregion

    private static void Attach(DependencyObject d)
    {
        TextBox textBox = d as TextBox;
        if (textBox != null)
        {
            MaskedText maskedText = GetInstance(d);
            if (maskedText == null)
            {
                maskedText = new MaskedText(textBox);
                SetInstance(d, maskedText);
            }
            else
            {
                maskedText.CreateProvider();
            }
        }
    }

    private static void Unattach(DependencyObject d)
    {
        MaskedText maskedText = GetInstance(d);
        if (maskedText != null)
        {
            maskedText.Dispose();
            SetInstance(d, null);
        }
    }
}

Usage is quite simple.

        <TextBox Margin="2" local:MaskedText.Mask="00/00/0000" local:MaskedText.PromptChar="?"/>

We’re no longer using a MaskedTextBox, but instead we use attached properties and behaviors to make existing TextBox controls behave appropriately.

With Styles, Templates, Decorators (check out the SpinDecorator by MindScape, for example) and attached properties and behaviors you can accomplish most of the things that you used to have to roll a new control for.  In the end, what you get is usually a more reusable piece of functionality than what you could get through inheritance.  For instance, the MaskedText class could be modified to support more than just a TextBox, giving you the same behavior for, say, a ComboBox, while inheritance limits how you can reuse the functionality.

WPF sure is powerful.

Read Full Post »

These are now available, and there’s much new in here. I’m going to provide some links to the pertinent stuff.

VS 2008 Service Pack 1:

VS 2008 Express Editions with Service Pack 1:

VS Team System 2008 Team Foundation SP 1:

.NET Framework 3.5 Service Pack 1

Team Foundation Server

A general description of the release can be found here.

Here’s a list of some of the new WPF features, with links to information about them.

There were some things left out of SP1 that we had been under the impression were "in the works".  Amongst these were a DateTimePicker and a DataGrid.  These did not make it into SP1, but it looks like we’ll be getting them "out of band" prior to their inclusion in some future WPF update.  The DataGrid has already been released to CodePlex in what’s being called the "WPF Toolkit", with the promise of this being the mechanism for introducing several other WPF features "out of band".

If I’ve missed any features, or if links are available for any of the features I didn’t provide links for, let me know and I’ll update this post.

Read Full Post »

Randomize Extension

I’m not sure how useful this is, but it’s a fun little thought experiment at the very least.  The goal here is to randomize the order of an IEnumerable<T> via an extension method.  It’s actually a rather simple one liner (well, two if you count the creation of the Random instance).  This specific implementation isn’t optimal, and it suffers all the same issues as the pseudo random number generator Random, since it’s built on that.  It does illustrate the power of functional programming and LINQ, though.

public static class RandomExtension
{
    public static IEnumerable<T> Randomize<T>(this IEnumerable<T> self)
    {
        return self.OrderBy(i => random.Next());
    }

    private static Random random = new Random();
}

Now you can easily iterate randomly over any enumerable.  LINQ/functional programming is quite fun, IMHO.

Edit 8/28: Greatly simplified the implementation of this extension method. The previous implementation was clever, but just not as efficient.  Looking at how simple this implementation is, you could argue there’s little reason to wrap it up in an extension method.

Read Full Post »