Change Detection and Scrolling FlowDocument

I want to detect (preferably through an event) when any content is added, changed, etc. in FlowDocument , and when that happens, I want the FlowDocumentScrollViewer display the FlowDocument to automatically scroll the end.

+5
scroll wpf flowdocument
source share
4 answers

You can detect changes in the FlowDocument by creating a text range and checking it for changes. Scrolling to the bottom is harder because you need to find the ScrollViewer . Also, for performance, you donโ€™t need to redo all scrolls with every change, so you should use DispatcherOperations .

Combining all this, 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 preliminary depth search in the visual tree using VisualTreeHelper.GetChildrenCount() and VisualTreeHelper.GetChild() and returning the first visual found type.

Note that for complete generality, I will not precompute scrollViewer at the top of the code, because the FlowDocumentScrollViewer template may change. If this does not happen, this code can be accelerated by calling .ApplyTemplate() on the FlowDocumentScrollViewer , and then ScrollViewer before registering the event handler:

 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 just call scrollViewer.GetTemplateChild("PART_ContentHost") and skip the visual tree search because GetTemplateChild is protected.

+7
source share

Do you use RichTextBox for editing? If so, you should simply hook up the TextChanged event , and then call the ScrollToVerticalOffset method with the value of the ViewportHeight property .

+2
source share

After connecting to the 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 only works in the FlowDocumentPageViewer, and also on the FlowDocumentReader (with ViewModes pages), for the FlowDocumentScrollViewer you should use the visual tree as indicated

 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(); 
+2
source share

You can use the following extension method to get an internal scroll view:

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

In addition, you can use these extension methods before adding content to check the scroll position of 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; } } 

Then just call scrollViewer.ScrollToEnd (), for example.

0
source share

All Articles