Pushing read-only GUI properties back into ViewModel

后端 未结 6 1828
攒了一身酷
攒了一身酷 2020-11-22 12:50

I want to write a ViewModel that always knows the current state of some read-only dependency properties from the View.

Specifically, my GUI contains a FlowDocumentPa

相关标签:
6条回答
  • 2020-11-22 13:22

    Yes, I've done this in the past with the ActualWidth and ActualHeight properties, both of which are read-only. I created an attached behavior that has ObservedWidth and ObservedHeight attached properties. It also has an Observe property that is used to do the initial hook-up. Usage looks like this:

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

    So the view model has Width and Height properties that are always in sync with the ObservedWidth and ObservedHeight attached properties. The Observe property simply attaches to the SizeChanged event of the FrameworkElement. In the handle, it updates its ObservedWidth and ObservedHeight properties. Ergo, the Width and Height of the view model is always in sync with the ActualWidth and ActualHeight of the UserControl.

    Perhaps not the perfect solution (I agree - read-only DPs should support OneWayToSource bindings), but it works and it upholds the MVVM pattern. Obviously, the ObservedWidth and ObservedHeight DPs are not read-only.

    UPDATE: here's code that implements the functionality described above:

    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 讨论(0)
  • 2020-11-22 13:26

    If anyone else is interested, I coded up an approximation of Kent's solution here:

    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
    }
    

    Feel free to use it in your apps. It works well. (Thanks Kent!)

    0 讨论(0)
  • 2020-11-22 13:28

    I like Dmitry Tashkinov's solution! However it crashed my VS in design mode. That's why I added a line to OnSourceChanged method:

        private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
                ((DataPipe)d).OnSourceChanged(e);
        }
    
    0 讨论(0)
  • 2020-11-22 13:29

    I use a universal solution which works not only with ActualWidth and ActualHeight, but also with any data you can bind to at least in reading mode.

    The markup looks like this, provided ViewportWidth and ViewportHeight are properties of the view model

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

    Here is the source code for the custom elements

    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 讨论(0)
  • 2020-11-22 13:30

    Here is another solution to this "bug" which I blogged about here:
    OneWayToSource Binding for ReadOnly Dependency Property

    It works by using two Dependency Properties, Listener and Mirror. Listener is bound OneWay to the TargetProperty and in the PropertyChangedCallback it updates the Mirror property which is bound OneWayToSource to whatever was specified in the Binding. I call it PushBinding and it can be set on any read-only Dependency Property like this

    <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.
    It contains source code and short sample usage.

    One last note, since .NET 4.0 we are even further away from built-in-support for this, since a OneWayToSource Binding reads the value back from the Source after it has updated it

    0 讨论(0)
  • 2020-11-22 13:36

    I think it can be done a bit simpler:

    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 讨论(0)
提交回复
热议问题