将只读GUI属性返回到ViewModel

我想写一个ViewModel总是知道从视图的一些只读依赖项属性的当前状态。

具体来说,我的GUI包含一个FlowDocumentPageViewer,它从FlowDocument一次显示一个页面。 FlowDocumentPageViewer公开了两个名为CanGoToPreviousPage和CanGoToNextPage的只读依赖项属性。 我希望我的ViewModel始终知道这两个View属性的值。

我想我可以用OneWayToSource数据绑定来做到这一点:

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

如果允许的话,这将是完美的:每当FlowDocumentPageViewer的CanGoToNextPage属性发生变化时,新的值将被下推到ViewModel的NextPageAvailable属性中,这正是我想要的。

不幸的是,这不能编译:我得到一个错误,说'CanGoToPreviousPage'属性是只读的,不能从标记设置。 显然,只读属性不支持任何types的数据绑定,甚至不支持对该属性只读的数据绑定。

我可以让我的ViewModel的属性为DependencyProperties,并使OneWay绑定到另一种方式,但我不是疯狂的关注分离违规(ViewModel将需要一个引用的视图,MVVM数据绑定应该避免)。

FlowDocumentPageViewer不公开一个CanGoToNextPageChanged事件,我不知道从DependencyProperty获得更改通知的任何好方法,但创build另一个DependencyProperty绑定它,这似乎在这里矫枉过正。

如何让我的ViewModel通知视图的只读属性的变化?

是的,我以前用ActualWidthActualHeight属性完成了这两个属性,它们都是只读的。 我创build了具有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同步。

也许不是完美的解决scheme(我同意 – 只读DP 应该支持OneWayToSource绑定),但它工作,并支持MVVM模式。 显然, ObservedWidthObservedHeight DPs 不是只读的。

更新:这里是实现上述function的代码:

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

我使用的是一种通用的解决scheme,不仅适用于ActualWidth和ActualHeight,而且适用于至less在阅读模式下可绑定的任何数据。

标记看起来像这样,只要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(); } } 

如果其他人有兴趣,我在这里编写了一个近似的肯特的解决scheme:

 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 } 

随意在您的应用程序中使用它。 它运作良好。 (谢谢Kent!)

这里是另一个解决这个“臭虫”,我在这里博客:
OneWayToSource绑定只读依赖项属性

它通过使用两个依赖属性,监听器和镜像来工作。 Listener绑定到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> 

在这里下载演示项目 。
它包含源代码和简短的示例用法,或者如果您对实现细节感兴趣,请访问我的WPF博客 。

最后一点,从.NET 4.0开始,我们甚至远离内置支持,因为OneWayToSource绑定在更新后从源读取值

我喜欢德米特里·塔什金诺夫的解决scheme! 但是它在devise模式下崩溃了我的VS。 这就是为什么我添加一行到OnSourceChanged方法:

     private static void OnSourceChanged(DependencyObject d,DependencyPropertyChangedEventArgs e)
     {
         if(!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject))。DefaultValue))
             ((数据pipe道)d).OnSourceChanged(E);
     }

我认为这可以做得更简单一点:

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