Empujar las propiedades de la GUI de solo lectura nuevamente dentro de ViewModel

124

Quiero escribir un ViewModel que siempre conozca el estado actual de algunas propiedades de dependencia de solo lectura desde 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 se permitiera, sería perfecto: cada vez que cambiara la propiedad CanGoToNextPage de FlowDocumentPageViewer, el nuevo valor sería introducido 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 establecer desde el marcado. Aparentemente, las propiedades de solo lectura no admiten ningún tipo de enlace de datos, ni siquiera el enlace de datos que es 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 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 obtener notificaciones de cambio de una DependencyProperty, salvo crear otra DependencyProperty para vincularlo, lo que parece excesivo aquí.

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

Joe White
fuente

Respuestas:

152

Sí, he hecho esto en el pasado con las propiedades ActualWidthy ActualHeight, las cuales son de solo lectura. Creé un comportamiento adjunto que tiene ObservedWidthy ObservedHeightpropiedades adjuntas. También tiene una Observepropiedad que se utiliza para hacer 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}"

Por lo que el modelo de vista tiene Widthy Heightpropiedades que están siempre en sincronía con las ObservedWidthy ObservedHeightunidos propiedades. La Observepropiedad simplemente se adjunta al SizeChangedevento de la FrameworkElement. En el identificador, actualiza sus ObservedWidthy sus ObservedHeightpropiedades. 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 ObservedWidthy los ObservedHeightDP no son de solo lectura.

ACTUALIZACIÓN: aquí hay un 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
fuente
2
Me pregunto si podrías hacer algunos trucos para adjuntar automáticamente las propiedades, sin necesidad de Observar. Pero esto parece una buena solución. ¡Gracias!
Joe White el
1
Gracias Kent Publiqué una muestra de código a continuación para esta clase "SizeObserver".
Scott Whitlock
52
+1 a este sentimiento: "los DP de solo lectura deben admitir enlaces OneWayToSource"
Tristán
3
Quizás incluso mejor crear una sola Sizepropiedad, combinando Altura y Ancho. Aprox. 50% menos de código.
Gerard
1
@Gerard: Eso no funcionará porque no hay ninguna ActualSizepropiedad FrameworkElement. Si desea la vinculación directa de las propiedades adjuntas, debe crear dos propiedades a las que vincular ActualWidthy ActualHeightrespectivamente.
dotNET
58

Utilizo una solución universal que funciona no solo con ActualWidth y ActualHeight, sino también con cualquier dato que pueda vincular al menos en modo de lectura.

El marcado tiene este aspecto, siempre que ViewportWidth y ViewportHeight sean propiedades del modelo de vista

<Canvas>
    <u:DataPiping.DataPipes>
         <u:DataPipeCollection>
             <u:DataPipe Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type Canvas}}, Path=ActualWidth}"
                         Target="{Binding Path=ViewportWidth, Mode=OneWayToSource}"/>
             <u:DataPipe Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type Canvas}}, Path=ActualHeight}"
                         Target="{Binding Path=ViewportHeight, Mode=OneWayToSource}"/>
          </u:DataPipeCollection>
     </u:DataPiping.DataPipes>
<Canvas>

Aquí está el código fuente de los elementos personalizados.

public class DataPiping
{
    #region DataPipes (Attached DependencyProperty)

    public static readonly DependencyProperty DataPipesProperty =
        DependencyProperty.RegisterAttached("DataPipes",
        typeof(DataPipeCollection),
        typeof(DataPiping),
        new UIPropertyMetadata(null));

    public static void SetDataPipes(DependencyObject o, DataPipeCollection value)
    {
        o.SetValue(DataPipesProperty, value);
    }

    public static DataPipeCollection GetDataPipes(DependencyObject o)
    {
        return (DataPipeCollection)o.GetValue(DataPipesProperty);
    }

    #endregion
}

public class DataPipeCollection : FreezableCollection<DataPipe>
{

}

public class DataPipe : Freezable
{
    #region Source (DependencyProperty)

    public object Source
    {
        get { return (object)GetValue(SourceProperty); }
        set { SetValue(SourceProperty, value); }
    }
    public static readonly DependencyProperty SourceProperty =
        DependencyProperty.Register("Source", typeof(object), typeof(DataPipe),
        new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnSourceChanged)));

    private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((DataPipe)d).OnSourceChanged(e);
    }

    protected virtual void OnSourceChanged(DependencyPropertyChangedEventArgs e)
    {
        Target = e.NewValue;
    }

    #endregion

    #region Target (DependencyProperty)

    public object Target
    {
        get { return (object)GetValue(TargetProperty); }
        set { SetValue(TargetProperty, value); }
    }
    public static readonly DependencyProperty TargetProperty =
        DependencyProperty.Register("Target", typeof(object), typeof(DataPipe),
        new FrameworkPropertyMetadata(null));

    #endregion

    protected override Freezable CreateInstanceCore()
    {
        return new DataPipe();
    }
}
Dmitry Tashkinov
fuente
(a través de una respuesta del usuario 543564): Esta no es una respuesta, sino un comentario a Dmitry: utilicé su solución y funcionó muy bien. Buena solución universal que puede usarse genéricamente en diferentes lugares Lo usé para insertar algunas propiedades del elemento ui (ActualHeight y ActualWidth) en mi modelo de vista.
Marc Gravell
2
¡Gracias! Esto me ayudó a unirme a una propiedad de solo obtención normal. Lamentablemente, la propiedad no publicó eventos INotifyPropertyChanged. Resolví esto asignando un nombre al enlace DataPipe y agregando lo siguiente al evento cambiado de los controles: BindingOperations.GetBindingExpressionBase (bindingName, DataPipe.SourceProperty) .UpdateTarget ();
chilltemp
3
Esta solución funcionó bien para mí. Mi único ajuste fue establecer BindsTwoWayByDefault en true para FrameworkPropertyMetadata en TargetProperty DependencyProperty.
Hasani Blackwell
1
La única queja acerca de esta solución parece ser que rompe la encapsulación limpia, ya que la Targetpropiedad debe hacerse escribible aunque no se debe cambiar desde el exterior: - /
O Mapper
Para aquellos que preferirían el paquete NuGet a copiar y pegar el código: he agregado DataPipe a mi biblioteca JungleControls de código abierto. Consulte la documentación de DataPipe .
Robert Važan
21

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éntase libre de usarlo en sus aplicaciones. Funciona bien. (¡Gracias Kent!)

Scott Whitlock
fuente
10

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

Funciona utilizando dos propiedades de dependencia, Listener y Mirror. Listener está vinculado OneWay a TargetProperty y en PropertyChangedCallback actualiza la propiedad Mirror que está vinculada a OneWayToSource a lo que se haya especificado 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 uso de muestra breve, o visite mi blog WPF si está interesado en los detalles de implementación.

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

Fredrik Hedblad
fuente
Las respuestas sobre Stack Overflow deben ser completamente independientes. Está bien incluir un enlace a referencias externas opcionales, pero todo el código requerido para la respuesta debe incluirse en la respuesta misma. Actualice su pregunta para que pueda usarse sin visitar ningún otro sitio web.
Peter Duniho
4

¡Me gusta la solución de Dmitry Tashkinov! Sin embargo, se estrelló mi VS en modo de diseño. Es por eso que 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
fuente
0

Creo que se puede hacer un poco más simple:

xaml:

behavior:ReadOnlyPropertyToModelBindingBehavior.ReadOnlyDependencyProperty="{Binding ActualWidth, RelativeSource={RelativeSource Self}}"
behavior:ReadOnlyPropertyToModelBindingBehavior.ModelProperty="{Binding MyViewModelProperty}"

cs:

public class ReadOnlyPropertyToModelBindingBehavior
{
  public static readonly DependencyProperty ReadOnlyDependencyPropertyProperty = DependencyProperty.RegisterAttached(
     "ReadOnlyDependencyProperty", 
     typeof(object), 
     typeof(ReadOnlyPropertyToModelBindingBehavior),
     new PropertyMetadata(OnReadOnlyDependencyPropertyPropertyChanged));

  public static void SetReadOnlyDependencyProperty(DependencyObject element, object value)
  {
     element.SetValue(ReadOnlyDependencyPropertyProperty, value);
  }

  public static object GetReadOnlyDependencyProperty(DependencyObject element)
  {
     return element.GetValue(ReadOnlyDependencyPropertyProperty);
  }

  private static void OnReadOnlyDependencyPropertyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
  {
     SetModelProperty(obj, e.NewValue);
  }


  public static readonly DependencyProperty ModelPropertyProperty = DependencyProperty.RegisterAttached(
     "ModelProperty", 
     typeof(object), 
     typeof(ReadOnlyPropertyToModelBindingBehavior), 
     new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

  public static void SetModelProperty(DependencyObject element, object value)
  {
     element.SetValue(ModelPropertyProperty, value);
  }

  public static object GetModelProperty(DependencyObject element)
  {
     return element.GetValue(ModelPropertyProperty);
  }
}
eriksmith200
fuente
2
Puede ser un poco más simple, pero si lo leo bien, solo permite uno de esos enlaces en el Elemento. Quiero decir, creo que con este enfoque, no podrás vincular ActualWidth y ActualHeight. Solo uno de ellos.
quetzalcoatl