A long time ago, I asked a similar question: Scrollable TextBox in WP7 (ala Skype and Facebook) —I want the same behavior on Windows Phone 8.1.
I have a TextBox where the user can type a note, and when the keyboard comes up, it moves the TextBox up so it's always in view. The problem is that if the note is too large, the user is unable to scroll the whole note easily.
Instead of moving the TextBox up, I'd like to resize the page so other elements (like app title) are always visible, too. And obviously, the TextBox should scroll easily even if the note is large.
This is my XAML:
<Page
x:Class="ScrollableTextBox.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ScrollableTextBox"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<!--LayoutRoot-->
<Grid x:Name="LayoutRoot"
Margin="21,-6.5,19,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--Title-->
<TextBlock Margin="0,19,0,24"
Style="{ThemeResource TitleTextBlockStyle}"
Text="APP TITLE" />
<!--ContentPanel-->
<Grid Grid.Row="1">
<ScrollViewer x:Name="NoteContentScrollViewer">
<TextBox x:Name="NoteContentTextBox"
AcceptsReturn="True"
ScrollViewer.VerticalScrollMode="Disabled"
VerticalAlignment="Stretch"
GotFocus="NoteContentTextBox_GotFocus" />
</ScrollViewer>
</Grid>
</Grid>
And this is the code-behind:
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
namespace ScrollableTextBox
{
public sealed partial class MainPage : Page
{
// Handle InputPane manually so the UI doesn't scroll when the keyboard appears
InputPane inputPane = InputPane.GetForCurrentView();
public MainPage()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
}
private void NoteContentTextBox_GotFocus(object sender, RoutedEventArgs e)
{
// Subscribe InputPane events to handle UI scrolling
inputPane.Showing += this.InputPaneShowing;
inputPane.Hiding += this.InputPaneHiding;
}
private void InputPaneShowing(InputPane sender, InputPaneVisibilityEventArgs e)
{
// Set EnsuredFocusedElementInView to true so the UI doesn't scroll
e.EnsuredFocusedElementInView = true;
// Set new margins to LayoutRoot (to compensate keyboard)
LayoutRoot.Margin = new Thickness(21, -6.5, 19, e.OccludedRect.Height);
// Unsubscribe InputPane Showing event
inputPane.Showing -= this.InputPaneShowing;
}
private void InputPaneHiding(InputPane sender, InputPaneVisibilityEventArgs e)
{
// Set EnsuredFocusedElementInView to false so the UI scrolls
e.EnsuredFocusedElementInView = false;
// Reset LayoutRoot margins
LayoutRoot.Margin = new Thickness(21, -6.5, 19, 0);
// Unsubscribe InputPane Hiding event to handle UI scrolling
inputPane.Hiding -= this.InputPaneHiding;
}
}
}
This works beautifully because the page is resized when the keyboard comes up, the user is able to scroll easily while editing the note, and other UI elements aren't moved out of view. However, there's one behavior missing: when the user taps the TextBox, it should scroll to the caret position, but right now it doesn't scroll at all (as we'd expect).
On Windows Phone 7, I used ScrollViewer.ScrollToVerticalOffset() to achieve this, but it doesn't work on WinRT. We should supposedly use ScrollViewer.ChangeView(), but I'm unable to make it work.
So, in short, I'd like the TextBox to scroll to the caret position when the user taps it, so he can start typing right away instead of having to scroll manually (or hitting the Enter key to get to the position). Any ideas?
I was able to solve the problem by using a DispatcherTimer, as Bryan Stump suggested. Here's the missing code:
// DispatcherTimer to ChangeView() of NoteContentScrollViewer
DispatcherTimer keyboardTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
Inside MainPage():
// Subscribe keyboardTimer Tick event
keyboardTimer.Tick += keyboardTimer_Tick;
Inside InputPaneShowing():
// Start() keyboardTimer to scroll to caret
keyboardTimer.Start();
And finally, the keyboardTimer Tick event:
private void keyboardTimer_Tick(object sender, object e)
{
// Stop timer so it doesn't repeat
keyboardTimer.Stop();
// Invoke ChangeView() on NoteContentScrollViewer, and use GetRectFromCharacterIndex to scroll to caret position
if (NoteContentTextBox.Text != "")
NoteContentScrollViewer.ChangeView(0, NoteContentTextBox.GetRectFromCharacterIndex(NoteContentTextBox.SelectionStart - 1, true).Y, null);
}
They key is the GetRectFromCharacterIndex method of TextBox to locate the position of the caret. This always ensures the caret is in view, at least on my testing.