Friday, October 7, 2011

Using Visual States to Set Focus on a Control

A common problem encountered in Silverlight applications is how to set the focus properly when transitioning between controls. For example, in the reference application I'm writing for the book Designing Silverlight Business Applications there is the option to either edit an existing record or create a new one. The result of clicking the appropriate button is a panel overlay that zooms into view. Obviously, it makes sense once the panel is rendered to set the focus to the first field so the user can simply begin entering information.

If the control was managed by adding and removing it from the visual tree, the solution would be simple. This is the perfect example I have of not resorting to exotic solutions to a simple problem. The focus problem is not a business problem, it is a user interface problem. It's perfectly valid to wire a loaded event to the control, name the field that requires focus, and call the Focus method on that field when the parent control is loaded. This is an example where even with MVVM, it just doesn't make sense to solve the problem in the view model when it is so simple to add the code-behind. This doesn't change my ability to have a design-time data or to test my view model. It also works well because if I reuse the same control, the loaded event will still fire every time it is added back to the visual tree.

I mentioned this would be simple if the control was managed based on adding and/or removing it ... but that's not how I manage it. Instead, I use the Jounce framework to position it in a region and show or hide it by changing the visual state. What's nice about this is that I can have a state based on the action that the view model is invoked with, for example "New" for a new item, "Edit" to edit an existing item, and "Closed" when the view should go away. These actions are all handled by setting the visual state, so there is no convuluted logic to set visibility properties or wire any other logic. The visual state is wired as a delegate by Jounce, so when I test my view models I can simply mock the delegate and confirm the transition is set without actually loading a view. I can also set different types of animations, such as zooming the control from the "new" button for one mode and having it fade in from the grid in the other mode.

The problem with this approach is that the control never leaves the visual tree. Everything is managed by visual states, so the "Closed" state simply sets the visibility to collapsed, which keeps it in the visual tree but just ignores any rendering or hit testing logic. So how can I effectively set the focus based on a visual state transition?

I've read some interesting solutions that create a special "reverse behavior" that wires to the control and can be data-bound to a property on the view model. The view model can simply set the property and the data-binding will force the action to happen. Personally, I think this is a fine approach but I don't see the need for my view model to have to understand that part of the UI concern is setting a focus. I wanted to approach it from a UI-centric view and only involve the view model if needed.

It turns out the solution is quite simple. All I need to do is create a control to help manage the focus. Call it the "focus helper" and start with a dependency property that points to another control:

public class FocusHelper : Control
{
    public static readonly DependencyProperty TargetElementProperty =
        DependencyProperty.Register(
            "TargetElement",
            typeof (Control),
            typeof (FocusHelper),
            null);
       
    public Control TargetElement
    {
        get { return (Control) GetValue(TargetElementProperty); }
        set { SetValue(TargetElementProperty, value); }            
    }

}

The reason this helper derives from Control is because I want to be able to place it inside a grid. The next thing I'll do is expose a property to set the focus. First, the details of the property itself:

public static readonly DependencyProperty SetFocusProperty =
    DependencyProperty.Register(
        "SetFocus",
        typeof(bool),
        typeof(FocusHelper),
        new PropertyMetadata(false, FocusChanged));

public bool SetFocus
{
    get { return (bool) GetValue(SetFocusProperty); }
    set { SetValue(SetFocusProperty, value);}
}

You'll notice that I set the property metadata to invoke a callback whenever the property changes. In that call, I want to see if I have a control set. If I do, then I'll simply call the Focus method for that control, then reset the flag back to false so it can fire again if it is set in the future;

private static void FocusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var targetElement = d.GetValue(TargetElementProperty) as Control;
    if (targetElement == null || e.NewValue == null || (!((bool) e.NewValue)))
    {
        return;
    }
    targetElement.Focus();
    d.SetValue(SetFocusProperty, false);
}

Now I've got a handy control that will fire the focus method on a target element whenever its own SetFocus property is set to true. It's easy to wire it to whatever input control I want, in this case a TextBox called "Title":

<Behaviors:FocusHelper x:Name="FocusHelper" 
       TargetElement="{Binding ElementName=Title}"/>
<TextBox x:Name="Title" Text="{Binding Title,Mode=TwoWay}"/>

Doing the binding this way also makes it Blend-friendly. If the focused element needs to change, the designer will be able to click on the binding and target the property all through the designer without touching the Xaml. The last step is to update the visual state. When the control is shown, the visibility is set to "Visible." I simply add another entry to the storyboard that sets the focus property to "true":

<VisualState x:Name="New">
    <Storyboard>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" 
                     Storyboard.TargetProperty="(UIElement.Visibility)">
            <DiscreteObjectKeyFrame KeyTime="0:0:0">
                <DiscreteObjectKeyFrame.Value>
                    <Visibility>Visible</Visibility>
                </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
        </ObjectAnimationUsingKeyFrames>
            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusHelper" 
                      Storyboard.TargetProperty="(FocusHelper.SetFocus)">
                <DiscreteObjectKeyFrame KeyTime="0:0:0">
                    <DiscreteObjectKeyFrame.Value>
                        <System:Boolean>true</System:Boolean>
                    </DiscreteObjectKeyFrame.Value>
                </DiscreteObjectKeyFrame>
            </ObjectAnimationUsingKeyFrames>
        </Storyboard>
</VisualState>

And that's it ... now any time I set the control to a visual state that makes it appear, the focus is set to the input control of my choice and the user is able to start typing or tabbing between fields right away. To learn more about the application, order your copy of Designing Silverlight Business Applications and read how it was constructed with access to the full source code.

Jeremy Likness