Detect FlowDocument Change and Scroll
Asked Answered
T

4

6

I want to detect (preferably through an event) when any content is added, changed, etc. in a FlowDocument and when it does I want to cause a FlowDocumentScrollViewer displaying the FlowDocument to automatically scroll to the end.

Temporal answered 31/10, 2009 at 18:36 Comment(0)
D
10

You can detect changes in the FlowDocument by creating a text range and monitoring it for changes. Scrolling to the bottom is more difficult because you have to find the ScrollViewer. Also for performance you don't want redo all the scrolling calculations on every change, so you should use DispatcherOperations.

Putting it all together, this code should do the trick:

var range = new TextRange(flowDocument.ContentStart, flowDocument.ContentEnd);
object operation = null;

range.Changed += (obj, e) =>
{
  if(operation==null)
    operation = Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() =>
    {
      operation = null;

      var scrollViewer = FindFirstVisualDescendantOfType<ScrollViewer>(flowDocument);
      scrollViewer.ScrollToBottom();
    });
};

where FindFirstVisualDescendantOfType is a simple depth-first prefix search of the visual tree using VisualTreeHelper.GetChildrenCount() and VisualTreeHelper.GetChild() and returning the first Visual found of the specified type.

Note that for full generality I don't precompute scrollViewer at the top of the code because the FlowDocumentScrollViewer's template can change. If this won't happen, this code can be speeded up by calling .ApplyTemplate() on the FlowDocumentScrollViewer and then computing scrollViewer before the event handler is registered:

var range = new TextRange(flowDocument.ContentStart, flowDocument.ContentEnd);
object operation = null;

flowDocument.ApplyTemplate();
var scrollViewer = FindFirstVisualDescendantOfType<ScrollViewer>(flowDocument);

range.Changed += (obj, e) =>
{
  if(operation==null)
    operation = Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() =>
    {
      operation = null;
      scrollViewer.ScrollToBottom();
    });
};

Note that we cannot simply call scrollViewer.GetTemplateChild("PART_ContentHost") and skip the visual tree search because GetTemplateChild is protected.

Divergent answered 19/11, 2009 at 0:48 Comment(1)
I did not need any of this with FlowDocument and FlowDocumentScrollViewer visual tree code. Just 2 Invokes, 1 to create and add the paragraph from a string and 1 to bring that paragraph into view.Polyp
Y
2

Are you using a RichTextBox to do the editing? If so you should just be able to hook the TextChanged event and then call the ScrollToVerticalOffset method with the value from the ViewportHeight property.

Year answered 1/11, 2009 at 16:25 Comment(1)
No. Using FlowDocumentScrollViewer, not RichTextBox.Temporal
S
2

After hooking up to a TextChanged Event, you can simply use:

// Showing Last Block
YourReader.Document.Blocks.LastBlock.BringIntoView();

// Or.. showing the last Inline
(YourReader.Document.Blocks.LastBlock as Paragraph).Inlines.LastInline.BringIntoView();

But, this works only on a FlowDocumentPageViewer, and also on a FlowDocumentReader (with pages ViewingModes), for a FlowDocumentScrollViewer you should use the visual tree as mentioned

public static ScrollViewer FindScroll(Visual visual)
        {
            if (visual is ScrollViewer)
                return visual as ScrollViewer;

            ScrollViewer searchChiled = null;
            DependencyObject chiled;

            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
            {
                chiled = VisualTreeHelper.GetChild(visual, i);
                if (chiled is Visual)
                    searchChiled = FindScroll(chiled as Visual);
                if (searchChiled != null)
                    return searchChiled;
            }

            return null;
        }

ScrollViewer scroller = FindScroll(YourReader as Visual);
if (scroller != null) 
   (scroller as ScrollViewer).ScrollToBottom();
Singe answered 23/11, 2012 at 10:49 Comment(1)
I am using only a FlowDocument and FlowDocumentscrollViewer. The BringIntoView solution above works perfectly for me. I did not need VisualTreeHelper My solution is to use Dispatcher.Invoke to write create the paragraph, store that paragraph as a variable and then Dispatcher.Invoke again to bring it into view. This seems the simplest method.Polyp
N
0

You can use the following extension method to get the inner scroll viewer:

public static class FlowDocumentScrollViewerExtensions
{
  public static ScrollViewer GetScrollViewer(this FlowDocumentScrollViewer element) {
    if (element == null) {
      throw new ArgumentNullException(nameof(element));
    }

    return element.Template?.FindName("PART_ContentHost", element) as ScrollViewer;
  }
}

Additionally you can use these extension methods before adding content to check the scroll position the scrollviewer itself (in case you want to scroll -only- if the scroll viewer was already at the end for example):

public static class ScrollViewerExtensions
{
  public static bool IsAtHome(this ScrollViewer element) {
    if (element == null) {
      throw new ArgumentNullException(nameof(element));
    }

    return element.VerticalOffset <= 0;
  }

  public static bool IsAtEnd(this ScrollViewer element) {
    if (element == null) {
      throw new ArgumentNullException(nameof(element));
    }

    return element.VerticalOffset >= element.ScrollableHeight;
  }
}

Afterwards just call scrollViewer.ScrollToEnd() for example.

Nikkinikkie answered 24/1, 2016 at 0:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.