Empurrando propriedades de GUI somente leitura de volta para ViewModel

Eu quero escrever um ViewModel que sempre sabe o estado atual de algumas propriedades de dependência somente leitura da exibição.

Especificamente, minha GUI contém um FlowDocumentPageViewer, que exibe uma página de cada vez a partir de um FlowDocument. O FlowDocumentPageViewer expõe duas propriedades de dependência somente leitura chamadas CanGoToPreviousPage e CanGoToNextPage. Eu quero que meu ViewModel sempre conheça os valores dessas duas propriedades View.

Eu imaginei que poderia fazer isso com uma binding de dados OneWayToSource:

 

Se isso fosse permitido, seria perfeito: sempre que a propriedade CanGoToNextPage do FlowDocumentPageViewer fosse alterada, o novo valor seria empurrado para baixo na propriedade NextPageAvailable do ViewModel, que é exatamente o que eu quero.

Infelizmente, isso não compila: recebo um erro dizendo que a propriedade ‘CanGoToPreviousPage’ é somente leitura e não pode ser definida a partir da marcação. Aparentemente, as propriedades somente leitura não suportam nenhum tipo de binding de dados, nem mesmo a binding de dados que é somente leitura em relação a essa propriedade.

Eu poderia fazer propriedades do meu ViewModel ser DependencyProperties e fazer uma binding OneWay indo para o outro lado, mas eu não sou louco sobre a violação de separação de preocupações (ViewModel precisaria de uma referência para o View, que binding de dados MVVM deve evitar ).

O FlowDocumentPageViewer não expõe um evento CanGoToNextPageChanged, e eu não conheço nenhuma maneira boa de obter notifications de alteração de um DependencyProperty, antes de criar outro DependencyProperty para vinculá-lo, o que parece ser um exagero aqui.

Como posso manter meu ViewModel informado sobre alterações nas propriedades somente leitura da visão?

Sim, eu fiz isso no passado com as propriedades ActualHeight e ActualHeight , ambas as quais são somente leitura. Eu criei um comportamento anexado que tem as propriedades anexadas ObservedHeight e ObservedHeight . Ele também possui uma propriedade Observe que é usada para fazer a conexão inicial. Uso parece com isso:

  

Portanto, o modelo de exibição possui propriedades Width e Height que estão sempre em sincronia com as propriedades anexadas ObservedHeight e ObservedHeight . A propriedade Observe simplesmente atribui ao evento SizeChanged do FrameworkElement . No identificador, atualiza suas propriedades ObservedHeight e ObservedHeight . Ergo, a Width e Height do modelo de exibição está sempre em sincronia com o ActualWidth e ActualHeight do UserControl .

Talvez não seja a solução perfeita (concordo - os DPs somente leitura devem suportar ligações OneWayToSource ), mas ela funciona e mantém o padrão MVVM. Obviamente, os DPs ObservedHeight e ObservedHeight não são somente leitura.

UPDATE: aqui está o código que implementa a funcionalidade descrita acima:

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

Eu uso uma solução universal que funciona não só com ActualWidth e ActualHeight, mas também com todos os dados que você pode ligar pelo menos no modo de leitura.

A marcação é assim, desde que ViewportWidth e ViewportHeight sejam propriedades do modelo de exibição

         

Aqui está o código-fonte dos 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 { } 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(); } } 

Se alguém estiver interessado, eu codifiquei uma aproximação da solução de Kent aqui:

 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 } 

Sinta-se à vontade para usá-lo em seus aplicativos. Isso funciona bem. (Obrigado Kent!)

Aqui está outra solução para este “bug” que eu escrevi aqui:
Ligação OneWayToSource para propriedade de dependência ReadOnly

Ele funciona usando duas propriedades de dependência, Listener e Mirror. Ouvinte é vinculado OneWay para o TargetProperty e no PropertyChangedCallback ele atualiza a propriedade Mirror que está vinculada OneWayToSource para o que foi especificado na vinculação. Eu chamo-lhe PushBinding e pode ser definido em qualquer Propriedade de Dependência somente leitura como esta

       

Baixe o projeto de demonstração aqui .
Ele contém o código fonte e o uso de amostras curtas, ou visita o meu blog WPF se você estiver interessado nos detalhes da implementação.

Uma última nota, desde o .NET 4.0, estamos ainda mais longe do suporte interno para isso, já que uma Ligação OneWayToSource lê o valor de volta da Fonte depois de ter sido atualizada.

Eu gosto da solução de Dmitry Tashkinov! No entanto, ele caiu meu VS no modo de design. É por isso que adicionei uma linha ao método OnSourceChanged:

     Vazio estático privado OnSourceChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
     {
         if (! ((bool) DesignerProperties.IsInDesignModeProperty.GetMetadata (typeof (DependencyObject)). DefaultValue))
             ((DataPipe) d). OnSourceChanged (e);
     }

Eu acho que isso pode ser feito um pouco mais simples:

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