133 votes

Repousser les propriétés GUI en lecture seule dans ViewModel

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

Plus précisément, mon interface graphique contient un FlowDocumentPageViewer, qui affiche une page à la fois 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 un lien de données OneWayToSource :

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

Si cela était autorisé, ce serait parfait : chaque fois que la propriété CanGoToNextPage du FlowDocumentPageViewer change, la nouvelle valeur serait poussée vers le bas dans la propriété NextPageAvailable du ViewModel, ce qui est exactement ce que je veux.

Malheureusement, ça 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 la balise. Apparemment, les propriétés en lecture seule ne supportent pas tout de liaison de données, pas même de 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 des DependencyProperties, et créer une liaison unidirectionnelle dans l'autre sens, mais je ne suis pas convaincu par la violation de la séparation des préoccupations (le ViewModel aurait besoin d'une référence à la vue, ce que la liaison de données MVVM est censée éviter).

FlowDocumentPageViewer n'expose pas d'événement CanGoToNextPageChanged, et je ne connais pas de bon moyen d'obtenir des notifications de changement à partir d'une DependencyProperty, à moins de créer une autre DependencyProperty à laquelle la 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 ?

163voto

Kent Boogaart Points 97432

Oui, j'ai déjà fait cela dans le passé avec la ActualWidth y ActualHeight qui sont toutes deux en lecture seule. J'ai créé un comportement joint qui a ObservedWidth y ObservedHeight propriétés attachées. Il dispose également d'un Observe qui est utilisée pour effectuer le branchement initial. L'utilisation ressemble à ceci :

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

Donc le modèle de vue a Width y Height qui sont toujours en synchronisation avec le ObservedWidth y ObservedHeight propriétés attachées. Le site Observe s'attache simplement à la propriété SizeChanged de l'événement FrameworkElement . Dans la poignée, il met à jour son ObservedWidth y ObservedHeight propriétés. Ergo, le Width y Height du modèle de vue est toujours en synchronisation avec la ActualWidth y ActualHeight de la UserControl .

Ce n'est peut-être pas la solution idéale (je suis d'accord - les PD en lecture seule). debe soutien OneWayToSource ), mais cela fonctionne et respecte le modèle MVVM. De toute évidence, le ObservedWidth y ObservedHeight Les PD sont no en lecture seule.

MISE À JOUR : 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);
    }
}

2 votes

Je me demande si vous ne pourriez pas faire une astuce pour attacher automatiquement les propriétés, sans avoir besoin d'Observe. Mais cela semble être une bonne solution. Merci.

1 votes

Merci Kent. J'ai posté un exemple de code ci-dessous pour cette classe "SizeObserver".

60 votes

+1 à ce sentiment : "les PD en lecture seule devraient supporter les liaisons OneWayToSource"

65voto

Dmitry Tashkinov Points 942

J'utilise une solution universelle qui fonctionne non seulement avec ActualWidth et ActualHeight, mais aussi 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();
    }
}

0 votes

(via une réponse de l'utilisateur543564) : 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ée pour pousser certaines propriétés d'éléments d'interface utilisateur (ActualHeight et ActualWidth) dans mon viewmodel.

2 votes

Merci ! Cela m'a aidé à lier une propriété normale (get only). Malheureusement, la propriété ne publiait pas d'é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 changed des contrôles : BindingOperations.GetBindingExpressionBase(bindingName, DataPipe.SourceProperty).UpdateTarget() ;

3 votes

Cette solution a bien fonctionné pour moi. Ma seule modification a été de définir BindsTwoWayByDefault à true pour les FrameworkPropertyMetadata sur le TargetProperty DependencyProperty.

20voto

Scott Whitlock Points 8172

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. Il fonctionne bien. (Merci Kent !)

10voto

Fredrik Hedblad Points 42772

Voici une autre solution à ce "bug" dont j'ai parlé ici :
Liaison OneWayToSource pour la propriété de dépendance en lecture seule

Il fonctionne en utilisant deux propriétés de dépendance, Listener et Mirror. Le Listener est lié de manière univoque à la propriété TargetProperty et dans le PropertyChangedCallback, il met à jour la propriété Mirror qui est liée de manière univoque à la source de ce qui a été spécifié dans le Binding. Je l'appelle PushBinding et elle peut être définie sur n'importe quelle propriété de dépendance en lecture seule, comme ceci

<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écharger le projet de démonstration ici .
Il contient le code source et un court exemple d'utilisation.

Enfin, depuis la version 4.0 de .NET, nous sommes encore plus éloignés de la prise en charge intégrée de cette fonction, puisqu'un fichier de type La liaison OneWayToSource relit la valeur de la source après l'avoir mise à jour.

1 votes

Les réponses sur Stack Overflow doivent être entièrement autonomes. Il est possible d'inclure un lien vers des références externes facultatives, mais tout le code nécessaire à 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 avoir à visiter un autre site Web.

0 votes

Cela ressemble à de la publicité pour votre blog plutôt qu'à une réponse.

0 votes

Oui, vous avez tous les deux raison, ça fait un peu publicité, même si ce n'était pas mon intention. Après avoir publié cet article de blog, j'ai répondu à toutes les questions qui concernaient la liaison OneWayToSource pour les propriétés en lecture seule. Je ne suis pas vraiment sûr de ce qu'il faut ajouter à la réponse, elle contient un exemple de code sur la façon de le faire et un lien vers le projet de démonstration qui n'est pas sur mon blog. Je vais le nettoyer un peu :-)

5voto

Dariusz Wasacz Points 121

J'aime la solution de Dmitry Tashkinov ! Cependant, elle a fait planter 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);
    }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X