Verschieben schreibgeschützter GUI-Eigenschaften zurück in ViewModel

124

Ich möchte ein ViewModel schreiben, das immer den aktuellen Status einiger schreibgeschützter Abhängigkeitseigenschaften aus der Ansicht kennt.

Insbesondere enthält meine GUI einen FlowDocumentPageViewer, der jeweils eine Seite aus einem FlowDocument anzeigt. FlowDocumentPageViewer stellt zwei schreibgeschützte Abhängigkeitseigenschaften mit den Namen CanGoToPreviousPage und CanGoToNextPage bereit. Ich möchte, dass mein ViewModel immer die Werte dieser beiden View-Eigenschaften kennt.

Ich dachte, ich könnte dies mit einer OneWayToSource-Datenbindung tun:

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

Wenn dies zulässig wäre, wäre es perfekt: Wenn sich die CanGoToNextPage-Eigenschaft von FlowDocumentPageViewer ändert, wird der neue Wert in die NextPageAvailable-Eigenschaft von ViewModel verschoben, genau das, was ich möchte.

Leider wird dies nicht kompiliert: Ich erhalte die Fehlermeldung, dass die Eigenschaft 'CanGoToPreviousPage' schreibgeschützt ist und nicht über das Markup festgelegt werden kann. Offenbar schreibgeschützte Eigenschaften nicht unterstützen jede Art von Datenbindung, nicht einmal , dass die Datenbindung schreibgeschützt in Bezug auf diese Eigenschaft.

Ich könnte festlegen, dass die Eigenschaften meines ViewModels DependencyProperties sind, und eine OneWay-Bindung in die andere Richtung erstellen, aber ich bin nicht verrückt nach der Verletzung der Trennung von Bedenken (ViewModel würde einen Verweis auf die Ansicht benötigen, den die MVVM-Datenbindung vermeiden soll ).

FlowDocumentPageViewer macht kein CanGoToNextPageChanged-Ereignis verfügbar, und ich kenne keine gute Möglichkeit, Änderungsbenachrichtigungen von einer DependencyProperty abzurufen, ohne eine andere DependencyProperty zu erstellen, an die sie gebunden werden kann, was hier wie ein Overkill erscheint.

Wie kann ich mein ViewModel über Änderungen an den schreibgeschützten Eigenschaften der Ansicht auf dem Laufenden halten?

Joe White
quelle

Antworten:

151

Ja, ich habe dies in der Vergangenheit mit den Eigenschaften ActualWidthund getan ActualHeight, die beide schreibgeschützt sind. Ich habe ein angebracht Verhalten , das hat ObservedWidthund ObservedHeightEigenschaften angebracht. Es hat auch eine ObserveEigenschaft, die für die anfängliche Verbindung verwendet wird. Die Verwendung sieht folgendermaßen aus:

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

So ist das View - Modell hat Widthund HeightEigenschaften, die immer synchron mit den ObservedWidthund ObservedHeightangefügten Eigenschaften. Die ObserveEigenschaft wird einfach an das SizeChangedEreignis des angehängt FrameworkElement. Im Handle werden seine ObservedWidthund ObservedHeightEigenschaften aktualisiert . Ergo ist das Widthund Heightdes Ansichtsmodells immer synchron mit dem ActualWidthund ActualHeightdes UserControl.

Vielleicht nicht die perfekte Lösung (Ich bin einverstanden - read-only DPs sollte unterstützen OneWayToSourceBindungen), aber es funktioniert und es bestätigt das MVVM - Muster. Offensichtlich sind die ObservedWidthund ObservedHeightDPs nicht schreibgeschützt.

UPDATE: Hier ist Code, der die oben beschriebene Funktionalität implementiert:

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
quelle
2
Ich frage mich, ob Sie einige Tricks ausführen könnten, um die Eigenschaften automatisch anzuhängen, ohne Observe zu benötigen. Aber das sieht nach einer guten Lösung aus. Vielen Dank!
Joe White
1
Danke Kent. Ich habe unten ein Codebeispiel für diese "SizeObserver" -Klasse veröffentlicht.
Scott Whitlock
52
+1 zu diesem Gefühl: "Nur-Lese-DPs sollten OneWayToSource-Bindungen unterstützen"
Tristan
3
Vielleicht sogar noch besser, nur eine SizeEigenschaft zu erstellen , die Höhe und Breite kombiniert. Ca. 50% weniger Code.
Gerard
1
@ Gerard: Das wird nicht funktionieren, weil es keine ActualSizeEigenschaft in gibt FrameworkElement. Wenn Sie direkt wollen die angefügten Eigenschaften zu binden, müssen Sie zwei Eigenschaften erstellen , um gebunden zu sein ActualWidthund ActualHeightjeweils.
dotNET
58

Ich verwende eine universelle Lösung, die nicht nur mit ActualWidth und ActualHeight funktioniert, sondern auch mit allen Daten, an die Sie zumindest im Lesemodus binden können.

Das Markup sieht folgendermaßen aus, sofern ViewportWidth und ViewportHeight Eigenschaften des Ansichtsmodells sind

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

Hier ist der Quellcode für die benutzerdefinierten Elemente

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
quelle
(über eine Antwort von user543564): Dies ist keine Antwort, sondern ein Kommentar an Dmitry - ich habe Ihre Lösung verwendet und sie hat großartig funktioniert. Schöne universelle Lösung, die generisch an verschiedenen Orten eingesetzt werden kann. Ich habe es verwendet, um einige UI-Elementeigenschaften (ActualHeight und ActualWidth) in mein Ansichtsmodell zu verschieben.
Marc Gravell
2
Vielen Dank! Dies hat mir geholfen, mich an eine normale Get Only-Eigenschaft zu binden. Leider hat die Eigenschaft keine INotifyPropertyChanged-Ereignisse veröffentlicht. Ich habe dieses Problem gelöst, indem ich der DataPipe-Bindung einen Namen zugewiesen und dem geänderten Steuerelementereignis Folgendes hinzugefügt habe: BindingOperations.GetBindingExpressionBase (bindingName, DataPipe.SourceProperty) .UpdateTarget ();
Chilltemp
3
Diese Lösung hat bei mir gut funktioniert. Meine einzige Optimierung bestand darin, BindsTwoWayByDefault für die FrameworkPropertyMetadata in der TargetProperty DependencyProperty auf true zu setzen.
Hasani Blackwell
1
Der einzige Kritikpunkt an dieser Lösung scheint zu sein, dass sie die saubere Kapselung unterbricht, da die TargetEigenschaft beschreibbar gemacht werden muss, obwohl sie nicht von außen geändert werden darf: - /
OR Mapper
Für diejenigen, die das NuGet-Paket dem Kopieren und Einfügen des Codes vorziehen möchten: Ich habe DataPipe zu meiner OpenSource-JungleControls-Bibliothek hinzugefügt. Siehe DataPipe-Dokumentation .
Robert Važan
21

Wenn jemand anderes interessiert ist, habe ich hier eine Annäherung an Kents Lösung codiert:

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
}

Fühlen Sie sich frei, es in Ihren Apps zu verwenden. Es funktioniert gut. (Danke Kent!)

Scott Whitlock
quelle
10

Hier ist eine weitere Lösung für diesen "Fehler", über den ich hier gebloggt habe :
OneWayToSource-Bindung für ReadOnly-Abhängigkeitseigenschaft

Es funktioniert mit zwei Abhängigkeitseigenschaften, Listener und Mirror. Der Listener ist OneWay an die TargetProperty gebunden und aktualisiert im PropertyChangedCallback die Mirror-Eigenschaft, die OneWayToSource an die in der Bindung angegebenen Werte bindet. Ich nenne es PushBindingund es kann für jede schreibgeschützte Abhängigkeitseigenschaft wie diese festgelegt werden

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

Laden Sie hier das Demo-Projekt herunter .
Es enthält Quellcode und kurze Beispielnutzung oder besuchen Sie meinen WPF-Blog, wenn Sie an den Implementierungsdetails interessiert sind.

Ein letzter Hinweis: Seit .NET 4.0 sind wir noch weiter von der integrierten Unterstützung entfernt, da eine OneWayToSource-Bindung den Wert aus der Quelle zurückliest, nachdem sie aktualisiert wurde

Fredrik Hedblad
quelle
Die Antworten zum Stapelüberlauf sollten vollständig in sich geschlossen sein. Es ist in Ordnung, einen Link zu optionalen externen Referenzen einzufügen, aber der gesamte für die Antwort erforderliche Code sollte in der Antwort selbst enthalten sein. Bitte aktualisieren Sie Ihre Frage, damit sie verwendet werden kann, ohne eine andere Website zu besuchen.
Peter Duniho
4

Ich mag die Lösung von Dmitry Tashkinov! Allerdings stürzte mein VS im Designmodus ab. Aus diesem Grund habe ich der OnSourceChanged-Methode eine Zeile hinzugefügt:

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

Ich denke, es kann ein bisschen einfacher gemacht werden:

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
quelle
2
Mag ein bisschen einfacher sein, aber wenn ich es gut lese, erlaubt es nur eine solche Bindung an das Element. Ich meine, ich denke, dass Sie mit diesem Ansatz nicht in der Lage sind, sowohl ActualWidth als auch ActualHeight zu binden . Nur einer von ihnen.
Quetzalcoatl