FlipView and Problems with Input Controls – Part 1

2015-12-06 at 17:18 2 comments

Check out this GitHub repo to find the source code for this post. The problem described here applies to Windows 8 / 8.1 store apps, too, not only to UWP apps.

In the Universal Windows Platform, you can use a control called FlipView to present a collection of items to the user. These items are shown one at a time and the user can perform swipe gestures (a.k.a flips) to navigate to the next or previous one in the sequence. While the FlipView is generally designed to display content that can only be viewed by the user (like e.g. an image gallery), I decided to use it as part of a forms layout where logically related input controls are put on the same page, and when the user is finished with them, he or she can swipe to the next page to fill out its form items. However, there are some issues with this approach when the user is using the touch keyboard that comes with Windows.

The touch keyboard potentially covers input controls if they are placed within a FlipView

The FlipView works perfectly fine when it is used to e.g. display images that the user can swipe through, but with my forms design, I faced the following issue: when a user touches e.g. a text box, then the touch keyboard should fade in. As it covers the bottom half of the screen, the selected text box should be scrolled to the top half if necessary so that the user can see the characters he or she types on the touch keyboard. But unfortunately this behavior is not performed when your input controls reside in a FlipView, as you can see in the the following screenshots:

Text Box 8 is about to be touched - you'd expect the touch keyboard to be faded in and the text box to be positioned (scrolled) to the top half of the screen.
Text Box 8 is about to be touched – you’d expect the touch keyboard to be faded in and the text box to be positioned (scrolled) to the top half of the screen…
...but unfortunately the text box is not moved and thus the keyboard covers the input control.
…but unfortunately the text box is not moved and thus the keyboard covers the input control.

Of course, this is a no-go for usability, but it took me actually some time to find out how this can be fixed.

The VirtualizingStackPanel is causing the problem

I won’t go into details how I figured this out – in fact it took me some days. I tried to re-implement the FlipView and getting in touch with all the components the FlipView is made of, I finally realized that the VirtualizingStackPanel that is used as the ItemsPanel is causing this problem. If you exchange it with a normal StackPanel, then the text box is just positioned correctly. Your XAML could look like this to change the ItemsPanel template:

<FlipView>
    <FlipView.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal" AreScrollSnapPointsRegular="True" />
        </ItemsPanelTemplate>
    </FlipView.ItemsPanel>
</FlipView>

Of course, you can also put these definitions in a style, if you want to. When in place, the text box is correctly moved to the upper half of the screen when the touch keyboard fades in:

With a StackPanel instead of a VirtualizingStackPanel in place, the text box is moved correctly.
With a StackPanel instead of a VirtualizingStackPanel in place, the text box is moved correctly.

Now I don’t have the slightest idea why the VirtualizingStackPanel is causing issues here – if you do, please let me know by leaving a comment. Anyways, using a StackPanel instead of a VirtualizingStackPanel comes at a cost.

UI Virtualization is now disabled for the FlipView

As the name is probably telling you, the main purpose of a VirtualizingStackPanel is to provide UI Virtualization, i.e. items that are currently not on screen are not rendered. This usually allows Items Controls to be loaded faster and act more fluently, especially when it holds several hundred or even thousands of items.

The StackPanel in turn does not virtualize the UI Elements it arranges: all of them will be included in the UI Render Process. This has two effects: you’ll use up more memory and the load time of the FlipView will be significantly higher.

While it is not so easy to deal with the former (I would advise you to perform data virtualization in this case), there is a relatively easy method to reduce the load time of the FlipView if you experience responsiveness problems: you can progressively add items to the FlipView after it was loaded.

The idea is the following: as the FlipView is only presenting one item at a time to the user, initialize it with just the one item it needs. Then add another item on every Dispatcher loop run if there’s time for it. Until the user decides to swipe to the next item for the first time, probably all of them (or at least enough of them) will be loaded. This can be easily done with the CoreDispatcher.RuneIdleAsync method that enqueues a delegate to be run by the dispatcher when the application is idle – this way you ensure that adding the items does not harm the responsiveness of your app. Enqueuing might look like this in C#:

public partial class MyView : UserControl
{
    private IList<UserControl> _subViewsToBeAdded;
    private readonly CoreDispatcher _coreDispatcher;

    public MyView(IList<UserControl> subViewsToBeAdded, CoreDispatcher dispatcher)
        : this()
    {
        if (subViewsToBeAdded == null) 
            throw new ArgumentNullException(nameof(subViewsToBeAdded));
        if (dispatcher == null) 
            throw new ArgumentNullException(nameof(dispatcher));

        _subViewsToBeAdded = subViewsToBeAdded;
        _dispatcher = dispatcher;

        AddNextView(null);
    }

    public MyView() { InitializeComponent(); }

    private void AddNextView(IdleDispatchedHandlerArgs e)
    {
        // If there are no more views to be added, stop enqueuing
        if (_subViewsToBeAdded.Count == 0)
        {
            _subViewsToBeAdded = null;
            return;
        }

        // Else get the next item from the collection, add it to the FlipView...
        var nextView = _subViewsToBeAdded[0];
        _subViewsToBeAdded.RemoveAt(0);
        FlipView.Items.Add(nextView);
        
        // ...and enqueue this method to be executed by the dispatcher in a future loop run
        // (if there's enough time to do that)
        _dispatcher.RunIdleAsync(AddNextView);
    }
}

In the example above, FlipView is expected to be defined in the XAML part of the User Control. Apart from that, nothing special is happening: in the constructor of the class, the passed in list and dispatcher are stored in fields and afterwards the AddNextView method is called. This method checks if another item can be added, if so, it performs that action and enqueues itself asynchronously with the dispatcher to be executed again in another UI Message Loop run. This way we ensure that the UI thread does not block when items are added to the FlipView.

Of course you can customize this code by e.g. doing all this stuff in View Models and have the FlipView create the actual views using a DataTemplateSelector. The important part is that this functionality has to run on the UI thread because of the UI thread affinity of the elements that will be added to the FlipView – you cannot use async await here to run this on the Thread Pool.

Summary

If you use input controls like text boxes within a FlipView, you might experience that the touch keyboard covers the currently focused control. One way to circumvent this problem is to replace the ItemsPanel of the FlipView using a StackPanel. Beware that you loose UI Virtualization in this case, but you can decrease the loading time of the FlipView by progressively adding the items to the FlipView in successive UI Message Loop runs.

There is still one issue left that I didn’t cover in this post: by default, the touch keyboard is not dismissed when the user touches the background of the FlipView, but we will approach that topic in the next post.

Do you want to delve deeper into Items Controls? Then I advise you to read Dr. WPF’s blog posts on “ItemsControl: A to Z” (although it’s written for WPF, it is still relevant for all XAML related technologies).

Wonder how it works? Check out the code at GitHub.

Comments

  1. Interesting problem, sounds like VirtualizingStackPanel could do better here. Can you report that in the Windows Feedback app under Developer Platform, if you haven’t already?

    Another approach to solving this for complex UIs is to handle the keyboard showing event yourself. Just look to the InputPane APIs. The Visibility event lets you know when the keyboard is about to show or hide, and gives you its size and location on the screen. If you set EnsuredFocusedElementInView to true, then the framework won’t try to do anything, and you can adjust your scrolled height and/or position as needed (I made use of this in Tweetium to get improved keyboard interactions over the standard behaviors).

    See the docs for more details:
    https://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.viewmanagement.inputpanevisibilityeventargs.ensuredfocusedelementinview.aspx

    1. Brandon, thank you for your comment. You are correct – one could also solve this problem by handling the InputPane.Showing and InputPane.Hiding events and scrolling the ScrollViewer manually – I just hadn’t put any work into that.

      The idea of reporting via the Windows Feedback app is great – although I had a conversation with Rob Caplan on Stack Overflow before, that’s probably the better way to do it.

Leave a Reply

Your email address will not be published. Required fields are marked *