Как создать прикрепленное поведение для автоматической прокрутки FlowDocumentScrollViewer - PullRequest
6 голосов
/ 21 июля 2010

Моя цель состоит в том, чтобы создать повторно используемое присоединенное поведение для FlowDocumentScrollViewer, чтобы средство просмотра автоматически прокручивало до конца всякий раз, когда FlowDocument был обновлен (добавлен).

Пока проблем:

  • OnEnabledChanged вызывается до того, как завершено визуальное дерево, и, таким образом, не находит ScrollViewer
  • Я не знаю, как подключиться к свойству DependencyProperty, содержащему FlowDocument. Мой план состоял в том, чтобы использовать измененное событие для инициализации свойства ManagedRange. (Запускается вручную при необходимости.)
  • Я не знаю, как добраться до свойства ScrollViewer из метода range_Changed, поскольку у него нет объекта 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!!
}

Ответы [ 2 ]

4 голосов
/ 05 августа 2010

OnEnabledChanged вызывается раньше визуальное дерево завершено, и, таким образом, не находит ScrollViewer

Используйте Dispatcher.BeginInvoke , чтобы поставить в очередь остальную часть работы, которая должна выполняться асинхронно, после построения визуального дерева. Вам также необходимо вызвать ApplyTemplate , чтобы убедиться, что шаблон создан:

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

Обратите внимание, что вам не нужно проверять, отличается ли новое значение от старого. Каркас обрабатывает это при настройке свойств зависимостей.

Вы также можете использовать FrameworkTemplate.FindName , чтобы получить ScrollViewer из FlowDocumentScrollViewer. 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, содержащий FlowDocument. Мой план состоял в том, чтобы использовать это измененное событие для инициализации Свойство ManagedRange. (Вручную срабатывает в первый раз, если необходимо.)

Нет встроенного в каркас способа получения уведомления об изменении свойства из свойства произвольной зависимости. Однако вы можете создать свой собственный DependencyProperty и просто привязать его к тому, который хотите посмотреть. См. Уведомление об изменении свойств зависимостей для получения дополнительной информации.

Создать свойство зависимости:

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

При изменении свойства Document FlowDocumentScrollViewer привязка обновит InternalDocument, и будет вызван OnFlowDocumentChanged.

Я не знаю, как добраться до Свойство ScrollViewer изнутри Метод range_Changed, так как он не иметь объект DependencyObject.

Свойство отправителя будет TextRange, поэтому вы можете использовать ((TextRange)sender).Start.Parent, чтобы получить объект DependencyObject, а затем пройтись по визуальному дереву.

Более простым способом было бы использовать лямбда-выражение для захвата переменной d в OnMonitoredRangeChanged, выполнив что-то вроде этого:

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

А затем создается перегрузка range_Changed, которая принимает объект DependencyObject. Тем не менее, это будет немного сложнее удалить обработчик, когда вы закончите.

Кроме того, хотя в ответе Обнаружение изменения и прокрутки FlowDocument говорится, что 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;
0 голосов
/ 04 августа 2010

Помогает ли это?

Это хорошее начало, по крайней мере (может быть?).

...