2009-07-05 19 views
106

Desidero scrivere un ViewModel che conosca sempre lo stato corrente di alcune proprietà di dipendenza di sola lettura dalla vista.Riattivazione delle proprietà della GUI di sola lettura in ViewModel

In particolare, la 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 che avrei potuto fare questo con un'associazione dati OneWayToSource:

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

Se questo è stato permesso, sarebbe perfetto: ogni volta di proprietà CanGoToNextPage del FlowDocumentPageViewer cambiato, il nuovo valore sarebbe ottenere spinto verso il basso nella NextPageAvailable del ViewModel proprietà, che è esattamente quello che voglio.

Sfortunatamente, questo non viene compilato: viene visualizzato un messaggio di errore che indica la proprietà "CanGoToPreviousPage" è di sola lettura e non può essere impostato dal markup. Le proprietà apparentemente in sola lettura non supportano alcun tipo di databinding, nemmeno il databinding che è di sola lettura rispetto a quella proprietà.

Potrei rendere le proprietà del mio ViewModel DependencyProperties e fare un binding OneWay andando nell'altro senso, ma non mi fa impazzire la violazione della separazione delle preoccupazioni (ViewModel avrebbe bisogno di un riferimento alla vista, che MVVM databinding dovrebbe evitare).

FlowDocumentPageViewer non espone un evento CanGoToNextPageChanged e non conosco alcun metodo valido per ottenere notifiche di modifica da DependencyProperty, a meno di creare un'altra DependencyProperty per associarlo a, che sembra eccessivo.

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

risposta

129

Sì, l'ho fatto in passato con le proprietà ActualWidth e ActualHeight, entrambe sono di sola lettura. Ho creato un comportamento allegato che ha proprietà allegate ObservedWidth e ObservedHeight. Ha anche una proprietà Observe che viene utilizzata per eseguire l'hook-up iniziale. Uso assomiglia a questo:

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

Così il modello di vista ha Width e Height proprietà che sono sempre in sincronia con le proprietà associate ObservedWidth e ObservedHeight. La proprietà Observe si collega semplicemente all'evento SizeChanged dello FrameworkElement. Nell'impugnatura, aggiorna le sue proprietà ObservedWidth e ObservedHeight. Ergo, Width e Height del modello di visualizzazione è sempre sincronizzato con ActualWidth e ActualHeight di UserControl.

Forse non è la soluzione perfetta (sono d'accordo - di sola lettura PS dovrebbe supporto OneWayToSource attacchi), ma funziona e che sostiene il modello MVVM. Ovviamente, le ObservedWidth e ObservedHeight DP sono non di sola lettura.

UPDATE: 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 potessi fare qualche trucco per collegare automaticamente le proprietà, senza bisogno di osservare. Ma questa sembra una soluzione eccellente. Grazie! –

+1

Grazie Kent. Ho pubblicato un esempio di codice qui sotto per questa classe "SizeObserver". –

+42

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

20

Se qualcun altro è interessato, ho codificato da 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 
} 

Sentitevi liberi di usarlo nelle tue app. Funziona bene. (Grazie Kent!)

49

Io 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 a questo, a condizione ViewportWidth e ViewportHeight sono proprietà del modello vista

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

(tramite una risposta dall'utente543564): Questa non è una risposta, ma un commento a Dmitry - Ho usato la tua soluzione e ha funzionato benissimo. Bella soluzione universale che può essere utilizzata genericamente in luoghi diversi. L'ho usato per spingere alcune proprietà dell'elemento ui (ActualHeight e ActualWidth) nel mio viewmodel. –

+2

Grazie! Questo mi ha aiutato a legare a una normale proprietà get only. Sfortunatamente la proprietà non ha pubblicato eventi INotifyPropertyChanged. Ho risolto questo problema assegnando un nome al collegamento DataPipe e aggiungendo quanto segue all'evento modificato dei 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. –

9

Ecco un'altra soluzione a questo " bug "di cui ho bloggato qui:
OneWayToSource Binding for ReadOnly Dependency Property

Funziona utilizzando t wo Proprietà di dipendenza, listener e mirror. Il Listener è associato OneWay alla TargetProperty e nella proprietà PropertyChangedCallback aggiorna la proprietà Mirror che è associata a OneWayToSource a qualsiasi cosa specificata nel Binding. Io lo chiamo PushBinding e può essere impostato su qualsiasi sola lettura dipendenza proprietà 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> 

Download Demo Project Here.
Contiene codice sorgente e breve utilizzo del campione oppure visita my WPF blog se sei interessato ai dettagli dell'implementazione.

Un'ultima nota, dal momento NET 4.0 siamo ancora più lontano dal built-in-supporto per questo, dal momento che un OneWayToSource Binding reads the value back from the Source after it has updated it

4

Mi piace la soluzione di Dmitry Tashkinov! Tuttavia, si è verificato un crash del VS in modalità progettazione. È per questo che ho aggiunto una linea di metodo di OnSourceChanged:

 
    private static void 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); 
    } 
} 
+0

Potrebbe 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 legare sia ActualWidth ** che ** ActualHeight. Solo uno di loro. – quetzalcoatl

Problemi correlati