Cómo crear un comportamiento adjunto para el desplazamiento automático de un FlowDocumentScrollViewer

StackOverflow https://stackoverflow.com/questions/3300969

Pregunta

Mi objetivo es crear un comportamiento adjunto reutilizable para un FlowDocumentScrollViewer, de modo que el visor se desplace automáticamente hasta el final cada vez que el FlowDocument se haya actualizado (agregado).

Problemas hasta ahora:

  • Se llama a OnEnabledChanged antes de que se complete el árbol visual y, por lo tanto, no encuentra ScrollViewer
  • No sé cómo adjuntarlo a DependencyProperty que contiene FlowDocument.Mi plan era utilizar su evento modificado para inicializar la propiedad ManagedRange.(Se activa manualmente por primera vez si es necesario).
  • No sé cómo llegar a la propiedad ScrollViewer desde el método range_Changed, ya que no tiene DependencyObject.

Me doy cuenta de que esos son potencialmente 3 problemas separados (también conocido como.preguntas).Sin embargo, dependen entre sí y del diseño general que he intentado para este comportamiento.Hago esto como una sola pregunta en caso de que lo haga de manera incorrecta.Si lo soy, ¿cuál es la forma correcta?

/// 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!!
}
¿Fue útil?

Solución

OnEnableChanged se llama antes de que se complete el árbol visual y, por lo tanto, no encuentra el ScrollViewer

Usar Despachador.BeginInvoke para poner en cola el resto del trabajo para que se realice de forma asincrónica, después de crear el árbol visual.También tendrás que llamar Aplicar plantilla para garantizar que se haya creado una instancia de la plantilla:

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

Tenga en cuenta que no es necesario comprobar si el nuevo valor es diferente del anterior.El marco maneja eso por usted al configurar las propiedades de dependencia.

También podrías usar FrameworkTemplate.FindName para obtener ScrollViewer de FlowDocumentScrollViewer.FlowDocumentScrollViewer tiene una parte de plantilla con nombre de tipo ScrollViewer llamada PART_ContentHost que es donde realmente alojará el contenido.Esto puede ser más preciso en caso de que se cambie la plantilla del visor y tenga más de un ScrollViewer cuando era niño.

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

No sé cómo adjuntar a la dependencia de la propiedad que contiene el documento de flujo.Mi plan era usar su evento cambiado para inicializar la propiedad ManagedRange.(Se activó manualmente por primera vez si es necesario).

No hay ninguna forma integrada en el marco para recibir una notificación de cambio de propiedad desde una propiedad de dependencia arbitraria.Sin embargo, puede crear su propia DependencyProperty y simplemente vincularla a la que desea ver.Ver Notificación de cambio para propiedades de dependencia para más información.

Cree una propiedad de dependencia:

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

Y reemplace su código de reflexión en OnEnabledChanged con simplemente:

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

Cuando la propiedad Documento de FlowDocumentScrollViewer cambia, el enlace actualizará InternalDocument y se llamará a OnFlowDocumentChanged.

No sé cómo llegar a la propiedad ScrollViewer desde el método Range_Changed, ya que no tiene DependencyObject.

La propiedad del remitente será TextRange, por lo que podría usar ((TextRange)sender).Start.Parent para obtener un DependencyObject y luego subir por el árbol visual.

Un método más sencillo sería utilizar una expresión lambda para capturar el d variable en OnMonitoredRangeChanged haciendo algo como esto:

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

Y luego crear una sobrecarga de range_Changed que acepta un DependencyObject.Sin embargo, eso hará que sea un poco más difícil eliminar el controlador cuando hayas terminado.

Además, aunque la respuesta a Detectar cambio y desplazamiento de FlowDocument dice que TextRange.Changed funcionará, no vi que se activara cuando lo probé.Si no funciona para usted y está dispuesto a utilizar la reflexión, hay un evento TextContainer.Changed que parece activarse:

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

El sender El parámetro será TextContainer y podrá usar la reflexión nuevamente para volver al FlowDocument:

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

Otros consejos

¿Esto ayuda?

¡Es un buen comienzo al menos (tal vez?).

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top