10

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!

Contango
  • 65,385
  • 53
  • 229
  • 279
SemMike
  • 1,363
  • 1
  • 12
  • 23

8 Answers8

9

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.

Patrick Simpson
  • 504
  • 4
  • 9
  • Patrick, this is a great solution. I found this work work best among all solutions given here. `Dispatcher.Invoke(() => { richTextBox.ScrollToEnd(); }), DispatcherPriority.ContextIdle);` Could you please update your answer to include this code so it's better seen? – Zach Apr 22 '15 at 12:09
  • Thank you for this answer! That also helped me in a case where I was getting `PropertyChanged` events of a custom control, originating from custom dependency properties which were invoked after the dependency properties of standard controls. Not sure if I made a mistake somewhere in implementing them, or if it has to do with the bindings. Anyways, `Dispatcher.Invoke` without specifying a priority would always execute BEFORE those `PropertyChanged` events of my custom dependency properties. Using `DispatcherPriority.ContextIdle`, all of a sudden, the dispatcher delegate is executed AFTER them. – j00hi Dec 09 '17 at 00:52
  • Using Dispatcher.Invoke with DispatcherPriority.ContextIdle causes a visual delay after controils are loaded before the additional action is applied. Instead using await Task.Yield() doesn't cause such visual delay. – Etienne Charland Jul 11 '18 at 21:01
3

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.

dowhilefor
  • 10,646
  • 3
  • 25
  • 43
  • Thanks, it doesn't work but it was worth a shot. Even calling both InvalidateArrange and InvalidateMeasure before UpdateLayout doesn't solve the problem, the richtextbox is still adding/layout out the text asynchronously after all these calls. It doesn't wait. – SemMike Jul 07 '11 at 18:02
1

With .net 4.5 or the async blc package you can use the following extension method

 /// <summary>
    /// Async Wait for a Uielement to be loaded
    /// </summary>
    /// <param name="element"></param>
    /// <returns></returns>
    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;
        return tcs.Task;
    }
Andreas
  • 3,275
  • 2
  • 34
  • 51
1

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;
}

Additional hints

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);
    
Contango
  • 65,385
  • 53
  • 229
  • 279
1

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();
Muad'Dib
  • 26,680
  • 5
  • 52
  • 68
  • same remark: thanks but I generate that long text many times during runtime, not just when the richtextbox is loaded (the text is generated from database queries)... – SemMike Jul 07 '11 at 17:44
  • @SemMike ahhhh, you left THAT little bit of information out :) – Muad'Dib Jul 07 '11 at 17:58
  • thanks, but I already tried the UpdateLayout event (following the Loaded suggestions), see my first edit... – SemMike Jul 07 '11 at 18:12
0

Try this:

richTextBox.CaretPosition = richTextBox.Document.ContentEnd;
richTextBox.ScrollToEnd(); // maybe not necessary
Pollitzer
  • 1,456
  • 2
  • 16
  • 26
0

The only (kludge) solution that worked for my WPF project was to fire off a separate thread that slept for a time and then asked to scroll to the end.

It is important to not try to invoke this "sleepy" thread on the main GUI, lest the user would be paused. Therefore, invoke a separate "sleepy" thread and periodically Dispatcher.Invoke on the main GUI thread and ask to scroll to the end.

Works perfectly and the user experience is not terrible:

using System;
using System.Threading;    
using System.Windows.Controls;

try {

    richTextBox.ScrollToEnd();

    Thread thread       = new Thread(new ThreadStart(ScrollToEndThread));
    thread.IsBackground = true;
    thread.Start();

} catch (Exception e) {

    Logger.Log(e.ToString());
}

and

private void ScrollToEndThread() {

// Using this thread to invoke scroll to bottoms on rtb
// rtb was loading for longer than 1 second sometimes, so need to 
// wait a little, then scroll to end
// There was no discernable function, ContextIdle, etc. that would wait
// for all the text to load then scroll to bottom
// Without this, on target machine, it would scroll to the bottom but the
// text was still loading, resulting in it scrolling only part of the way
// on really long text.
// Src: https://stackoverflow.com/questions/6614718/wait-until-control-layout-is-finished
    for (int i=1000; i <= 10000; i += 3000) {

        System.Threading.Thread.Sleep(i);

        this.richTextBox.Dispatcher.Invoke(
            new ScrollToEndDelegate(ScrollToEnd),
            System.Windows.Threading.DispatcherPriority.ContextIdle,
            new object[] {  }
            );
    }
}

private delegate void ScrollToEndDelegate();

private void ScrollToEnd() {

    richTextBox.ScrollToEnd();
}
Mike
  • 141
  • 2
  • 5
0

Try adding your richTextBox.ScrollToEnd(); call to the LayoutUpdated event handler of your RichTextBox object.

Philipp Schmid
  • 5,636
  • 5
  • 41
  • 61
  • 1
    Thats misleading. And i think alot of people still think that this event is only called, when the layout of the control is changed, where the listener was added. But thats not the case. LayoutUpdated is called when ANY element changes its layout. Have a look [here](http://msdn.microsoft.com/en-us/library/system.windows.uielement.layoutupdated.aspx) – dowhilefor Jul 07 '11 at 18:05