【问题标题】:How to create an Attached Behavior for automatic scrolling of a FlowDocumentScrollViewer如何为 FlowDocumentScrollViewer 的自动滚动创建附加行为
【发布时间】:2010-07-21 15:27:25
【问题描述】:

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

目前的问题:

  • 在可视化树完成之前调用 OnEnabledChanged,因此找不到 ScrollViewer
  • 我不知道如何附加到包含 FlowDocument 的 DependencyProperty。我的计划是使用 it's changed 事件来初始化 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!!
}

【问题讨论】:

    标签: c# wpf c#-4.0 attachedbehaviors


    【解决方案1】:

    OnEnabledChanged 在之前被调用 视觉树已完成,因此 没有找到 ScrollViewer

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

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

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

    您也可以使用FrameworkTemplate.FindName 从 FlowDocumentScrollViewer 中获取 ScrollViewer。 FlowDocumentScrollViewer 有一个类型为 ScrollViewer 的命名模板部分,称为 PART_ContentHost,它将实际托管内容。如果查看器被重新模板化并且有多个 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);
        }));
    }
    

    我不知道如何附加到 DependencyProperty 包含 流文档。我的计划是使用它的 更改事件以初始化 托管范围属性。 (手动 如果第一次触发 需要。)

    框架中无法从任意依赖属性中获取属性更改通知。但是,您可以创建自己的 DependencyProperty 并将其绑定到您要观看的那个。请参阅Change Notification for Dependency Properties 了解更多信息。

    创建依赖属性:

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

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

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

    当 FlowDocumentScrollViewer 的 Document 属性发生变化时,绑定会更新 InternalDocument,并调用 OnFlowDocumentChanged。

    我不知道怎么去 ScrollViewer 属性从内部 range_Changed 方法,因为它没有 有 DependencyObject。

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

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

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

    然后创建一个接受 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 });
    

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

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

    【讨论】:

    • 令人印象深刻,谢谢。我会在接下来的一两天内详细了解这一点。
    • 感谢所有帮助。我能够使用 TextContainer 反射逻辑让它工作。遗憾的是 MS 将 ITextContainer 作为内部接口。这可能会在未来的 .NET 版本中中断,但不是我的第一次 hack。
    【解决方案2】:

    Does this help?

    至少这是一个好的开始(也许?)。

    【讨论】:

      猜你喜欢
      • 2021-07-25
      • 2011-09-18
      • 1970-01-01
      • 2021-12-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-08
      相关资源
      最近更新 更多