2009-07-05 18 views
106

Deseo escribir un ViewModel que siempre conozca el estado actual de algunas propiedades de dependencia de solo lectura de la Vista.Impulsar propiedades de GUI de solo lectura en ViewModel

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 siempre sepa los valores de estas dos propiedades de Vista.

pensé que podía hacer esto con un enlace de datos OneWayToSource:

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

Si esto era permitido, sería perfecto: cada vez que la propiedad CanGoToNextPage del FlowDocumentPageViewer modificado, el nuevo valor sería empujada hacia abajo en NextPageAvailable del modelo de vista propiedad, que es exactamente lo que quiero.

Lamentablemente, esto no compila: Recibo un error que dice La propiedad 'CanGoToPreviousPage' es de solo lectura y no se puede configurar desde el marcado. Al parecer, las propiedades de solo lectura no admiten ningún tipo de enlace de datos, ni siquiera el 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 yendo para otro lado, pero no estoy loco por la violación de separación de preocupaciones (ViewModel necesitaría una referencia a View, que datavinding de MVVM se supone que debe evitar).

FlowDocumentPageViewer no expone un evento CanGoToNextPageChanged, y no conozco ninguna buena forma de obtener notificaciones de cambio de DependencyProperty, salvo crear otro DependencyProperty para vincularlo, lo que parece exagerado aquí.

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

Respuesta

129

Sí, he hecho esto en el pasado con las propiedades ActualWidth y ActualHeight, que son de solo lectura. Creé un comportamiento adjunto que tiene propiedades adjuntas ObservedWidth y ObservedHeight. También tiene una propiedad Observe que se utiliza para hacer la conexión inicial. Uso parece a esto:

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

lo tanto, el modelo de vista tiene Width y Height propiedades que siempre están en sincronía con los ObservedWidth y ObservedHeight propiedades adjuntas. La propiedad Observe simplemente se conecta al evento SizeChanged del FrameworkElement. En el mango, actualiza sus propiedades ObservedWidth y ObservedHeight. Ergo, el Width y el Height del modelo de vista siempre están sincronizados con ActualWidth y ActualHeight del UserControl.

Tal vez no sea la solución perfecta (estoy de acuerdo - de sólo lectura AD debe apoyo OneWayToSource fijaciones), pero funciona y se mantiene el patrón MVVM. Obviamente, los DP ObservedWidth y ObservedHeight son no de solo lectura.

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

me pregunto si pudieras hacer algún truco para adjuntar automáticamente las propiedades, sin necesidad de observar. Pero esto parece una buena solución. ¡Gracias! –

+1

Gracias Kent. Publiqué un ejemplo de código a continuación para esta clase "SizeObserver". –

+42

+1 a este sentimiento: "los DP de solo lectura deben ser compatibles con los enlaces OneWayToSource" – Tristan

20

Si alguien más está interesado, me codificados hasta 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 tus aplicaciones Funciona bien. (¡Gracias Kent!)

49

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

El marcado se parece a esto, siempre ViewportWidth y ViewportHeight son 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 para 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(); 
    } 
} 
+0

(a través de una respuesta del usuario543564): esta no es una respuesta, sino un comentario para Dmitry: utilicé su solución y funcionó muy bien. Buena solución universal que se puede usar genéricamente en diferentes lugares. Lo usé para insertar algunas propiedades del elemento ui (ActualHeight y ActualWidth) en mi viewmodel. –

+2

¡Gracias! Esto me ayudó a vincularme a una propiedad de obtener solo normal. Lamentablemente, la propiedad no publicó eventos INotifyPropertyChanged. Lo resolví asignando un nombre al enlace de la conexión de datos y agregando lo siguiente al evento de controles modificados: BindingOperations.GetBindingExpressionBase (bindingName, DataPipe.SourceProperty) .UpdateTarget(); – chilltemp

+3

Esta solución funcionó bien para mí. Mi único cambio fue establecer BindsTwoWayByDefault en true para FrameworkPropertyMetadata en TargetProperty DependencyProperty. –

9

Aquí hay otra solución a este " error "que publiqué aquí:
OneWayToSource Binding for ReadOnly Dependency Property

Funciona mediante el uso de t wo Dependency Properties, Listener and Mirror. El oyente está vinculado a OneWay con TargetProperty y en PropertyChangedCallback actualiza la propiedad Mirror que vincula OneWayToSource a lo que se especificó en el enlace. Lo llamo PushBinding y que se pueden establecer en cualquier dependencia de sólo lectura propiedad 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> 

Download Demo Project Here.
Contiene código fuente y uso de muestra breve, o visite my WPF blog si está interesado en los detalles de implementación.

Una última nota, ya que .NET 4.0 estamos aún más lejos de una función de soporte para esto, ya que un OneWayToSource Binding reads the value back from the Source after it has updated it

4

me gusta la solución de Dmitry Tashkinov! Sin embargo, colisionó mi VS en modo de diseño. Es por eso que he añadido una línea con el método OnSourceChanged:

 
    private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
    { 
     if (!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue)) 
      ((DataPipe)d).OnSourceChanged(e); 
    } 
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); 
    } 
} 
+0

Puede ser un poco más simple, pero si lo leo bien, permite ** solo un ** tal enlace en el Elemento. Quiero decir, creo que con este enfoque, no podrás unir tanto a ActualWidth ** como a ** ActualHeight. Solo uno de ellos. – quetzalcoatl

Cuestiones relacionadas