I've got a Windows Store app with a RichEditBox
(editor) and a Grid
(MarginNotes).
I need the vertical scroll position of the two elements to be matched at all times. The purpose of this is to allow the user to add notes in the margin of the document.
I've already figured out Note positioning based on the cursor position - when a note is added, a text selection is made of everything up to the cursor. that selection is then added to a second, invisible RichEditBox
, inside a StackPanel
. I then get the ActualHeight
of this control which gives me the position of the note in the grid.
My issue is that when I scroll the RichEditBox
up and down, the Grid
does not scroll accordingly.
First Technique
I tried putting them both inside a ScrollViewer
, and disabling scrolling on the RichEditBox
<ScrollViewer x:Name="EditorScroller"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="{Binding *" />
<ColumnDefinition Width="150" />
</Grid.ColumnDefinitions>
<Grid x:Name="MarginNotes" Grid.Column="0" HorizontalAlignment="Right"
Height="{Binding ActualHeight, ElementName=editor}">
</Grid>
<StackPanel Grid.Column="1">
<RichEditBox x:Name="margin_helper" Opacity="0" Height="Auto"></RichEditBox>
</StackPanel>
<RichEditBox x:Name="editor" Grid.Column="1" Height="Auto"
ScrollViewer.VerticalScrollBarVisibility="Hidden" />
</Grid>
</ScrollViewer>
When I scroll to the bottom of the RichEditBox
control, and hit enter a few times, the cursor drops out of sight. The ScrollViewer
doesn't scroll automatically with the cursor.
I tried adding C# code which would check the position of the cursor, compare it to the VerticalOffset
and height of the editor, and then adjust the scroll accordingly. This worked, but was incredibly slow. Initially I had it on the KeyUp
event which brought the app to a standstill when I typed a sentence. Afterwards I put it on a 5 second timer, but this still slowed down the app performance and also meant that there could be a 5 second delay between the cursor dropping out of sight and the RichEditBox
scrolling.
Second Technique
I also tried putting just MarginNotes
in its own ScrollViewer
, and programmatically setting the VerticalOffset
based off my RichEditBox
s ViewChanged
event.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="{Binding *" />
<ColumnDefinition Width="150" />
</Grid.ColumnDefinitions>
<ScrollViewer x:Name="MarginScroller" Grid.Column="0"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<Grid x:Name="MarginNotes" HorizontalAlignment="Right"
Height="{Binding ActualHeight, ElementName=editor}">
</Grid>
</ScrollViewer>
<StackPanel Grid.Column="1">
<RichEditBox x:Name="margin_helper" Opacity="0" Height="Auto"></RichEditBox>
</StackPanel>
<RichEditBox x:Name="editor" Grid.Column="1" Height="Auto"
Loaded="editor_loaded" SizeChanged="editor_SizeChanged" />
</Grid>
relevant event handlers
void editor_Loaded(object sender, RoutedEventArgs e)
{
// setting this in the OnNavigatedTo causes a crash, has to be set here.
// this uses WinRTXAMLToolkit as suggested by Nate Diamond to find the
// ScrollViewer and add the event handler
editor.GetFirstDescendantOfType<ScrollViewer>().ViewChanged += editor_ViewChanged;
}
private void editor_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
// when the RichEditBox scrolls, scroll the MarginScroller the same amount
double editor_vertical_offset = ((ScrollViewer)sender).VerticalOffset;
MarginScroller.ChangeView(0, editor_vertical_offset, 1);
}
private void editor_SizeChanged(object sender, SizeChangedEventArgs e)
{
// when the RichEditBox size changes, change the size of MarginNotes to match
string text = "";
editor.Document.GetText(TextGetOptions.None, out text);
margin_helper.Document.SetText(TextSetOptions.None, text);
MarginNotes.Height = margin_helper.ActualHeight;
}
This worked, but was quite laggy as scrolling is not applied until the ViewChanged
event fires, after scrolling has stopped. I tried using the ViewChanging
event, but it does not fire at all for some reason. Additionally, the Grid
was sometimes mis-positioned after a fast scroll.
So, what makes this difficult is that the size of the text or the placement of the text in different types of TextBoxes means that syncing the scrollbar doesn't guarantee you are syncing the text. Having said that, here's how you do it.
void MainPage_Loaded(object sender, RoutedEventArgs args)
{
MyRichEditBox.Document.SetText(Windows.UI.Text.TextSetOptions.None, MyTextBox.Text);
var textboxScroll = Children(MyTextBox).First(x => x is ScrollViewer) as ScrollViewer;
textboxScroll.ViewChanged += (s, e) => Sync(MyTextBox, MyRichEditBox);
}
public void Sync(TextBox textbox, RichEditBox richbox)
{
var textboxScroll = Children(textbox).First(x => x is ScrollViewer) as ScrollViewer;
var richboxScroll = Children(richbox).First(x => x is ScrollViewer) as ScrollViewer;
richboxScroll.ChangeView(null, textboxScroll.VerticalOffset, null);
}
public static IEnumerable<FrameworkElement> Children(FrameworkElement element)
{
Func<DependencyObject, List<FrameworkElement>> recurseChildren = null;
recurseChildren = (parent) =>
{
var list = new List<FrameworkElement>();
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is FrameworkElement)
list.Add(child as FrameworkElement);
list.AddRange(recurseChildren(child));
}
return list;
};
var children = recurseChildren(element);
return children;
}
Deciding when to invoke the sync is tricky. Maybe on PointerReleased, PointerExit, LostFocus, KeyUp - there are a lot of ways to scroll is the real issue there. You might need to handle all of those. But, it is what it is. At least you can.
Best of luck.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With