A subtle(r) TextBox for UWP

I’m not a huge fan of the standard text input control in most platforms. Not in Windows Forms, not in HTML, not on any of the XAML platforms. The UWP TextBox control is not an exception. In its default style, a TextBox is a huge, sterile bunch of white pixels staring at you. It waits for your input, and even after you provided a value, it remains a a huge, sterile bunch of white pixels staring at you. When a TextBox asks for your name and you type ‘Tim Smith’, it still keeps enough bordered white space to hold ‘Hubert Blaine Wolfeschlegelsteinhausenbergerdorff, Sr.’ When a TextBox asks for your city and you type ‘Rome’ or ‘Paris’, it still keeps enough bordered white space to hold ‘Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch’.

Enough philosophy, I think I made my point: let’s build a better TextBox.

This article presents the SubtleTextBox control (pending registration). It’s a TextBox control for UWP that looks like a regular TextBox (white and bordered) when it has the focus, but looks more subtle (hence its name) when the user is paying attention to other controls on the view. When the SubtleTextBox does not have the focus, it looks more (or entirely) like a TextBlock – with a transparent background and no border. When the user is moving through the input controls in ‘slow’ mode -by clicking or pressing the tab key- the transition between the two states is smoothly animated. In ‘fast’ mode -hovering the mouse- the transition goes without animation to give immediate feedback to the user. Here’s how the control looks like in a small sample app. This app displays some views with different types of textboxes. The ones that fade away smoothly when losing focus are SubtleTextBox instances:
SubtleTextBoxFull

Building the SubtleTextBox

Here’s how SubtleTextBox was built. I started with deriving a class from TextBox, adding a field to hold its current state:

/// <summary>
/// TextBox that looks like a TextBlock when not editing.
/// </summary>
public class SubtleTextBox : TextBox
{
    private bool isInTextBlockMode = false;

    // ...
}

The difference between TextBox-Style and TextBlock-Style lies in the Opacity of the control’s Background and BorderBrush. The Opacity of these elements in TextBoxStyle is 1, which is hardcoded as HighOpacity. The Opacity of these elements in pure TextBlockStyle is 0, but you may want to configure this to any value between 1 and 0, to apply a faded style. I created a dependency property called LowOpacity for this. That’s not the world’s best name for it -since it reveals implementation details- but it’s still better than the semantically correct ‘ReverseSubtleness’:

/// <summary>
/// Registers the LowOpacity dependency property.
/// </summary>
public static readonly DependencyProperty LowOpacityProperty = DependencyProperty.Register(
    "LowOpacity", 
    typeof(double), 
    typeof(SubtleTextBox), 
    new PropertyMetadata(0.0));

/// <summary>
/// Gets or sets the lowest opacity for border and background.
/// </summary>
/// <value>The low opacity.</value>
/// <remarks>This is the value used in TextBlock mode.</remarks>
public double LowOpacity
{
    get { return (double)GetValue(LowOpacityProperty); }
    set { SetValue(LowOpacityProperty, value); }
}

When the control is Loaded, we make sure to give the control its own SolidColorBrush instance of Background and BorderBrush. Otherwise we’ll simultaneously animate ALL text boxes (subtle and regular ones) on the view. If you want to see that show, just put the assignments in comment…

When the control appears on screen, it will look like a regular TextBox -to hint the user that it’s for input- and then it fades away to its TextBlock state:

/// <summary>
/// Initializes a new instance of the <see cref="SubtleTextBox"/> class.
/// </summary>
public SubtleTextBox()
{
    Loaded += SubtleTextBox_Loaded; ;
    timer.Interval = TimeSpan.FromSeconds(2);
    timer.Tick += Timer_Tick;
}

Here are the internal methods to switch visual state:

/// <summary>
/// Makes the control look like a read-only TextBlock.
/// </summary>
public void ApplyTextBlockStyle()
{
    if (isInTextBlockMode)
    {
        return;
    }

    isInTextBlockMode = true;
    Animate(HighOpacity, LowOpacity);
}

/// <summary>
/// Makes the control look like a regular TextBox.
/// </summary>
public void ApplyTextBoxStyle()
{
    if (!isInTextBlockMode)
    {
        return;
    }

    isInTextBlockMode = false;
    Animate(LowOpacity, HighOpacity);
}

They are called when the control retrieves and loses focus:

protected override void OnGotFocus(RoutedEventArgs e)
{
    timer.Stop();
    ApplyTextBoxStyle();
    base.OnGotFocus(e);
}

protected override void OnLostFocus(RoutedEventArgs e)
{
    ApplyTextBlockStyle();
    base.OnLostFocus(e);
}

The transition is made of two simultaneous DoubleAnimations (Opacity of Background and Opacity of BorderBrush) in a StoryBoard. Typically story boards are defined in XAML. If you create these programmatically you can hook them in the visual tree with Storyboard.SetTarget and Storyboard.SetTargetProperty. Also don’t forget to activate EnableDependentAnimation, or you’ll see nothing:

private void Animate(double from, double to)
{
    var storyboard = new Storyboard();

    var animation = new DoubleAnimation
    {
        From = from,
        To = to,
        Duration = new Duration(TimeSpan.FromMilliseconds(Duration)),
        EnableDependentAnimation = true
    };
    Storyboard.SetTarget(animation, BorderBrush);
    Storyboard.SetTargetProperty(animation, nameof(BorderBrush.Opacity));
    storyboard.Children.Add(animation);

    animation = new DoubleAnimation
    {
        From = from,
        To = to,
        Duration = new Duration(TimeSpan.FromMilliseconds(Duration)),
        EnableDependentAnimation = true
    };
    Storyboard.SetTarget(animation, Background);
    Storyboard.SetTargetProperty(animation, nameof(Background.Opacity));
    storyboard.Children.Add(animation);

    storyboard.Begin();
}

I could have manipulated and animated the controls ‘official’ VisualStates, but these are not supposed to be animated.

Here’s how to use SubtleTextBox in XAML:

<controls:SubtleTextBox PlaceholderText="Subtle TextBox 0 %" />
<controls:SubtleTextBox PlaceholderText="Subtle TextBox 10 %" 
                        LowOpacity=".1" />

Here’s how it looks like in the sample app:

SubtleTextBoxShort

I use the SubtleTextBox in some of my views to host the non-mandatory input fields. But there’s also another use case:

Extending a Slider

A long time ago in a galaxy far away –called Windows 8- I wrote an ‘EnhancedSlider‘ control. It was a Slider that came with a TextBox to allow the user to manually adjust its value. I used it successfully in some apps. Today, the need for such a control is even higher: on the Windows 10 Universal Platform we can not make any assumption anymore on screen sizes. The user (or the hardware) may make a view so narrow that any Slider control would become inaccurate. A Slider can still be used to get in close range of the intended value, but it makes sense to allow keyboard input to let the user enter the exact final  value.

I actually created SubtleTextBox for this purpose. It comes with a behavior that I didn’t mention yet. The control it can ‘flash’: it can switch to TextBox mode and get back to TextBlock mode to get the attention of the user. That way it can be used as an extension to input controls such as a Slider, or a RadialGauge in interactive mode.

When the value of the slider has changed (through manipulation or two-way binding), we can call the SuggestInput method to indicate the user that there is an alternative input control bound to the same value.

Here’s how the method is implemented:


private DispatcherTimer timer = new DispatcherTimer();

/// <summary>
/// Briefly makes the control look like a regular TextBox.
/// </summary>
public void SuggestInput()
{
    ApplyTextBoxStyle();
    timer.Start();
}

private void Timer_Tick(object sender, object e)
{
    timer.Stop();
    ApplyTextBlockStyle();
}

Here’s how the controls are hooked to each other in a sample view:

<controls:SubtleTextBox x:Name="ValueBox"
                        Text="{x:Bind Model.SomeValue, Mode=TwoWay}"
                        LowOpacity=".1"
                        InputScope="Number"
                        HorizontalAlignment="Right"
                        TextAlignment="Right"
                        Margin="0 40 0 0" />
<Slider x:Name="ValueSlider"
        Maximum="500"
        Value="{x:Bind Model.SomeValue, Mode=TwoWay}"
        ValueChanged="ValueSlider_ValueChanged" />
private void ValueSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
    ValueBox.SuggestInput();
}

Here’s how this looks like in the sample app – the TextBox that decorates the top Slider is a regular one, the two others are of the subtle flavor:
SubtleTextBoxSlider

Please consider that all controls in this sample view are bound to the same value in the ViewModel. So the two subtle textboxes flash together, which is not very … subtle. In a real app, it looks a lot better. Here’s an example. The textbox that decorates the slider, and the textbox for the optional notes are ‘subtle’ versions:
SubtleTextBoxInReal

The SubtleTextBox and its sample app live here on GitHub.

Enjoy!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s