Repousser les propriétés de l'interface graphique en lecture seule dans ViewModel

124

Je veux é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 veux que mon ViewModel connaisse toujours les valeurs de ces deux propriétés de vue.

J'ai pensé que je pourrais 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 était modifiée, la nouvelle valeur serait poussée vers le bas dans la propriété NextPageAvailable de ViewModel, ce qui est exactement ce que je veux.

Malheureusement, cela ne compile pas: j'obtiens une erreur indiquant 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, pas même 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 créer une liaison OneWay dans l'autre sens, mais je ne suis pas fou de la violation de séparation des préoccupations (ViewModel aurait besoin d'une référence à la vue, que la liaison de données MVVM est censée éviter ).

FlowDocumentPageViewer n'expose pas un événement CanGoToNextPageChanged, et je ne connais aucun bon moyen 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?

Joe White
la source

Réponses:

152

Oui, j'ai fait cela dans le passé avec les propriétés ActualWidthand ActualHeight, qui sont toutes deux en lecture seule. J'ai créé un comportement attaché qui a ObservedWidthet des ObservedHeightpropriétés attachées. Il a également une Observepropriété qui est utilisée pour faire 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 d'affichage a Widthet Heightpropriétés qui sont toujours en phase avec les ObservedWidthet ObservedHeightpropriétés attachées. La Observepropriété s'attache simplement à l' SizeChangedévénement du FrameworkElement. Dans le handle, il met à jour ses propriétés ObservedWidthet ObservedHeight. Ergo, le Widthet Heightdu modèle de vue est toujours synchronisé avec le ActualWidthet ActualHeightdu UserControl.

Peut-être pas la solution parfaite (je suis d'accord - les DP en lecture seule devraient prendre en charge les OneWayToSourceliaisons), mais cela fonctionne et il maintient le modèle MVVM. De toute évidence, les DP ObservedWidthet neObservedHeight 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);
    }
}
Kent Boogaart
la source
2
Je me demande si vous pourriez faire une supercherie pour attacher automatiquement les propriétés, sans avoir besoin d'Observe. Mais cela ressemble à une bonne solution. Merci!
Joe White
1
Merci Kent. J'ai posté un exemple de code ci-dessous pour cette classe "SizeObserver".
Scott Whitlock
52
+1 à ce sentiment: "Les DP en lecture seule devraient prendre en charge les liaisons OneWayToSource"
Tristan
3
Peut-être encore mieux de créer une seule Sizepropriété, combinant hauteur et largeur. Environ. 50% de code en moins.
Gerard
1
@Gerard: Cela ne fonctionnera pas car il n'y a pas de ActualSizepropriété FrameworkElement. Si vous souhaitez une liaison directe des propriétés attachées, vous devez créer deux propriétés à lier respectivement à ActualWidthet ActualHeight.
dotNET
59

J'utilise une solution universelle qui fonctionne non seulement avec ActualWidth et ActualHeight, mais également avec toutes les données auxquelles vous pouvez vous 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

<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 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<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();
    }
}
Dmitry Tashkinov
la source
(via une réponse de user543564): Ce n'est pas une réponse mais un commentaire à Dmitry - J'ai utilisé votre solution et cela a très bien fonctionné. Belle solution universelle qui peut être utilisée de manière générique dans différents endroits. Je l'ai utilisé pour pousser certaines propriétés d'élément d'interface utilisateur (ActualHeight et ActualWidth) dans mon viewmodel.
Marc Gravell
2
Merci! Cela m'a aidé à me lier à une propriété normale. Malheureusement, la propriété n'a pas publié les événements INotifyPropertyChanged. J'ai résolu ce problème en attribuant un nom à la liaison DataPipe et en ajoutant ce qui suit à l'événement de modification des contrôles: BindingOperations.GetBindingExpressionBase (bindingName, DataPipe.SourceProperty) .UpdateTarget ();
chilltemp
3
Cette solution a bien fonctionné pour moi. Mon seul ajustement était de définir BindsTwoWayByDefault sur true pour le FrameworkPropertyMetadata sur le TargetProperty DependencyProperty.
Hasani Blackwell
1
Le seul reproche à propos de cette solution semble être qu'elle rompt l'encapsulation propre, car la Targetpropriété doit être rendue inscriptible même si elle ne doit pas être modifiée de l'extérieur: - /
OR Mapper
Pour ceux qui préféreraient le package NuGet au copier-coller du code: j'ai ajouté DataPipe à ma bibliothèque open source JungleControls. Consultez la documentation DataPipe .
Robert Važan
21

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

Scott Whitlock
la source
10

Voici une autre solution à ce «bug» dont j'ai parlé ici:
OneWayToSource Binding for ReadOnly Dependency Property

Cela fonctionne en utilisant deux propriétés de dépendance, Listener et Mirror. Listener est lié OneWay à TargetProperty et dans PropertyChangedCallback il met à jour la propriété Mirror qui est liée OneWayToSource à ce qui a été spécifié dans la liaison. Je l'appelle PushBindinget 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 court exemple d'utilisation, ou visitez mon blog WPF si vous êtes intéressé par les détails de l'implémentation.

Une dernière note, depuis .NET 4.0, nous sommes encore plus éloignés de la prise en charge intégrée de cela, car une liaison OneWayToSource lit la valeur à partir de la source après l'avoir mise à jour

Fredrik Hedblad
la source
Les réponses sur Stack Overflow doivent être entièrement autonomes. C'est bien d'inclure un lien vers des références externes facultatives, mais tout le code requis pour la réponse doit être inclus dans la réponse elle-même. Veuillez mettre à jour votre question afin qu'elle puisse être utilisée sans visiter aucun autre site Web.
Peter Duniho le
4

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:

    private static void OnSourceChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (! ((bool) DesignerProperties.IsInDesignModeProperty.GetMetadata (typeof (DependencyObject)). DefaultValue))
            ((DataPipe) d) .OnSourceChanged (e);
    }
Dariusz Wasacz
la source
0

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);
  }
}
eriksmith200
la source
2
Peut-être un peu plus simple, mais si je le lis bien, cela ne permet qu'une seule liaison de ce type sur l'élément. Je veux dire, je pense qu'avec cette approche, vous ne pourrez pas lier à la fois ActualWidth et ActualHeight. Juste l'un d'entre eux.
quetzalcoatl