it-swarm.com.de

Schreibgeschützte GUI-Eigenschaften in ViewModel zurückschieben

Ich möchte ein ViewModel schreiben, das den aktuellen Status einiger schreibgeschützter Abhängigkeitseigenschaften immer 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 dem Namen CanGoToPreviousPage und CanGoToNextPage zur Verfügung. Mein ViewModel soll immer die Werte dieser beiden View-Eigenschaften kennen.

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

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

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

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

Ich könnte meine ViewModel-Eigenschaften zu DependencyProperties machen und eine OneWay-Bindung in die andere Richtung machen, aber ich bin nicht verrückt nach der Verletzung der Separation von Bedenken (ViewModel würde einen Verweis auf die View benötigen, die MVVM-Datenbindung vermeiden soll ).

FlowDocumentPageViewer macht kein CanGoToNextPageChanged-Ereignis verfügbar, und ich kenne keine gute Möglichkeit, Änderungsbenachrichtigungen von einer DependencyProperty zu erhalten, kurz bevor eine andere DependencyProperty erstellt wird, an die es gebunden werden kann, was hier wie ein Overkill erscheint.

Wie kann ich mein ViewModel über Änderungen an den schreibgeschützten Eigenschaften der Ansicht informieren?

117
Joe White

Ja, ich habe dies in der Vergangenheit mit den Eigenschaften ActualWidth und ActualHeight gemacht, die beide schreibgeschützt sind. Ich habe ein angefügtes Verhalten erstellt, das die Eigenschaften ObservedWidth und ObservedHeight angehängt hat. Es hat auch eine Observe -Eigenschaft, die für den ersten Verbindungsaufbau verwendet wird. Die Nutzung sieht so aus:

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

Das Ansichtsmodell verfügt also über die Eigenschaften Width und Height, die immer mit den angefügten Eigenschaften ObservedWidth und ObservedHeight übereinstimmen. Die Observe -Eigenschaft hängt einfach mit dem SizeChanged -Ereignis der FrameworkElement zusammen. Im Handle werden die Eigenschaften ObservedWidth und ObservedHeight aktualisiert. Daher sind die Width und Height des Ansichtsmodells immer synchron mit den ActualWidth und ActualHeight der UserControl.

Vielleicht nicht die perfekte Lösung (ich stimme zu - schreibgeschützte DPs sollten unterstützen OneWayToSource Bindungen), aber es funktioniert und es hält das MVVM-Muster. Offensichtlich sind die DPs ObservedWidth und ObservedHeightnicht nur lesbar.

UPDATE: Hier ist der 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);
    }
}
143
Kent Boogaart

Ich verwende eine universelle Lösung, die nicht nur mit ActualWidth und ActualHeight funktioniert, sondern auch mit allen Daten, an die Sie sich zumindest im Lesemodus anschließen 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();
    }
}
53

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

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 läuft gut. (Danke Kent!)

20
Scott Whitlock

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. Listener ist OneWay an die TargetProperty gebunden und aktualisiert im PropertyChangedCallback die Mirror-Eigenschaft, bei der OneWayToSource gebunden ist, mit den Angaben in der Bindung. Ich nenne es PushBinding und kann auf jede schreibgeschützte Abhängigkeitseigenschaft wie diese gesetzt 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>

Demo-Projekt hier herunterladen .
Sie enthält Quellcode und kurze Beispielanwendungen. Besuchen Sie mein WPF-Blog , wenn Sie an den Implementierungsdetails interessiert sind.

Noch eine letzte Anmerkung, seit .NET 4.0 sind wir noch weiter von der integrierten Unterstützung entfernt, da ein OneWayToSource Binding den Wert nach der Aktualisierung aus der Quelle liest.

10
Fredrik Hedblad

Ich mag die Lösung von Dmitry Tashkinov! Jedoch stürzte mein VS im Entwurfsmodus 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); 
 } 
4
Dariusz Wasacz

Ich denke, es geht etwas einfacher:

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