如何创建自动滚动FlowDocumentScrollViewer的附加行为

chi*_*emp 6 c# wpf attachedbehaviors c#-4.0

我的目标是为FlowDocumentScrollViewer创建可重用的附加行为,以便每当FlowDocument更新(附加)时,查看器自动滚动到末尾.

到目前为止的问题

  • 在可视树完成之前调用OnEnabledChanged,因此找不到ScrollViewer
  • 我不知道如何附加到包含FlowDocument的DependencyProperty.我的计划是使用它的已更改事件来初始化ManagedRange属性.(如果需要,第一次手动触发.)
  • 我不知道如何从range_Changed方法中获取ScrollViewer属性,因为它没有DependencyObject.

我意识到那些可能是3个独立的问题(也就是问题).但是它们彼此依赖,并且我尝试了这种行为的整体设计.我问这是一个单一的问题,以防我以错误的方式解决这个问题.如果我是,那么正确的方法是什么?

/// 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!!
}
Run Code Online (Sandbox Code Playgroud)

Qua*_*ter 4

OnEnabledChanged 在可视化树完成之前被调用,因此找不到 ScrollViewer

构建可视化树后,使用Dispatcher.BeginInvoke将其余工作排队以异步发生。您还需要调用ApplyTemplate以确保模板已实例化:

d.Dispatcher.BeginInvoke(new Action(() =>
{
    ((FrameworkElement)d).ApplyTemplate();
    d.SetValue(ScrollViewerProperty, FindScrollViewer(d));
}));
Run Code Online (Sandbox Code Playgroud)

请注意,您不需要检查新值是否与旧值不同。设置依赖属性时,框架会为您处理该问题。

您还可以使用FrameworkTemplate.FindName从 FlowDocumentScrollViewer 获取 ScrollViewer。FlowDocumentScrollViewer 有一个名为 PART_ContentHost 的 ScrollViewer 类型的命名模板部分,它是实际托管内容的位置。如果查看器被重新模板化并且具有多个 ScrollViewer 作为子项,则这可能会更准确。

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);
    }));
}
Run Code Online (Sandbox Code Playgroud)

我不知道如何附加到包含 FlowDocument 的 DependencyProperty。我的计划是使用它的更改事件来初始化 ManagedRange 属性。(如果需要,首次手动触发。)

框架中没有内置方法可以从任意依赖属性获取属性更改通知。但是,您可以创建自己的 DependencyProperty 并将其绑定到您想要观看的属性。有关详细信息, 请参阅依赖项属性的更改通知。

创建依赖属性:

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

并将 OnEnabledChanged 中的反射代码替换为:

BindingOperations.SetBinding(d, InternalDocumentProperty, 
    new Binding("Document") { Source = d });
Run Code Online (Sandbox Code Playgroud)

当 FlowDocumentScrollViewer 的 Document 属性更改时,绑定将更新 InternalDocument,并调用 OnFlowDocumentChanged。

我不知道如何从 range_Changed 方法中访问 ScrollViewer 属性,因为它没有 DependencyObject。

sender 属性将是一个 TextRange,因此您可以用来((TextRange)sender).Start.Parent获取 DependencyObject,然后沿着可视化树向上走。

一种更简单的方法是使用 lambda 表达式来捕获dOnMonitoredRangeChanged 中的变量,方法如下:

range.Changed += (sender, args) => range_Changed(d);
Run Code Online (Sandbox Code Playgroud)

然后创建接受 DependencyObject 的 range_Changed 重载。不过,这将使完成后删除处理程序变得更加困难。

另外,虽然Detect FlowDocument Change and Scroll的答案说 TextRange.Changed 可以工作,但当我测试它时,我并没有看到它实际触发。如果它对您不起作用并且您愿意使用反射,则有一个 TextContainer.Changed 事件似乎会触发:

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 });
Run Code Online (Sandbox Code Playgroud)

参数sender将是 TextContainer,您可以再次使用反射返回 FlowDocument:

var document = sender.GetType().GetProperty("Parent", 
    BindingFlags.Instance | BindingFlags.NonPublic)
    .GetValue(sender, null) as FlowDocument;
var viewer = document.Parent;
Run Code Online (Sandbox Code Playgroud)