web-dev-qa-db-fra.com

Réinitialisation des propriétés d'interface graphique en lecture seule dans ViewModel

Je veux écrire un ViewModel qui connaisse 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 de View.

Je pensais pouvoir le faire avec une liaison de données OneWayToSource:

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

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

Malheureusement, cela ne compile pas: j'obtiens une erreur disant La propriété 'CanGoToPreviousPage' est en lecture seule et ne peut pas être définie à partir de balises. Apparemment, les propriétés en lecture seule ne prennent pas en charge le type de liaison de données any, pas même la liaison de données en lecture seule par rapport à cette propriété.

Je pourrais transformer les propriétés de mon ViewModel en propriétés DependencyProperties et créer une liaison OneWay dans le sens inverse, mais je ne suis pas folle de la violation de séparation des problèmes (ViewModel aurait besoin d'une référence à View, ce que la liaison de données MVVM est supposée éviter ).

FlowDocumentPageViewer n'expose pas un événement CanGoToNextPageChanged et je ne connais aucun moyen efficace d'obtenir des notifications de modification d'un DependencyProperty, à moins de créer un autre DependencyProperty auquel le lier, ce qui semble excessif ici.

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

117
Joe White

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

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

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

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

UPDATE: voici le code qui implémente la fonctionnalité décrite 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);
    }
}
143
Kent Boogaart

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

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

<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>

Voici le code source pour les é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<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();
    }
}
53
Dmitry Tashkinov

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

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!)

20
Scott Whitlock

Voici une autre solution à ce "bug" sur lequel j'ai blogué ici:
OneWayToSource Binding for ReadOnly Dependency Property

Il fonctionne en utilisant deux propriétés de dépendance, Listener et Mirror. Le programme d'écoute est lié OneWay à TargetProperty et, dans le 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.

<TextBlock Name="myTextBlock"
           Background="LightBlue">
    <pb:PushBindingManager.PushBindings>
        <pb:PushBinding TargetProperty="ActualHeight" Path="Height"/>
        <pb:PushBinding TargetProperty="ActualWidth" Path="Width"/>
    </pb:PushBindingManager.PushBindings>
</TextBlock>

Téléchargez le projet de démonstration ici .
Il contient le code source et un exemple d'utilisation, ou visitez le site mon blog WPF si les détails de la mise en oeuvre vous intéressent.

Une dernière remarque, depuis .NET 4.0, nous sommes encore plus éloignés du support intégré pour cela, puisqu'un OneWayToSource Binding lit la valeur de la source après l'avoir mise à jour

10
Fredrik Hedblad

J'aime la solution de Dmitry Tashkinov! Cependant, mon VS s'est écrasé en mode création. 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); 
 } 
4
Dariusz Wasacz

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