【问题标题】:Pushing read-only GUI properties back into ViewModel将只读 GUI 属性推回 ViewModel
【发布时间】:2010-11-08 04:00:50
【问题描述】:

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

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

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

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

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

很遗憾,这无法编译:我收到一条错误消息,提示 'CanGoToPreviousPage' 属性是只读的,无法从标记中设置。 显然只读属性不支持 任何类型的数据绑定,甚至不包括对该属性的只读数据绑定。

我可以让我的 ViewModel 的属性为 DependencyProperties,并以另一种方式进行 OneWay 绑定,但我并不担心违反关注点分离(ViewModel 需要对 View 的引用,MVVM 数据绑定是应该避免)。

FlowDocumentPageViewer 没有公开 CanGoToNextPageChanged 事件,而且我不知道有什么好的方法可以从 DependencyProperty 获取更改通知,除了创建另一个 DependencyProperty 来绑定它,这在这里看起来有点过头了。

如何让我的 ViewModel 了解视图只读属性的更改?

【问题讨论】:

    标签: wpf data-binding mvvm readonly


    【解决方案1】:

    是的,我过去曾使用 ActualWidthActualHeight 属性完成此操作,这两个属性都是只读的。我创建了一个具有ObservedWidthObservedHeight 附加属性的附加行为。它还有一个Observe 属性,用于进行初始连接。用法如下所示:

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

    因此视图模型具有WidthHeight 属性,它们始终与ObservedWidthObservedHeight 附加属性同步。 Observe 属性只是附加到FrameworkElementSizeChanged 事件。在句柄中,它更新其ObservedWidthObservedHeight 属性。因此,视图模型的WidthHeight 始终与UserControlActualWidthActualHeight 同步。

    也许不是完美的解决方案(我同意 - 只读 DPs应该 支持 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);
        }
    }
    

    【讨论】:

    • 我想知道你是否可以做一些技巧来自动附加属性,而不需要观察。但这看起来是一个很好的解决方案。谢谢!
    • 谢谢肯特。我在下面为这个“SizeObserver”类发布了一个代码示例。
    • 对此观点+1:“只读 DP 应支持 OneWayToSource 绑定”
    • 也许最好只创建一个Size 属性,结合高度和宽度。大约。代码减少 50%。
    • @Gerard:这行不通,因为FrameworkElement 中没有ActualSize 属性。如果要直接绑定附加属性,则必须创建两个属性分别绑定到ActualWidthActualHeight
    【解决方案2】:

    我使用了一种通用解决方案,它不仅适用于 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();
        }
    }
    

    【讨论】:

    • (通过 user543564 的回答):这不是答案,而是对 Dmitry 的评论 - 我使用了您的解决方案,效果很好。很好的通用解决方案,可以在不同的地方通用。我用它来将一些 ui 元素属性(ActualHeight 和 ActualWidth)推送到我的视图模型中。
    • 谢谢!这帮助我绑定到一个普通的 get only 属性。不幸的是,该属性没有发布 INotifyPropertyChanged 事件。我通过为 DataPipe 绑定分配名称并将以下内容添加到控件更改事件来解决此问题:BindingOperations.GetBindingExpressionBase(bindingName, DataPipe.SourceProperty).UpdateTarget();
    • 这个解决方案对我来说效果很好。我唯一的调整是将 TargetProperty DependencyProperty 上的 FrameworkPropertyMetadata 的 BindsTwoWayByDefault 设置为 true。
    • 这个解决方案的唯一抱怨似乎是它破坏了干净的封装,因为 Target 属性必须是可写的,即使它不能从外部更改:-/
    • 对于那些更喜欢 NuGet 包而不是复制粘贴代码的人:我已将 DataPipe 添加到我的开源 JungleControls 库中。见DataPipe documentation
    【解决方案3】:

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

    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
    }
    

    随意在您的应用中使用它。它运作良好。 (谢谢肯特!)

    【讨论】:

      【解决方案4】:

      这是我在博客中提到的这个“错误”的另一个解决方案:
      OneWayToSource Binding for ReadOnly Dependency Property

      它通过使用两个依赖属性,监听器和镜像来工作。侦听器绑定到 TargetProperty 的 OneWay,并且在 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.
      它包含源代码和简短的示例用法。

      最后一点,从 .NET 4.0 开始,我们离内置支持更远了,因为 OneWayToSource Binding reads the value back from the Source after it has updated it

      【讨论】:

      • 关于 Stack Overflow 的答案应该是完全独立的。包含指向可选外部参考的链接很好,但答案所需的所有代码都应包含在答案本身中。请更新您的问题,以便无需访问任何其他网站即可使用它。
      • 这看起来像是在你的博客上做自我广告,而不是一个答案。
      • 是的,我想你们都说对了,虽然这不是我的本意,但它看起来确实有点像广告。在我发表那篇博文之后,我刚刚回答了所有关于只读属性的 OneWayToSource 绑定的问题。不过,我不确定还有什么要添加到答案中,它包含如何执行此操作的示例代码以及指向我博客上没有的演示项目的链接。我会清理一下:-)
      【解决方案5】:

      我喜欢 Dmitry Tashkinov 的解决方案! 但是它在设计模式下使我的 VS 崩溃了。这就是为什么我在 OnSourceChanged 方法中添加了一行:

      私有静态无效 OnSourceChanged(DependencyObject d,DependencyPropertyChangedEventArgs e) { if (!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue)) ((DataPipe)d).OnSourceChanged(e); }

      【讨论】:

        【解决方案6】:

        我觉得可以简单一点:

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

        【讨论】:

        • 可能会简单一点,但如果我读得好,它允许只有一个元素上的这种绑定。我的意思是,我认为使用这种方法,您将无法同时绑定 ActualWidth ActualHeight。只有其中之一。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-02-01
        • 2010-11-22
        • 2010-12-02
        • 1970-01-01
        • 2011-08-26
        相关资源
        最近更新 更多