Friday, March 20, 2009

In this post I’d like to take an in-depth look at DomainUpDown. We’ll look at the control from a code-centric point of view and follow along some samples to showcase its features.

image

You can see DomainUpDown live here. The sample page is (currently) SL3, but the control itself works fine under both SL2 and SL3.
Jesse Liberty even did a video on it, which can be seen here.

A DomainUpDown is a control that ‘spins’ over a domain of items. As such it inherits from our UpDownBase and will not make assumptions on the type of your domain.

Items and ItemsControl

Let’s start by looking at how we instantiate a DomainUpDown in Xaml with a simple domain.

    <inputToolkit:DomainUpDown

     MinWidth="120"

     HorizontalAlignment="Left">

      <system:String>11111</system:String>

      <system:String>22222</system:String>

      <system:String>33333</system:String>

      <system:String>aaaaa</system:String>

      <system:String>bbbbb</system:String>

    </inputToolkit:DomainUpDown>

This is a weird domain of strings that we create directly through Xaml.
I would think that a more useful scenario is to bind Items straight to a collection, so the following might be used:

<inputToolkit:DomainUpDown
  ItemsSource="{Binding}"
  MinWidth="120"
  HorizontalAlignment="Left" />

In this case, apparently the DataContext is set to be a collection, let’s check the codebehind:

IEnumerable airports = Airport.SampleAirports;
DataContext = airports;

As we expected, the DataContext is set to a collection, that we get from some static class. Another sample is more direct about it:

CultureInfo[] cultures = new[]
                             {
                                 new CultureInfo("zh-Hans"),    // chinese simplified
                                 new CultureInfo("da"),         // danish
                                 new CultureInfo("nl-NL"),      // dutch
                                 new CultureInfo("en-US"),      // english us
                                 new CultureInfo("fr"),         // french
                                 new CultureInfo("de"),         // german
                                 new CultureInfo("he"),         // hebrew
                                 new CultureInfo("it"),         // italian
                                 new CultureInfo("ru"),         // russian
                                 new CultureInfo("es-ES")       // spanish
                             };
cultureList.ItemsSource = cultures;

Our domain exists of Cultures and we set the ItemsSource to point to it directly.

ButtonSpinner

Once you instantiate the DUD, you will notice that it exists of a TextBox and a ButtonSpinner. The ButtonSpinner consists of two buttons going Up and Down. I have introduced visual states on ButtonSpinner like so:

[TemplateVisualState(Name = VisualStates.StateIncreaseEnabled, GroupName = VisualStates.GroupIncrease)]
[TemplateVisualState(Name = VisualStates.StateIncreaseDisabled, GroupName = VisualStates.GroupIncrease)]

[TemplateVisualState(Name = VisualStates.StateDecreaseEnabled, GroupName = VisualStates.GroupDecrease)]
[TemplateVisualState(Name = VisualStates.StateDecreaseDisabled, GroupName = VisualStates.GroupDecrease)]
public abstract partial class Spinner : Control

and a DP to set influence them:

/// <summary>
/// Gets or sets the spin direction that is currently valid.
/// </summary>
public ValidSpinDirections ValidSpinDirection
{
    get { return (ValidSpinDirections)GetValue(ValidSpinDirectionProperty); }
    set { SetValue(ValidSpinDirectionProperty, value); }
}

ValidSpinDirections is an enum that will allow the ButtonSpinner to trigger the correct VisualState and disable buttons.

So, once you reach the edge of your domain, the correct button will be disabled.

IsCyclic

DUD introduces a DP called IsCyclic that will change the above behavior. If set to Cycle the DUD will not disable any buttons.

ItemTemplate

A DUD is templateable, and in order to do so, exposes a DP called ItemTemplate.

The ItemTemplate can be used to define the way an item should look in displaymode (see next paragraph).

<inputToolkit:DomainUpDown
      ItemsSource="{Binding}"
      HorizontalAlignment="Left"
      Height="Auto"
      FontSize="34">
      <inputToolkit:DomainUpDown.ItemTemplate>
        <DataTemplate>
          <Grid MinWidth="370">
            <Grid.Background>
              <SolidColorBrush Color="#aa000000" />
            </Grid.Background>
            <TextBlock
              Foreground="#22ffffff"
              Margin="4+0,2+0"
              FontSize="34"
              Text="{Binding CodeFaa}" />
            <StackPanel
              HorizontalAlignment="Right"
              Margin="0, 0, 8, 0">
              <TextBlock
                HorizontalAlignment="Right"
                Foreground="White"
                FontSize="12"
                Text="{Binding LimitedName}"
                Padding="2" />
              <TextBlock
                HorizontalAlignment="Right"
                Foreground="White"
                FontSize="14"
                Text="{Binding City}"
                Padding="2" />
              <TextBlock
                HorizontalAlignment="Right"
                Foreground="White"
                FontSize="14"
                Text="{Binding State}"
                Padding="2" />
            </StackPanel>
          </Grid>
        </DataTemplate>
      </inputToolkit:DomainUpDown.ItemTemplate>
    </inputToolkit:DomainUpDown>

Binding against our airlines sample data, this produces the following DUD:

image

The ItemTemplate is what gives our DUD most of its usefulness, as you will see in a while.

EditMode and DisplayMode

DUD introduces the concept of being in Display mode and in Edit mode:

[TemplateVisualState(Name = VisualStates.StateEdit, GroupName = VisualStates.GroupInteractionMode)]
[TemplateVisualState(Name = VisualStates.StateDisplay, GroupName = VisualStates.GroupInteractionMode)]

DisplayMode hides the TextBox and show a ContentControl, and EditMode will do the opposite.

The airlines DUD that you see above this paragraph is in DisplayMode. Going to EditMode is simple, by clicking your mouse inside the TextBox or tabbing into the control. You can use escape to cancel an edit and enter to commit an edit.It looks like this:

image

Edit mode can be used to quickly select an item. This allows rapid selection out of a huge list.
Once the user has typed some text, the default behavior is to check the domain for items that match using their ToString() representation. You can be sure that Airport has overridden ToString() to give back the “CodeFaa” property.

Please be mindful of your scenario. If you feel you wish to guide your users in their selection, please use an AutoCompleteBox.

That said, there are possibilities of going beyond a simple ToString comparison. Read on!

Converter, ConverterParameter and ConverterCulture

[disclaimer: lately other toolkit controls have been moving away from this strategy and we might decide introducing memberPaths is the best way forward for DUD as well.]

In order to be more sophisticated about how an item should be presented in a String format, you can plug-in a Converter.

Let’s see an example:

<inputToolkit:DomainUpDown
     Converter="{StaticResource BorderToStringConverter}"
     MinWidth="120"
     Height="Auto"
     HorizontalAlignment="Left">
     <Border
       Background="Red"
       Margin="4"
       BorderThickness="1"
       BorderBrush="Black"
       Width="80"
       Height="50" />
     <Border
       Background="Green"
       Margin="4"
       BorderThickness="1"
       BorderBrush="Black"
       Width="80"
       Height="50" />
     <Border
       Background="Blue"
       Margin="4"
       BorderThickness="1"
       BorderBrush="Black"
       Width="80"
       Height="50" />
   </inputToolkit:DomainUpDown>

That Xaml creates this visual:

image

In EditMode, we’ll see this:

image

As you can imagine, the ToString of Border is not ‘Green’. The DUD defines a Converter on line 2 and that BorderToStringConverter allows DUD to make a more informed decision on how to present the item in the TextBox and how to do a comparison.

The converter used here is quite simple:

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    // expecting a border
    Border element = value as Border;
    if (element != null)
    {
        SolidColorBrush b = element.Background as SolidColorBrush;

        if (b != null)
        {
            // use the colors class to find a friendly name for this color.
            string colorname = (from c in typeof(Colors).GetProperties(BindingFlags.Public | BindingFlags.Static)
                                where c.GetValue(null, new object[] { }).Equals(b.Color)
                                select c.Name).FirstOrDefault();

            // no friendly name found, use the rgb code.
            if (String.IsNullOrEmpty(colorname))
            {
                colorname = b.Color.ToString();
            }
            return colorname;
        }
    }
    return String.Empty;
}

The converter is only used to determine a string representation for the items in the domain. What happens if a user types a string that can’t be matched?

ParseError

The ParseError event would be raised.

Not handling that event will trigger a couple of behaviors that I will discuss in the next paragraph.
By handling this event, we could possibly add an item to the domain on the fly and select it. One of the samples does just that:

private void DomainUpDown_ParseError(object sender, UpDownParseErrorEventArgs e)
{
    DomainUpDown dud = (DomainUpDown)sender;

    // get the text that was unable to parse.
    string text = e.Text;

    ………… do ugly stuff here

    if (backgroundColor != null)
    {
        dud.Items.Add(new Border
        {
            Width = 120,
            Height = 80,
            Background = backgroundColor,
            BorderBrush = new SolidColorBrush(Colors.Yellow),
            BorderThickness = new Thickness(4)
        });
        dud.CurrentIndex = dud.Items.Count - 1;
    }
}

In that sample, the user could type Yellow and an item would be added that is a Border with a fill of Yellow.

[In case you were wondering why I did not just use an ItemTemplate and the Strings ‘Red’, ‘Green’, ‘Blue’ instead of all this Converter work: that wouldn’t make a good sample would it! But yes, it would have been easier!]

Invalid Input: InvalidInputAction and FallbackItem

Let’s pretend you did not handle the ParseError and the user typed something that can not be matched. There are several ways you might configure DUD to handle this situation.

The InvalidInputAction is an enum with two options: UseFallbackItem and TextBoxCannotLoseFocus.

When UseFallbackItem is set, DUD will look at the FallbackItem and select it. If there is no FallbackItem set, DUD will just ignore the edit and move back to DisplayMode (as if you pressed Escape).
If we set TextBoxCannotLoseFocus, the Error VisualState is triggered and DUD will attempt to set focus back to DUD when it loses focus.

image

This behavior can not be guaranteed (ofcourse) and should be used with great care. I would love feedback on this behavior, because it is quite daring indeed! If we get positive feedback on this, we might consider implementing it in other controls.

CurrentIndex

The DUD can be managed by setting the CurrentIndex DP.
If you set an index larger than the amount of Items, I will revert back to the old index and present you with an exception.

During Initialization from Xaml this is allowed though, and the index you wanted will be remembered and set as soon as possible.

IsEditable

You can make your DUD always be in DisplayMode by setting IsEditable to false. This enables fun slideshow scenarios.

Conclusion

A DomainUpDown is a very simple control that comes in handy when screen real-estate is at a premium, or when items are presented that can not easily be matched by a string representation or when you just want a page through data.

Pretty soon we’ll build an image slide show with it.

Name
E-mail
Home page

Comment (HTML not allowed)  

Enter the code shown (prevents robots):