2009-07-05 56 views
106

我想寫一個ViewModel,它始終知道視圖中某些只讀依賴項屬性的當前狀態。將只讀GUI屬性返回到ViewModel

具體來說,我的GUI包含一個FlowDocumentPageViewer,它每次從FlowDocument顯示一個頁面。 FlowDocumentPageViewer公開了兩個名爲CanGoToPreviousPage和CanGoToNextPage的只讀依賴項屬性。我希望我的ViewModel始終知道這兩個視圖屬性的值。

我想我可以用OneWayToSource數據綁定做到這一點:

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

如果這是允許的,這將是完美的:每當FlowDocumentPageViewer的CanGoToNextPage性質改變,新的價值會得到向下推到視圖模型的NextPageAvailable財產,這正是我想要的。

不幸的是,這不能編譯:我得到一個錯誤,說'CanGoToPreviousPage'屬性是隻讀的,不能從標記中設置。顯然,只讀屬性不支持任何類型的數據綁定,甚至不支持對該屬性只讀的數據綁定。

我可以讓我的ViewModel的屬性成爲DependencyProperties,並使OneWay綁定成爲另一種方式,但我並沒有爲關注分離違規而瘋狂(ViewModel需要對View的引用,其中MVVM數據綁定應該避免)。

FlowDocumentPageViewer不公開CanGoToNextPageChanged事件,並且我不知道從DependencyProperty獲取更改通知的任何好方法,但是沒有創建另一個將其綁定到的DependencyProperty,這看起來像是過度殺傷。

如何讓ViewModel瞭解視圖只讀屬性的更改?

回答

129

是的,我過去用ActualWidthActualHeight屬性完成了這兩個屬性,它們都是隻讀的。我創建了附加屬性爲ObservedWidthObservedHeight的附加行爲。它還具有用於執行初始連接的Observe屬性。用法是這樣的:

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

因此視圖模型具有WidthHeight性質是始終與ObservedWidthObservedHeight附加屬性同步。 Observe屬性僅附加到FrameworkElementSizeChanged事件。在句柄中,它會更新其ObservedWidthObservedHeight屬性。 Ergo,視圖模型的WidthHeight始終與UserControlActualWidthActualHeight同步。也許不是完美的解決方案(我同意 - 只讀DP 應該支持OneWayToSource綁定),但它的工作原理和它支持MVVM模式。顯然,ObservedWidthObservedHeight DP是而不是只讀。

更新:這是實現上述功能的代碼:

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

我不知道如果你可以做一些技巧來自動附加屬性,而不需要觀察。但這看起來很不錯。謝謝! – 2009-07-05 12:58:08

+1

謝謝肯特。我爲這個「SizeObserver」類發佈了一個代碼示例。 – 2009-08-20 12:54:19

+42

+1到這種情緒:「只讀移民應該支持OneWayToSource綁定」 – Tristan 2011-05-21 19:04:54

20

如果任何人有興趣,我編寫了這裏肯特的解決方案的近似值:

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 
} 

隨意使用它在你的應用程序。它運作良好。 (感謝肯特!)

49

我使用的作品不僅具有ActualWidth的和的ActualHeight的通用解決方案,而且與任何數據,你至少可以在閱讀模式結合。

的標記看起來像這樣,提供ViewportWidth和ViewportHeight是視圖模型

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

這裏的屬性被用於定製元素

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

源代碼下面是另一個解決這個「 bug「這是我在這裏寫的:
OneWayToSource Binding for ReadOnly Dependency Property

它的工作原理是使用t wo依賴屬性,監聽器和鏡像。 Listener綁定OneWay到TargetProperty,並在PropertyChangedCallback中更新綁定OneWayToSource的Mirror屬性,以指定綁定中指定的值。我把它叫做PushBinding,它可以在任何只讀依賴屬性像這樣

<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進行設置。
它包含源代碼和短採樣的使用,或訪問my WPF blog如果你有興趣的實施細則。

最後一個音符,因爲.NET 4.0,我們進一步從內置支持這一點,因爲OneWayToSource Binding reads the value back from the Source after it has updated it

4

我喜歡梅德Tashkinov的解決方案了!但是它在設計模式下崩潰了我的VS。這就是爲什麼我添加了一行到OnSourceChanged方法:

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

,我認爲這是可以做到簡單一點:

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