wpf - Empujando las propiedades de la GUI de solo lectura a ViewModel
data-binding mvvm (6)
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 siempre sepa 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: siempre que la propiedad CanGoToNextPage de FlowDocumentPageViewer cambiara, el nuevo valor sería empujado hacia abajo en la propiedad NextPageAvailable de ViewModel, que es exactamente lo que quiero.
Desafortunadamente, esto no se compila: recibo 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 enlace de datos de solo lectura con respecto a esa propiedad.
Podría hacer que las propiedades de mi ViewModel sean DependencyProperties, y hacer que un enlace OneWay vaya en otra dirección, pero no estoy loco por la violación de separación de preocupaciones (ViewModel necesitaría una referencia a View, que se supone que evita el enlace de datos MVVM )
FlowDocumentPageViewer no expone un evento CanGoToNextPageChanged, y no conozco ninguna buena forma de obtener notificaciones de cambio desde 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?
¡Me gusta la solución de Dmitry Tashkinov! Sin embargo, colisionó mi VS en modo diseño. Es por eso que agregué una línea al método OnSourceChanged:
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue)) ((DataPipe)d).OnSourceChanged(e); }
Aquí hay otra solución a este "error" sobre el cual blogé aquí:
Enlace de OneWayToSource para la propiedad de ReadOnly Dependency
Funciona mediante el uso de dos Propiedades de dependencia, Escucha y Espejo. 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 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 Demo Project aquí .
Contiene código fuente y uso de muestra breve, o visite mi blog de WPF si está interesado en los detalles de la implementación.
Una última nota, desde .NET 4.0 estamos aún más lejos del soporte integrado para esto, ya que OneWayToSource Binding lee el valor de nuevo desde el origen después de haberlo actualizado
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);
}
}
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 usa 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 tanto, el modelo de vista tiene propiedades de Width
y Height
que están siempre sincronizadas con las propiedades adjuntas ObservedWidth
y ObservedHeight
. La propiedad Observe
simplemente se une al evento SizeChanged
de FrameworkElement
. En el mango, actualiza sus propiedades ObservedWidth
y ObservedHeight
. Ergo, el Width
y la Height
del modelo de vista siempre está sincronizado con el ActualWidth
y la ActualWidth
ActualHeight
del ActualHeight
de UserControl
.
Quizás no sea la solución perfecta (estoy de acuerdo, las DP de solo lectura deberían ser compatibles OneWayToSource
vinculaciones de OneWayToSource
), pero funciona y mantiene el patrón de MVVM. Obviamente, ObservedWidth
y ObservedHeight
no son de solo lectura.
ACTUALIZACIÓN: aquí está el código que implementa la funcionalidad descrita arriba:
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);
}
}
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!)
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 modo de lectura.
El marcado se ve así, 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();
}
}