Reputation: 79
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?
Upvotes: 2
Views: 2432
Reputation: 12653
XAML:
<TextBox
AcceptsReturn="True"
Name="textBox"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollMode="Auto"
TextChanged="TextBox_TextChanged"
/>
C#:
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
var grid = (Grid)VisualTreeHelper.GetChild(textBox, 0);
for (var i = 0; i <= VisualTreeHelper.GetChildrenCount(grid) - 1; i++)
{
object obj = VisualTreeHelper.GetChild(grid, i);
if (!(obj is ScrollViewer)) continue;
((ScrollViewer)obj).ChangeView(null, ((ScrollViewer)obj).ExtentHeight, null);
break;
}
}
Upvotes: 0
Reputation: 79
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.
Upvotes: 0
Reputation: 1439
Scroll viewer and change view aren't working? I asked a post about something related on MSDN such I can link later (I'm on mobile atm). To explain the reason the scroll viewer did not work would require a deep dive into how dependency properties like vertical offset are prioritized in the UI. Any animation will overwrite a value set in the code, and the input pane is opening when you are trying to set the new height. Change view must be called after the input pane animation completes. Try setting a dispatcher timer for a half second interval and calling change view in the tick event handler. This effectively waits for the UI animation to end so your new value will be set correctly.
Have you tried using the textbox.select method? There could be a built in mechanic to focus the selected position.
Upvotes: 1