views:

149

answers:

2

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

Problems so far:

  • OnEnabledChanged gets called before the visual tree is completed, and thus doesn't find the ScrollViewer
  • I don't know how to attach to the DependencyProperty containing the FlowDocument. My plan was to use it's changed event to initialize the ManagedRange property. (Manually triggered for the first time if needed.)
  • I don't know how to get to the ScrollViewer property from within the range_Changed method, as it doesn't have the DependencyObject.

I realize that those are potentially 3 separate issues (aka. questions). However they are dependent on each other and the overall design I've attempted for this behavior. I'm asking this as a single question in case I'm going about this the wrong way. If I am, 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!!
}
A: 

Does this help?

It's a good start at least (maybe?).

TheSoftwareJedi
+2  A: 

OnEnabledChanged gets called before the visual tree is completed, and thus doesn't find the ScrollViewer

Use Dispatcher.BeginInvoke to enqueue the rest of the work to happen asynchronously, after the visual tree is built. You will also need to call ApplyTemplate to ensure that the template has been instantiated:

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

Note that you don't need to check whether the new value is different from the old one. The framework handles that for you when setting dependency properties.

You could also use FrameworkTemplate.FindName to get the ScrollViewer from the FlowDocumentScrollViewer. FlowDocumentScrollViewer has a named template part of type ScrollViewer called PART_ContentHost that is where it will actually host the content. This can be more accurate in case the viewer is re-templated 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 don't know how to attach to the DependencyProperty containing the FlowDocument. My plan was to use it's changed event to initialize the ManagedRange property. (Manually triggered for the first time if needed.)

There is no way built into the framework to get property changed notification from an arbitrary dependency property. However, you can create your own DependencyProperty and just bind it to the one you want to watch. See Change Notification for Dependency Properties for more information.

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 with simply:

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

When the Document property of the FlowDocumentScrollViewer changes, the binding will update InternalDocument, and OnFlowDocumentChanged will be called.

I don't know how to get to the ScrollViewer property from within the range_Changed method, as it doesn't have the DependencyObject.

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

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

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

And then creating an overload of range_Changed that takes in a DependencyObject. That will make it a little harder to remove the handler when you're done, though.

Also, although the answer to Detect FlowDocument Change and Scroll says that TextRange.Changed will work, I didn't see it actually fire when I tested it. If it doesn't work for you and you're willing to use reflection, there is a TextContainer.Changed event that does seem 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 will be the TextContainer, and you can use reflection again to get back to the FlowDocument:

var document = sender.GetType().GetProperty("Parent", 
    BindingFlags.Instance | BindingFlags.NonPublic)
    .GetValue(sender, null) as FlowDocument;
var viewer = document.Parent;
Quartermeister
Impressive, thanks. I'll look at this in detail in the next day or two.
chilltemp
Thanks for all the help. I was able to get it working using the TextContainer reflection logic. It's a shame that MS has ITextContainer as an internal interface. This might break in a future .NET version, but not my first hack like it.
chilltemp