Réactivation des propriétés de l’interface graphique en lecture seule dans ViewModel

Je souhaite écrire un ViewModel qui connaît toujours l’état actuel de certaines propriétés de dépendance en lecture seule à partir de la vue.

Plus précisément, mon interface graphique contient un FlowDocumentPageViewer, qui affiche une page à la fois à partir d’un FlowDocument. FlowDocumentPageViewer expose deux propriétés de dépendance en lecture seule appelées CanGoToPreviousPage et CanGoToNextPage. Je souhaite que mon ViewModel connaisse toujours les valeurs de ces deux propriétés View.

Je me suis dit que je pouvais le faire avec une liaison de données OneWayToSource:

 

Si cela était autorisé, ce serait parfait: chaque fois que la propriété CanGoToNextPage de FlowDocumentPageViewer changerait, la nouvelle valeur serait transférée dans la propriété NextPageAvailable du ViewModel, ce qui est exactement ce que je veux.

Malheureusement, cela ne comstack pas: je reçois une erreur en disant que la propriété ‘CanGoToPreviousPage’ est en lecture seule et ne peut pas être définie à partir du balisage. Apparemment, les propriétés en lecture seule ne prennent en charge aucun type de liaison de données, même pas la liaison de données en lecture seule par rapport à cette propriété.

Je pourrais faire en sorte que les propriétés de mon ViewModel soient DependencyProperties et rendre une liaison OneWay inverse, mais je ne suis pas fou de la violation de séparation des préoccupations (ViewModel nécessiterait une référence à View, que la liaison de données MVVM est censée éviter). ).

FlowDocumentPageViewer n’expose pas un événement CanGoToNextPageChanged, et je ne connais aucun moyen d’obtenir des notifications de modification à partir d’une DependencyProperty, à moins de créer une autre propriété DependencyProperty pour la lier, ce qui semble exagéré ici.

Comment puis-je garder mon ViewModel informé des modifications apscopes aux propriétés en lecture seule de la vue?

    Oui, je l’ai déjà fait avec les propriétés ActualWidth et ActualHeight , toutes deux en lecture seule. J’ai créé un comportement attaché doté des propriétés attachées ObservedWidth et ObservedHeight . Il possède également une propriété Observe qui est utilisée pour effectuer le raccordement initial. L’utilisation ressemble à ceci:

      

    Le modèle de vue a donc des propriétés Width et Height toujours synchronisées avec les propriétés attachées ObservedWidth et ObservedHeight . La propriété Observe s'attache simplement à l'événement SizeChanged du FrameworkElement . Dans le descripteur, il met à jour ses propriétés ObservedWidth et ObservedHeight . Ergo, la Width et la Height du modèle de vue sont toujours synchronisées avec ActualWidth et ActualHeight de UserControl .

    Peut-être pas la solution parfaite (je suis d'accord - les PDD en lecture seule devraient prendre en OneWayToSource liaisons OneWayToSource ), mais cela fonctionne et respecte le modèle MVVM. De toute évidence, les DP ObservedWidth et ObservedHeight ne sont pas en lecture seule.

    UPDATE: voici le code qui implémente les fonctionnalités décrites ci-dessus:

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

    J’utilise une solution universelle qui fonctionne non seulement avec ActualWidth et ActualHeight, mais également avec toutes les données que vous pouvez lier au moins en mode lecture.

    Le balisage ressemble à ceci, à condition que ViewportWidth et ViewportHeight soient des propriétés du modèle de vue

             

    Voici le code source des éléments personnalisés

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

    Si quelqu’un d’autre est intéressé, j’ai codé une approximation de la solution de Kent ici:

     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 } 

    N’hésitez pas à l’utiliser dans vos applications. Ça marche bien. (Merci Kent!)

    Voici une autre solution à ce “bug” dont j’ai parlé ici:
    OneWayToSource Binding pour la propriété ReadOnly Dependency

    Cela fonctionne en utilisant deux propriétés de dépendance, un écouteur et un miroir. L’écouteur est lié à OneWay à la propriété TargetProperty et dans la propriété PropertyChangedCallback, il met à jour la propriété Mirror qui est liée à OneWayToSource à tout ce qui a été spécifié dans la liaison. Je l’appelle PushBinding et il peut être défini sur n’importe quelle propriété de dépendance en lecture seule comme celle-ci

           

    Télécharger le projet de démonstration ici .
    Il contient du code source et des exemples d’utilisation courts, ou visitez mon blog WPF si vous êtes intéressé par les détails de l’implémentation.

    Une dernière remarque: depuis .NET 4.0, nous sums encore plus loin de la prise en charge intégrée, car une liaison OneWayToSource lit la valeur de la source après l’avoir mise à jour.

    J’aime la solution de Dmitry Tashkinov! Cependant, il a planté mon VS en mode conception. C’est pourquoi j’ai ajouté une ligne à la méthode OnSourceChanged:

         void statique privé OnSourceChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
         {
             if (! ((bool) DesignerProperties.IsInDesignModeProperty.GetMetadata (typeof (DependencyObject)). DefaultValue))
                 ((DataPipe) d) .OnSourceChanged (e);
         }
    

    Je pense que cela peut être fait un peu plus 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); } }