Programmatically Positioning the Page When Virtual Keyboard Opens
Tapping a textbox in a Windows Store app automatically show the virtual keyboard. If this keyboard would cover the tapped control, it scrolls the page just enough to make it completely visible. Most of the time this behavior works like a charm but there are times when the app would work even better if it could be modified. The best way to do it is of course to programmatically set the vertical offset of the ScrollViewer
which is used when the page is being repositioned automatically.
Let's make it work on an example with a set of textboxes arranged vertically. The page will use an ItemsControl
with a simple item template:
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<ItemsControl ItemsSource="{Binding List}" Margin="120 140 0 0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0 10 0 0">
<TextBox Text="{Binding Text, Mode=TwoWay}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
The DataContext
will be a list of 10 items:
public class Item
{
public string Text { get; set; }
}
public class ViewModel
{
public List<Item> List { get; set; }
}
public MainPage()
{
this.InitializeComponent();
DataContext = new ViewModel
{
List = Enumerable.Range(0, 10)
.Select(i => new Item { Text = i.ToString() })
.ToList()
};
}
That's just enough items that the bottom gets hidden when the virtual keyboard is shown. If at that moment the bottom textbox is focused, the page is scrolled automatically to make it visible. If any of the other textboxes is focused, the keyboard covers the bottom one. The goal is to make all of the text boxes visible, no matter which one is focused. This makes a lot of sense in scenarios when all values are related and the user needs to see them all to be able to fill them in correctly.
The first step is to get a reference to the ScrollViewer
which gets scrolled automatically. It is positioned above the page in the visual tree. Here's the function that can be used to find it:
public static T FindParent<T>(FrameworkElement reference)
where T : FrameworkElement
{
FrameworkElement parent = reference;
while (parent != null)
{
parent = parent.Parent as FrameworkElement;
var rc = parent as T;
if (rc != null)
{
return rc;
}
}
return null;
}
All that's left, is calling the following block of code when any of the textboxes gets focus:
var parentScrollViewer = FindParent<ScrollViewer>(this);
parentScrollViewer.VerticalScrollMode = ScrollMode.Enabled;
// let's skip the offset calculation for now
parentScrollViewer.ScrollToVerticalOffset(offset);
parentScrollViewer.UpdateLayout();
The first impulse would be to call it in the TextBox.GotFocus
event but that's not a good choice for two reasons:
- Virtual keyboard doesn't show every time a textbox gets focus. It must be tapped, not clicked or reached by keyboard. The page shouldn't scroll when there's no virtual keyboard covering it.
- Even when tapped, virtual keyboard shows after the textbox gets focus, i.e. we can't scroll the page in advance since we don't know the size of the keyboard.
A better choice is to scroll the page when the keyboard is shown. Fortunately an event is raised when this happens. To attach a handler to it, the following code should be called in the page constructor:
var inputPane = InputPane.GetForCurrentView();
inputPane.Showing += OnShowingInputPane;
Handler arguments also include info on how much of the screen is covered by the keyboard. This can be used to calculate how much the page must be scrolled - a difference between the bottom position in the page that must remain visible and the top position of the keyboard.
var offset = 580 - args.OccludedRect.Top;
As it turns out, calling the code directly in the event handler still doesn't work. Since this is the Showing
handler and there is no Showed
handler, the keyboard is still not visible when the handler is called which means that the page still doesn't scroll as expected. To make sure the code gets called after the keyboard is shown, it must be run asynchronously through the Dispatcher
which schedules it appropriately. This is the final handler code:
private async void OnShowingInputPane(InputPane sender,
InputPaneVisibilityEventArgs args)
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
var parentScrollViewer = FindParent<ScrollViewer>(this);
parentScrollViewer.VerticalScrollMode = ScrollMode.Enabled;
var offset = 580 - args.OccludedRect.Top;
parentScrollViewer.ScrollToVerticalOffset(offset);
parentScrollViewer.UpdateLayout();
});
}
Of course the handler must be detached when the page is unloaded:
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
var inputPane = InputPane.GetForCurrentView();
inputPane.Showing -= OnShowingInputPane;
}
Don't forget to set the fixed vertical position of the page in above code to the correct value for your page. This one works great for the sample described at the beginning.