首页 文章

将只读GUI属性推送回ViewModel

提问于
浏览
118

我想编写一个ViewModel,它始终知道View中某些只读依赖项属性的当前状态 .

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

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

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

如果这是允许的,那将是完美的:每当FlowDocumentPageViewer的CanGoToNextPage属性发生更改时,新值将被推送到ViewModel的NextPageAvailable属性,这正是我想要的 .

不幸的是,这不编译:我收到一个错误说 'CanGoToPreviousPage' property is read-only and cannot be set from markup. 显然只读属性不支持任何类型的数据绑定,甚至不支持与该属性相关的只读数据绑定 .

我可以让我的ViewModel的属性为DependencyProperties,并使OneWay绑定以另一种方式运行,但我并不为关注点分离违规而疯狂(ViewModel需要对View的引用,MVVM数据绑定应该避免) .

FlowDocumentPageViewer不公开CanGoToNextPageChanged事件,我不知道从DependencyProperty获取更改通知的任何好方法,没有创建另一个DependencyProperty来绑定它,这在这里看起来有点过分 .

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

6 回答

  • 4

    是的,我过去使用 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 同步 .

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

    我使用的通用解决方案不仅适用于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();
        }
    }
    
  • 143

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

    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
    }
    

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

  • 54

    这是我在这里博客的另一个解决方案"bug":
    OneWayToSource Binding for ReadOnly Dependency Property

    它通过使用两个依赖项属性,侦听器和镜像来工作 . Listener将OneWay绑定到TargetProperty,并在PropertyChangedCallback中更新Mirror属性,该属性将OneWayToSource绑定到Binding中指定的任何内容 . 我称之为 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

  • 10

    我喜欢Dmitry Tashkinov的解决方案!然而它在设计模式下撞毁了我的VS.这就是为什么我在OnSourceChanged方法中添加了一行:

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

    我认为它可以做得更简单一些:

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

相关问题