I am loading quite a lot of rich text into a RichTextBox
(WPF) and I want to scroll to the end of content:
richTextBox.Document.Blocks.Add(...)
richTextBox.UpdateLayout();
richTextBox.ScrollToEnd();
This doesn't work, ScrollToEnd
is executed when the layout is not finished, so it doesn't scroll to the end, it scrolls to around the first third of the text.
Is there a way to force a wait until the RichTextBox
has finished its painting and layout operations so that ScrollToEnd
actually scrolls to the end of the text?
Thanks.
Stuff that doesn't work:
EDIT:
I have tried the LayoutUpdated
event but it's fired immediately, same problem: the control is still laying out more text inside the richtextbox when it's fired so even a ScrollToEnd
there doesn't work...
I tried this:
richTextBox.Document.Blocks.Add(...)
richTextBoxLayoutChanged = true;
richTextBox.UpdateLayout();
richTextBox.ScrollToEnd();
and inside the richTextBox.LayoutUpdated
event handler:
if (richTextBoxLayoutChanged)
{
richTextBoxLayoutChanged = false;
richTextBox.ScrollToEnd();
}
The event is fired correctly but too soon, the richtextbox is still adding more text when it's fired, layout is not finished so ScrollToEnd
fails again.
EDIT 2: Following on dowhilefor's answer: MSDN on InvalidateArrange says
After the invalidation, the element will have its layout updated, which will occur asynchronously unless subsequently forced by UpdateLayout.
Yet even
richTextBox.InvalidateArrange();
richTextBox.InvalidateMeasure();
richTextBox.UpdateLayout();
does NOT wait: after these calls the richtextbox is still adding more text and laying it out inside itself asynchronously. ARG!
Have a look at UpdateLayout
especially:
Calling this method has no effect if layout is unchanged, or if neither arrangement nor measurement state of a layout is invalid
So calling InvalidateMeasure or InvalidateArrange, depending on your needs should work.
But considering your piece of code. I think that won't work. Alot of WPF loading and creating is deffered, so adding something to Document.Blocks does not necesarilly change the UI directly. But i must say, this is just a guess and maybe i'm wrong.
I have had a related situation: I have a print preview dialog that creates a fancy rendering. Normally, the user will click a button to actually print it, but I also wanted to use it to save an image without user involvement. In this case, creating the image has to wait until the layout is complete.
I managed that using the following:
Dispatcher.Invoke(new Action(() => {SaveDocumentAsImage(....);}), DispatcherPriority.ContextIdle);
The key is the DispatcherPriority.ContextIdle
, which waits until background tasks have completed.
Edit: As per Zach's request, including the code applicable for this specific case:
Dispatcher.Invoke(() => { richTextBox.ScrollToEnd(); }), DispatcherPriority.ContextIdle);
I should note that I'm not really happy with this solution, as it feels incredibly fragile. However, it does seem to work in my specific case.
The answer by @Andreas works well.
However, what if the control is already loaded? The event would never fire, and the wait would potentially hang forever. To fix this, return immediately if the form is already loaded:
/// <summary>
/// Intent: Wait until control is loaded.
/// </summary>
public static Task WaitForLoaded(this FrameworkElement element)
{
var tcs = new TaskCompletionSource<object>();
RoutedEventHandler handler = null;
handler = (s, e) =>
{
element.Loaded -= handler;
tcs.SetResult(null);
};
element.Loaded += handler;
if (element.IsLoaded == true)
{
element.Loaded -= handler;
tcs.SetResult(null);
}
return tcs.Task;
}
These hints may or may not be useful.
The code above is really useful in an attached property. An attached property only triggers if the value changes. When toggling the attached property to trigger it, use task.Yield()
to put the call to the back of the dispatcher queue:
await Task.Yield(); // Put ourselves to the back of the dispatcher queue.
PopWindowToForegroundNow = false;
await Task.Yield(); // Put ourselves to the back of the dispatcher queue.
PopWindowToForegroundNow = false;
The code above is really useful in an attached property. When toggling the attached property to trigger it, you can use the dispatcher, and set the priority to Loaded
:
// Ensure PopWindowToForegroundNow is initialized to true
// (attached properties only trigger when the value changes).
Application.Current.Dispatcher.Invoke(
async
() =>
{
if (PopWindowToForegroundNow == false)
{
// Already visible!
}
else
{
await Task.Yield(); // Put ourselves to the back of the dispatcher queue.
PopWindowToForegroundNow = false;
}
}, DispatcherPriority.Loaded);
you should be able to use the Loaded event
if you are doing this more then one time, then you should look at the LayoutUpdated event
myRichTextBox.LayoutUpdated += (source,args)=> ((RichTextBox)source).ScrollToEnd();
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