wpf 将只读GUI属性推回ViewModel

o2gm4chl  于 2023-05-01  发布在  其他
关注(0)|答案(7)|浏览(130)

我想编写一个ViewModel,它总是知道View中某些只读依赖项属性的当前状态。
具体地说,我的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知道视图的只读属性的变化?

6ojccjat

6ojccjat1#

是的,我以前用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不是只读的。
UPDATE:下面是实现上述功能的代码:

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

h4cxqtbf2#

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

lsmepo6l3#

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

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
}

请在您的应用程序中使用它。效果很好。(谢谢肯特!)

fumotvh3

fumotvh34#

下面是我在博客中提到的这个“bug”的另一个解决方案:
OneWayToSource Binding for ReadOnly Dependency Property
它通过使用两个依赖项属性(Listener和Mirror)来工作。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
它包含源代码和简短的示例用法。

8fq7wneg

8fq7wneg5#

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

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

xytpbqjk6#

我觉得可以简单一点:
示例1:

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

dgenwo3n7#

类似于@eriksmith200的想法-它可以用Behavior更简单地完成,没有对属性数量的限制,下面是ActualHeightActualWidth的示例:
示例1:

<Window x:Class="WpfApp21.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:WpfApp21"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow"
        Width="378"
        Height="293"
        d:DataContext="{d:DesignInstance Type=local:Model}"
        mc:Ignorable="d">
    <Behaviors:Interaction.Behaviors>
        <local:ReadOnlyPropertyToModelBindingBehavior ModelProperty="{Binding Height}" ReadOnlyDependencyProperty="{Binding ActualHeight, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" />
        <local:ReadOnlyPropertyToModelBindingBehavior ModelProperty="{Binding Width}" ReadOnlyDependencyProperty="{Binding ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" />
    </Behaviors:Interaction.Behaviors>
    <Grid>
        <TextBlock Margin="35,38,0,0"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"
                   Text="{Binding Height}"
                   TextWrapping="Wrap">
            
        </TextBlock>
        <TextBlock Margin="35,103,0,0"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"
                   Text="{Binding Width}"
                   TextWrapping="Wrap" />
    </Grid>
</Window>

cs:

using Microsoft.Xaml.Behaviors;
using System.ComponentModel;
using System.Windows;

namespace WpfApp21
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new Model();
        }
    }

    public class Model : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private double _Height;
        public double Height
        {
            get { return _Height; }
            set
            {
                if (value == _Height) return;
                _Height = value;
                OnPropertyChanged(nameof(Height));
            }
        }

        private double _Width;
        public double Width
        {
            get { return _Width; }
            set
            {
                if (value == _Width) return;
                _Width = value;
                OnPropertyChanged(nameof(Width));
            }
        }
    }

    public class ReadOnlyPropertyToModelBindingBehavior : Behavior<UIElement>
    {
        public object ReadOnlyDependencyProperty
        {
            get { return (object)GetValue(ReadOnlyDependencyPropertyProperty); }
            set { SetValue(ReadOnlyDependencyPropertyProperty, value); }
        }

        public static readonly DependencyProperty ReadOnlyDependencyPropertyProperty =
            DependencyProperty.Register("ReadOnlyDependencyProperty", typeof(object), typeof(ReadOnlyPropertyToModelBindingBehavior),
                new PropertyMetadata(null, OnReadOnlyDependencyPropertyPropertyChanged));

        public object ModelProperty
        {
            get { return (object)GetValue(ModelPropertyProperty); }
            set { SetValue(ModelPropertyProperty, value); }
        }

        public static readonly DependencyProperty ModelPropertyProperty =
            DependencyProperty.Register("ModelProperty", typeof(object), typeof(ReadOnlyPropertyToModelBindingBehavior), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        private static void OnReadOnlyDependencyPropertyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var b = obj as ReadOnlyPropertyToModelBindingBehavior;
            b.ModelProperty = e.NewValue;
        }
    }
}

相关问题