将只读GUI属性推回ViewModel中


124

我想编写一个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知道视图的只读属性的更改?

Answers:


151

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

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

因此,视图模型具有WidthHeight属性,这些属性始终与ObservedWidthObservedHeight附加属性同步。该Observe属性仅附加到的SizeChanged事件FrameworkElement。在句柄中,它更新其ObservedWidthObservedHeight属性。因此,这个WidthHeight视图模型总是同步与ActualWidthActualHeightUserControl

也许不是完美的解决方案(我同意-只读DP 应该支持OneWayToSource绑定),但是它可以工作并且支持MVVM模式。显然,ObservedWidthObservedHeightDP 不是只读的。

更新:这是实现上述功能的代码:

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

2
我想知道您是否可以做一些技巧来自动附加属性,而无需观察。但这似乎是一个很好的解决方案。谢谢!
乔·怀特2009年

1
谢谢肯特。我在下面为该“ SizeObserver”类发布了代码示例。
Scott Whitlock,2009年

52
对此观点+1:“只读DP应该支持OneWayToSource绑定”
Tristan

3
Size结合使用Heigth和Width,仅创建一个属性可能更好。大约 代码减少50%。
Gerard

1
@杰拉德:那是行不通的,因为中没有ActualSize财产FrameworkElement。如果要直接绑定附加属性,则必须创建两个属性分别绑定到ActualWidthActualHeight
dotNET

58

我使用的通用解决方案不仅适用于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)推入到我的视图模型中。
马克·格雷夫

2
谢谢!这帮助我绑定到普通的仅获取属性。不幸的是,该属性未发布INotifyPropertyChanged事件。我通过为DataPipe绑定分配名称并将以下内容添加到控件更改的事件中来解决此问题:BindingOperations.GetBindingExpressionBase(bindingName,DataPipe.SourceProperty).UpdateTarget();
chilltemp 2011年

3
该解决方案对我来说效果很好。我唯一的调整是将TargetProperty DependencyProperty上的FrameworkPropertyMetadata的BindsTwoWayByDefault设置为true。
哈萨尼·布莱克韦尔

1
关于此解决方案的唯一抱怨似乎是它破坏了干净的封装,因为Target即使不能从外部对其进行更改,也必须使该属性可写:-/
OR Mapper

对于那些更喜欢NuGet软件包而不是复制粘贴代码的人:我已经将DataPipe添加到我的开源JungleControls库中。请参阅DataPipe文档
罗伯特·瓦赞(RobertVažan)2014年

21

如果其他人有兴趣,我会在此处编写Kent解决方案的近似值:

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
}

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


10

这是我在此处发布过的“错误”的另一个解决方案:
ReadOnly依赖项属性的OneWayToSource绑定

它通过使用两个依赖项属性(侦听器和镜像)来工作。侦听器被OneWay绑定到TargetProperty,并在PropertyChangedCallback中将Mirror属性更新为OneWayToSource绑定到绑定中指定的任何属性。我称之为它PushBinding,它可以在任何只读的Dependency属性上设置,就像这样

<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绑定在更新值后从Source读取回值。


关于堆栈溢出的答案应完全独立。可以包括指向可选外部引用的链接,但是答案所需的所有代码都应包含在答案本身中。请更新您的问题,以便可以在不访问任何其他网站的情况下使用它。
Peter Duniho

4

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

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

0

我认为可以将其简化一些:

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

2
可能有点简单,但如果我读得很好,它允许只有一个对等元素结合。我的意思是,我认为,使用这种方法,您将无法同时绑定ActualWidth ActualHeight。只是其中之一。
quetzalcoatl
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.