Enviar propiedades GUI de solo lectura a ViewModel

Resuelto Joe White asked hace 15 años • 7 respuestas

Quiero escribir un ViewModel que siempre conozca el estado actual de algunas propiedades de dependencia de solo lectura de la Vista.

Específicamente, mi GUI contiene un FlowDocumentPageViewer, que muestra una página a la vez desde un FlowDocument. FlowDocumentPageViewer expone dos propiedades de dependencia de solo lectura llamadas CanGoToPreviousPage y CanGoToNextPage. Quiero que mi ViewModel sepa siempre los valores de estas dos propiedades de Vista.

Pensé que podría hacer esto con un enlace de datos OneWayToSource:

<FlowDocumentPageViewer
    CanGoToNextPage="{Binding NextPageAvailable, Mode=OneWayToSource}" ...>

Si esto estuviera permitido, sería perfecto: cada vez que cambiara la propiedad CanGoToNextPage de FlowDocumentPageViewer, el nuevo valor se introduciría en la propiedad NextPageAvailable de ViewModel, que es exactamente lo que quiero.

Desafortunadamente, esto no se compila: aparece un error que dice que la propiedad 'CanGoToPreviousPage' es de solo lectura y no se puede configurar desde el marcado. Aparentemente, las propiedades de solo lectura no admiten ningún tipo de enlace de datos, ni siquiera un enlace de datos de solo lectura con respecto a esa propiedad.

Podría hacer que las propiedades de mi ViewModel sean DependencyProperties y hacer un enlace OneWay en sentido contrario, pero no estoy loco por la violación de la separación de preocupaciones (ViewModel necesitaría una referencia a la Vista, que se supone que debe evitar el enlace de datos MVVM). ).

FlowDocumentPageViewer no expone un evento CanGoToNextPageChanged, y no conozco ninguna buena manera de recibir notificaciones de cambios de DependencyProperty, salvo crear otro DependencyProperty al que vincularlo, lo que parece excesivo aquí.

¿Cómo puedo mantener informado a mi ViewModel sobre los cambios en las propiedades de solo lectura de la vista?

Joe White avatar Jul 05 '09 07:07 Joe White
Aceptado

Sí, he hecho esto en el pasado con las propiedades ActualWidthy ActualHeight, las cuales son de solo lectura. Creé un comportamiento adjunto que tiene propiedades ObservedWidthadjuntas ObservedHeight. También tiene una Observepropiedad que se utiliza para realizar la conexión inicial. El uso se ve así:

<UserControl ...
    SizeObserver.Observe="True"
    SizeObserver.ObservedWidth="{Binding Width, Mode=OneWayToSource}"
    SizeObserver.ObservedHeight="{Binding Height, Mode=OneWayToSource}"

Entonces, el modelo de vista tiene Widthpropiedades Heightque siempre están sincronizadas con las propiedades ObservedWidthadjuntas ObservedHeight. La Observepropiedad simplemente se adjunta al SizeChangedevento del FrameworkElement. En el identificador, actualiza sus propiedades ObservedWidthy ObservedHeight. Ergo, el Widthy Heightdel modelo de vista siempre está sincronizado con el ActualWidthy ActualHeightdel UserControl.

Quizás no sea la solución perfecta (estoy de acuerdo: los DP de solo lectura deberían admitir OneWayToSourceenlaces), pero funciona y mantiene el patrón MVVM. Obviamente, los DP ObservedWidthy no son de sólo lectura.ObservedHeight

ACTUALIZACIÓN: aquí está el código que implementa la funcionalidad descrita anteriormente:

public static class SizeObserver
{
    public static readonly DependencyProperty ObserveProperty = DependencyProperty.RegisterAttached(
        "Observe",
        typeof(bool),
        typeof(SizeObserver),
        new FrameworkPropertyMetadata(OnObserveChanged));

    public static readonly DependencyProperty ObservedWidthProperty = DependencyProperty.RegisterAttached(
        "ObservedWidth",
        typeof(double),
        typeof(SizeObserver));

    public static readonly DependencyProperty ObservedHeightProperty = DependencyProperty.RegisterAttached(
        "ObservedHeight",
        typeof(double),
        typeof(SizeObserver));

    public static bool GetObserve(FrameworkElement frameworkElement)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        return (bool)frameworkElement.GetValue(ObserveProperty);
    }

    public static void SetObserve(FrameworkElement frameworkElement, bool observe)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        frameworkElement.SetValue(ObserveProperty, observe);
    }

    public static double GetObservedWidth(FrameworkElement frameworkElement)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        return (double)frameworkElement.GetValue(ObservedWidthProperty);
    }

    public static void SetObservedWidth(FrameworkElement frameworkElement, double observedWidth)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        frameworkElement.SetValue(ObservedWidthProperty, observedWidth);
    }

    public static double GetObservedHeight(FrameworkElement frameworkElement)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        return (double)frameworkElement.GetValue(ObservedHeightProperty);
    }

    public static void SetObservedHeight(FrameworkElement frameworkElement, double observedHeight)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        frameworkElement.SetValue(ObservedHeightProperty, observedHeight);
    }

    private static void OnObserveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var frameworkElement = (FrameworkElement)dependencyObject;

        if ((bool)e.NewValue)
        {
            frameworkElement.SizeChanged += OnFrameworkElementSizeChanged;
            UpdateObservedSizesForFrameworkElement(frameworkElement);
        }
        else
        {
            frameworkElement.SizeChanged -= OnFrameworkElementSizeChanged;
        }
    }

    private static void OnFrameworkElementSizeChanged(object sender, SizeChangedEventArgs e)
    {
        UpdateObservedSizesForFrameworkElement((FrameworkElement)sender);
    }

    private static void UpdateObservedSizesForFrameworkElement(FrameworkElement frameworkElement)
    {
        // WPF 4.0 onwards
        frameworkElement.SetCurrentValue(ObservedWidthProperty, frameworkElement.ActualWidth);
        frameworkElement.SetCurrentValue(ObservedHeightProperty, frameworkElement.ActualHeight);

        // WPF 3.5 and prior
        ////SetObservedWidth(frameworkElement, frameworkElement.ActualWidth);
        ////SetObservedHeight(frameworkElement, frameworkElement.ActualHeight);
    }
}
Kent Boogaart avatar Jul 05 '2009 09:07 Kent Boogaart

Si alguien más está interesado, codifiqué una aproximación de la solución de Kent aquí:

class SizeObserver
{
    #region " Observe "

    public static bool GetObserve(FrameworkElement elem)
    {
        return (bool)elem.GetValue(ObserveProperty);
    }

    public static void SetObserve(
      FrameworkElement elem, bool value)
    {
        elem.SetValue(ObserveProperty, value);
    }

    public static readonly DependencyProperty ObserveProperty =
        DependencyProperty.RegisterAttached("Observe", typeof(bool), typeof(SizeObserver),
        new UIPropertyMetadata(false, OnObserveChanged));

    static void OnObserveChanged(
      DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement elem = depObj as FrameworkElement;
        if (elem == null)
            return;

        if (e.NewValue is bool == false)
            return;

        if ((bool)e.NewValue)
            elem.SizeChanged += OnSizeChanged;
        else
            elem.SizeChanged -= OnSizeChanged;
    }

    static void OnSizeChanged(object sender, RoutedEventArgs e)
    {
        if (!Object.ReferenceEquals(sender, e.OriginalSource))
            return;

        FrameworkElement elem = e.OriginalSource as FrameworkElement;
        if (elem != null)
        {
            SetObservedWidth(elem, elem.ActualWidth);
            SetObservedHeight(elem, elem.ActualHeight);
        }
    }

    #endregion

    #region " ObservedWidth "

    public static double GetObservedWidth(DependencyObject obj)
    {
        return (double)obj.GetValue(ObservedWidthProperty);
    }

    public static void SetObservedWidth(DependencyObject obj, double value)
    {
        obj.SetValue(ObservedWidthProperty, value);
    }

    // Using a DependencyProperty as the backing store for ObservedWidth.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ObservedWidthProperty =
        DependencyProperty.RegisterAttached("ObservedWidth", typeof(double), typeof(SizeObserver), new UIPropertyMetadata(0.0));

    #endregion

    #region " ObservedHeight "

    public static double GetObservedHeight(DependencyObject obj)
    {
        return (double)obj.GetValue(ObservedHeightProperty);
    }

    public static void SetObservedHeight(DependencyObject obj, double value)
    {
        obj.SetValue(ObservedHeightProperty, value);
    }

    // Using a DependencyProperty as the backing store for ObservedHeight.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ObservedHeightProperty =
        DependencyProperty.RegisterAttached("ObservedHeight", typeof(double), typeof(SizeObserver), new UIPropertyMetadata(0.0));

    #endregion
}

Siéntete libre de usarlo en tus aplicaciones. Funciona bien. (¡Gracias Kent!)

Scott Whitlock avatar Aug 20 '2009 12:08 Scott Whitlock

Aquí hay otra solución a este "error" sobre el que escribí en el blog aquí:
Enlace OneWayToSource para propiedad de dependencia de solo lectura

Funciona mediante el uso de dos propiedades de dependencia, escucha y espejo. El oyente está vinculado OneWay a TargetProperty y en PropertyChangedCallback actualiza la propiedad Mirror que está vinculada OneWayToSource a lo que se especificó en el enlace. Lo llamo PushBindingy se puede configurar en cualquier propiedad de dependencia de solo lectura como esta

<TextBlock Name="myTextBlock"
           Background="LightBlue">
    <pb:PushBindingManager.PushBindings>
        <pb:PushBinding TargetProperty="ActualHeight" Path="Height"/>
        <pb:PushBinding TargetProperty="ActualWidth" Path="Width"/>
    </pb:PushBindingManager.PushBindings>
</TextBlock>

Descargue el proyecto de demostración aquí .
Contiene código fuente y un breve ejemplo de uso.

Una última nota, desde .NET 4.0 estamos aún más lejos del soporte integrado para esto, ya que un enlace OneWayToSource lee el valor de la fuente después de haberlo actualizado.

Fredrik Hedblad avatar Aug 29 '2011 00:08 Fredrik Hedblad

¡Me gusta la solución de Dmitry Tashkinov! Sin embargo, falló mi VS en modo de diseño. Por eso agregué una línea al método OnSourceChanged:

    vacío estático privado OnSourceChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
            ((DataPipe)d).OnSourceChanged(e);
    }
Dariusz Wasacz avatar Oct 04 '2012 13:10 Dariusz Wasacz