Invio delle proprietà della GUI di sola lettura in ViewModel


124

Voglio scrivere un ViewModel che conosca sempre lo stato corrente di alcune proprietà di dipendenza di sola lettura dalla vista.

In particolare, la mia GUI contiene un FlowDocumentPageViewer, che visualizza una pagina alla volta da un FlowDocument. FlowDocumentPageViewer espone due proprietà di dipendenza di sola lettura denominate CanGoToPreviousPage e CanGoToNextPage. Voglio che il mio ViewModel conosca sempre i valori di queste due proprietà View.

Ho pensato di poterlo fare con un database di OneWayToSource:

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

Se ciò fosse consentito, sarebbe perfetto: ogni volta che cambiava la proprietà CanGoToNextPage di FlowDocumentPageViewer, il nuovo valore veniva inserito nella proprietà NextPageAvailable di ViewModel, che è esattamente quello che voglio.

Sfortunatamente, questo non viene compilato: viene visualizzato un errore che dice che la proprietà 'CanGoToPreviousPage' è di sola lettura e non può essere impostata dal markup. Le proprietà apparentemente di sola lettura non supportano alcun tipo di associazione di dati, nemmeno di associazione di sola lettura rispetto a quella proprietà.

Potrei fare in modo che le proprietà del mio ViewModel siano DependencyProperties, e fare un legame OneWay andando dall'altra parte, ma non vado pazzo per la violazione della separazione delle preoccupazioni (ViewModel avrebbe bisogno di un riferimento a View, che si suppone che il database MVVM evitasse ).

FlowDocumentPageViewer non espone un evento CanGoToNextPageChanged e non conosco alcun buon modo per ottenere notifiche di modifica da una DependencyProperty, a parte la creazione di un'altra DependencyProperty a cui associarlo, che qui sembra eccessivo.

Come posso mantenere ViewModel informato delle modifiche alle proprietà di sola lettura della vista?

Risposte:


152

Sì, l'ho fatto in passato con le proprietà ActualWidthe ActualHeight, entrambe di sola lettura. Ho creato un comportamento collegato che ha ObservedWidthe ObservedHeightattaccato proprietà. Ha anche una Observeproprietà che viene utilizzata per eseguire il collegamento iniziale. L'utilizzo è simile al seguente:

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

Quindi il modello di vista ha Widthe Heightproprietà che sono sempre in sintonia con le ObservedWidthe ObservedHeightannessi proprietà. La Observeproprietà si collega semplicemente SizeChangedall'evento del FrameworkElement. Nella maniglia, aggiorna le sue ObservedWidthe le ObservedHeightproprietà. Ergo, il Widthe Heightdel modello di visualizzazione è sempre in sincronia con il ActualWidthe ActualHeightdel UserControl.

Forse non è la soluzione perfetta (sono d'accordo - i DP di sola lettura dovrebbero supportare i OneWayToSourcebinding), ma funziona e sostiene il modello MVVM. Ovviamente, i DP ObservedWidthe nonObservedHeight sono di sola lettura.

AGGIORNAMENTO: ecco il codice che implementa la funzionalità sopra descritta:

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
Mi chiedo se potresti fare qualche trucco per attaccare automaticamente le proprietà, senza bisogno di Osservare. Ma questa sembra un'ottima soluzione. Grazie!
Joe White,

1
Grazie Kent. Ho pubblicato un esempio di codice di seguito per questa classe "SizeObserver".
Scott Whitlock,

52
+1 a questo sentimento: "i DP di sola lettura dovrebbero supportare i collegamenti OneWayToSource"
Tristan

3
Forse anche meglio crearne solo uno Size proprietà, combinando Heigth e Width. Circa. 50% di codice in meno.
Gerard,

1
@Gerard: non funzionerà perché non ci sono ActualSizeproprietà FrameworkElement. Se si desidera l'associazione diretta delle proprietà associate, è necessario creare due proprietà da associare ActualWidthe ActualHeightrispettivamente.
dotNET il

58

Uso una soluzione universale che funziona non solo con ActualWidth e ActualHeight, ma anche con tutti i dati che è possibile associare almeno in modalità di lettura.

Il markup è simile al seguente, a condizione che ViewportWidth e ViewportHeight siano proprietà del modello di visualizzazione

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

Ecco il codice sorgente per gli elementi personalizzati

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();
    }
}

(tramite una risposta dell'utente543564): Questa non è una risposta ma un commento a Dmitry: ho usato la tua soluzione e ha funzionato alla grande. Bella soluzione universale che può essere utilizzata genericamente in luoghi diversi. L'ho usato per inserire alcune proprietà dell'elemento dell'interfaccia utente (ActualHeight e ActualWidth) nel mio modello di visualizzazione.
Marc Gravell

2
Grazie! Questo mi ha aiutato a legare ad un normale ottenere solo proprietà. Purtroppo la proprietà non ha pubblicato eventi INotifyPropertyChanged. Ho risolto questo assegnando un nome all'associazione DataPipe e aggiungendo quanto segue all'evento modificato controlli: BindingOperations.GetBindingExpressionBase (bindingName, DataPipe.SourceProperty) .UpdateTarget ();
Chilltemp,

3
Questa soluzione ha funzionato bene per me. La mia unica modifica è stata l'impostazione di BindsTwoWayByDefault su true per FrameworkPropertyMetadata su TargetProperty DependencyProperty.
Hasani Blackwell,

1
L'unico aspetto negativo di questa soluzione sembra essere la rottura dell'incapsulamento pulito, poiché la Targetproprietà deve essere resa scrivibile anche se non deve essere modificata dall'esterno: - /
OR Mapper

Per coloro che preferirebbero il pacchetto NuGet piuttosto che copiare e incollare il codice: ho aggiunto DataPipe alla mia libreria JungleControls opensource. Vedi la documentazione di DataPipe .
Robert Važan,

21

Se qualcun altro è interessato, ho codificato un'approssimazione della soluzione di Kent qui:

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
}

Sentiti libero di usarlo nelle tue app. Funziona bene. (Grazie Kent!)


10

Ecco un'altra soluzione a questo "bug" di cui ho scritto un blog qui:
OneWayToSource Binding per proprietà di dipendenza ReadOnly

Funziona utilizzando due proprietà di dipendenza, Listener e Mirror. Il listener è associato OneWay a TargetProperty e in PropertyChangedCallback aggiorna la proprietà Mirror che è associata OneWayToSource a tutto ciò che è stato specificato nel Binding. Lo chiamo PushBindinge può essere impostato su qualsiasi proprietà di dipendenza di sola lettura come questa

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

Scarica Demo Project qui .
Contiene il codice sorgente e un breve esempio di utilizzo, oppure visita il mio blog WPF se sei interessato ai dettagli di implementazione.

Un'ultima nota, dal momento che .NET 4.0 siamo ancora più lontani dal supporto integrato per questo, dal momento che un OneWayToSource Binding legge il valore indietro da Source dopo averlo aggiornato


Le risposte su Stack Overflow devono essere completamente autonome. Va bene includere un collegamento a riferimenti esterni opzionali, ma tutto il codice richiesto per la risposta dovrebbe essere incluso nella risposta stessa. Aggiorna la tua domanda in modo che possa essere utilizzata senza visitare altri siti Web.
Peter Duniho,

4

Mi piace la soluzione di Dmitry Tashkinov! Tuttavia ha bloccato il mio VS in modalità progettazione. Ecco perché ho aggiunto una riga al metodo OnSourceChanged:

    void statico privato OnSourceChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (! ((bool) DesignerProperties.IsInDesignModeProperty.GetMetadata (typeof (DependencyObject)). DefaultValue))
            ((DataPipe) d) .OnSourceChanged (e);
    }

0

Penso che possa essere fatto un po 'più semplice:

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

2
Può essere un po 'più semplice, ma se lo leggo bene, consente solo un tale legame sull'elemento. Voglio dire, penso che con questo approccio, non sarai in grado di associare sia ActualWidth che ActualHeight. Solo uno di loro.
quetzalcoatl,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.