How to create attached behavior for automatic scrolling FlowDocumentScrollViewer

My goal is to create a reusable Attached Behavior for the FlowDocumentScrollViewer so that the viewer automatically scrolls to the end whenever the FlowDocument has been updated (added).

Problems so far:

  • OnEnabledChanged receives a call until the visual tree completes and therefore does not find ScrollViewer
  • I do not know how to connect to the DependencyProperty containing the FlowDocument. My plan was to use this modified event to initialize the ManagedRange property. (Manually starts for the first time, if necessary.)
  • I do not know how to get to the ScrollViewer property from the range_Changed method, since it does not have DependencyObject.

I understand that these are potentially 3 separate questions (aka. Questions). However, they depend on each other and the overall design that I tried to make for this behavior. I ask this as the only question if I am wrong. If so, what is the right way?

/// Attached Dependency Properties not shown here: /// bool Enabled /// DependencyProperty DocumentProperty /// TextRange MonitoredRange /// ScrollViewer ScrollViewer public static void OnEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d == null || System.ComponentModel.DesignerProperties.GetIsInDesignMode(d)) return; DependencyProperty documentProperty = null; ScrollViewer scrollViewer = null; if (e.NewValue is bool && (bool)e.NewValue) { // Using reflection so that this will work with similar types. FieldInfo documentFieldInfo = d.GetType().GetFields().FirstOrDefault((m) => m.Name == "DocumentProperty"); documentProperty = documentFieldInfo.GetValue(d) as DependencyProperty; // doesn't work. the visual tree hasn't been built yet scrollViewer = FindScrollViewer(d); } if (documentProperty != d.GetValue(DocumentPropertyProperty) as DependencyProperty) d.SetValue(DocumentPropertyProperty, documentProperty); if (scrollViewer != d.GetValue(ScrollViewerProperty) as ScrollViewer) d.SetValue(ScrollViewerProperty, scrollViewer); } private static ScrollViewer FindScrollViewer(DependencyObject obj) { do { if (VisualTreeHelper.GetChildrenCount(obj) > 0) obj = VisualTreeHelper.GetChild(obj as Visual, 0); else return null; } while (!(obj is ScrollViewer)); return obj as ScrollViewer; } public static void OnDocumentPropertyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (e.OldValue != null) { DependencyProperty dp = e.OldValue as DependencyProperty; // -= OnFlowDocumentChanged } if (e.NewValue != null) { DependencyProperty dp = e.NewValue as DependencyProperty; // += OnFlowDocumentChanged // dp.AddOwner(typeof(AutoScrollBehavior), new PropertyMetadata(OnFlowDocumentChanged)); // System.ArgumentException was unhandled by user code Message='AutoScrollBehavior' // type must derive from DependencyObject. } } public static void OnFlowDocumentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { TextRange range = null; if (e.NewValue != null) { FlowDocument doc = e.NewValue as FlowDocument; if (doc != null) range = new TextRange(doc.ContentStart, doc.ContentEnd); } if (range != d.GetValue(MonitoredRangeProperty) as TextRange) d.SetValue(MonitoredRangeProperty, range); } public static void OnMonitoredRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (e.OldValue != null) { TextRange range = e.OldValue as TextRange; if (range != null) range.Changed -= new EventHandler(range_Changed); } if (e.NewValue != null) { TextRange range = e.NewValue as TextRange; if (range != null) range.Changed -= new EventHandler(range_Changed); } } static void range_Changed(object sender, EventArgs e) { // need ScrollViewer!! } 
+6
c # wpf attachedbehaviors
source share
2 answers

OnEnabledChanged is called before the visual tree is completed, and therefore does not find ScrollViewer

Use Dispatcher.BeginInvoke to include the rest of the work in the asynchronous process after creating the visual tree. You also need to call ApplyTemplate to ensure that the template has been created:

 d.Dispatcher.BeginInvoke(new Action(() => { ((FrameworkElement)d).ApplyTemplate(); d.SetValue(ScrollViewerProperty, FindScrollViewer(d)); })); 

Please note that you do not need to check if the new value is different from the old. This environment handles this for you when setting dependency properties.

You can also use FrameworkTemplate.FindName to get the ScrollViewer from the FlowDocumentScrollViewer. FlowDocumentScrollViewer has a named part of a ScrollViewer type template called PART_ContentHost where it will host the content. This may be more accurate in the case of a repeated view template and has more than one ScrollViewer as a child.

 var control = d as Control; if (control != null) { control.Dispatcher.BeginInvoke(new Action(() => { control.ApplyTemplate(); control.SetValue(ScrollViewerProperty, control.Template.FindName("PART_ContentHost", control) as ScrollViewer); })); } 

I do not know how to connect to a DependencyProperty containing a FlowDocument. My plan was to use its modified event to initialize the ManagedRange property. (Manually starts for the first time, if necessary.)

The structure does not have a built-in method of receiving notification of a property change from an arbitrary dependency property. However, you can create your own DependencyProperty and just bind it to the one you want to see. For more information, see Modify dependency property notification .

Create a dependency property:

 private static readonly DependencyProperty InternalDocumentProperty = DependencyProperty.RegisterAttached( "InternalDocument", typeof(FlowDocument), typeof(YourType), new PropertyMetadata(OnFlowDocumentChanged)); 

And replace your reflection code in OnEnabledChanged simply:

 BindingOperations.SetBinding(d, InternalDocumentProperty, new Binding("Document") { Source = d }); 

When the Document FlowDocumentScrollViewer property changes, the binding updates the InternalDocument and calls OnFlowDocumentChanged.

I do not know how to get to the ScrollViewer Property from within range_Changed, since it does not have a DependencyObject.

The sender property will be TextRange, so you can use ((TextRange)sender).Start.Parent to get a DependencyObject and then go to the visual tree.

An easier way would be to use a lambda expression to capture the variable d in OnMonitoredRangeChanged by doing something like this:

 range.Changed += (sender, args) => range_Changed(d); 

And then create a range_Changed overload that takes a DependencyObject. This will make it a little harder to remove the handler when you are done.

Also, although the response to FlowDocument's Change and Scroll Detection says that TextRange.Changed will work, I have not seen it really work when I tested it. If this does not work for you, and you are ready to use reflection, there is a TextContainer.Changed event, which seems to fire:

 var container = doc.GetType().GetProperty("TextContainer", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(doc, null); var changedEvent = container.GetType().GetEvent("Changed", BindingFlags.Instance | BindingFlags.NonPublic); EventHandler handler = range_Changed; var typedHandler = Delegate.CreateDelegate(changedEvent.EventHandlerType, handler.Target, handler.Method); changedEvent.GetAddMethod(true).Invoke(container, new object[] { typedHandler }); 

The sender parameter sender be TextContainer, and you can use reflection again to return to the FlowDocument:

 var document = sender.GetType().GetProperty("Parent", BindingFlags.Instance | BindingFlags.NonPublic) .GetValue(sender, null) as FlowDocument; var viewer = document.Parent; 
+3
source share

Does it help?

This is a good start at least (maybe?).

0
source share

All Articles