Wait for the control layout to end

I am loading quite a lot of rich text into a RichTextBox (WPF) and I want to scroll to the end of the content:

 richTextBox.Document.Blocks.Add(...) richTextBox.UpdateLayout(); richTextBox.ScrollToEnd(); 

This does not work, ScrollToEnd is executed when the layout is not finished yet, so it does not scroll to the end, it scrolls around the first third of the text.

Is there a way to make wait for RichTextBox to complete the drawing and layout operations so that ScrollToEnd really scrolls to the end of the text?

Thanks.

Material that does not work:

EDIT : I tried the LayoutUpdated event, but it fired immediately, the same problem: the control still selects more text inside the richtextbox when it fires, even if ScrollToEnd doesn't work there ... I tried this:

 richTextBox.Document.Blocks.Add(...) richTextBoxLayoutChanged = true; richTextBox.UpdateLayout(); richTextBox.ScrollToEnd(); 

and inside the richTextBox.LayoutUpdated event richTextBox.LayoutUpdated :

 if (richTextBoxLayoutChanged) { richTextBoxLayoutChanged = false; richTextBox.ScrollToEnd(); } 

The event fires correctly, but too early, richtextbox still adds more text when it ScrollToEnd , the layout is not completed, so ScrollToEnd does not work again.

EDIT 2 : Following the dowhile for the answer: MSDN on InvalidateArrange says

After the invalidation, the element will update its layout, which will occur asynchronously, unless it was forcedly called UpdateLayout.

But even

 richTextBox.InvalidateArrange(); richTextBox.InvalidateMeasure(); richTextBox.UpdateLayout(); 

MUST NOT: after these calls, richtextbox still adds more text and sets it inside itself asynchronously. ARG!

+7
source share
7 answers

Check out UpdateLayout

special

The call to this method does not work if the layout has not changed, or if no layout or measurement status of the layout is invalid.

So calling InvalidateMeasure or InvalidateArrange, depending on your needs, should work.

But considering your piece of code. I think this will not work. The WPF download and creation utility shares, so adding something to Document.Blocks does not require changing the user interface directly. But I have to say, this is just a hunch, and maybe I'm wrong.

+2
source

I had a related situation: I have a preview dialog that creates a fantastic render. Usually the user presses a button for actual printing, but I also wanted to use it to save the image without user intervention. In this case, the creation of the image should wait for the completion of the layout.

I managed to use the following:

 Dispatcher.Invoke(new Action(() => {SaveDocumentAsImage(....);}), DispatcherPriority.ContextIdle); 

The DispatcherPriority.ContextIdle key that waits for the completion of background tasks.

Edit: as per Zach's request, including the code applicable for this particular case:

 Dispatcher.Invoke(() => { richTextBox.ScrollToEnd(); }), DispatcherPriority.ContextIdle); 

I should note that I am not very happy with this decision because it feels incredibly fragile. However, it seems to work in my specific case.

+7
source

you can use the loaded event

If you do this more than once, you should see the LayoutUpdated event

 myRichTextBox.LayoutUpdated += (source,args)=> ((RichTextBox)source).ScrollToEnd(); 
+1
source

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

+1
source

With .net 4.5 or 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; } 
+1
source

Try the following:

 richTextBox.CaretPosition = richTextBox.Document.ContentEnd; richTextBox.ScrollToEnd(); // maybe not necessary 
0
source

@Andreas answer works well.

However, what if the control is already loaded? The event never fires, and the wait would potentially freeze 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 Tips

These tips may or may not be helpful.

  • The code above is really useful in an attached property. The bound property is only triggered when the value changes. When switching a connected property to call it, use task.Yield() to place the call in 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 switching a connected property to start it, you can use the dispatcher and set the Loaded priority:

     // 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); 
0
source

All Articles