在XAML中从只读属性进行OneWayToSource绑定

92

我试图使用OneWayToSource模式绑定到一个Readonly属性,但在XAML中似乎无法实现:

<controls:FlagThingy IsModified="{Binding FlagIsModified, 
                                          ElementName=container, 
                                          Mode=OneWayToSource}" />

我得到的是:

属性“FlagThingy.IsModified”无法设置,因为它没有可访问的 set 访问器。

IsModified 是在 FlagThingy 上一个只读的 DependencyProperty。我想将该值绑定到容器上的 FlagIsModified 属性。

要明确一点:

FlagThingy.IsModified --> container.FlagIsModified
------ READONLY -----     ----- READWRITE --------

仅使用XAML是否可能实现这一点?


更新: 好吧,我通过在容器上设置绑定而不是在FlagThingy上设置绑定来解决了此问题。但我仍然想知道这是否可能。


但是你如何为只读属性设置值呢? - idursun
3
你不能这样做。这也不是我试图实现的目标。 我试图从只读属性IsModified转换为可读写属性FlagIsModified - Inferis
好问题。你的解决方法只在容器是DependencyObject并且FlagIsModified是DependencyProperty时才有效。 - Josh G
10
好问题,但我不理解已接受的答案。如果有些 WPF 大师能给我更多启示,我将不胜感激 - 这是一个错误还是按设计来的? - Oskar
根据这里,这是一个bug。不过还没有修复计划。 - user1151923
9个回答

49

OneWayToSource的一些研究结果...

选项#1。

// Control definition
public partial class FlagThingy : UserControl
{
    public static readonly DependencyProperty IsModifiedProperty = 
            DependencyProperty.Register("IsModified", typeof(bool), typeof(FlagThingy), new PropertyMetadata());
}
<controls:FlagThingy x:Name="_flagThingy" />
// Binding Code
Binding binding = new Binding();
binding.Path = new PropertyPath("FlagIsModified");
binding.ElementName = "container";
binding.Mode = BindingMode.OneWayToSource;
_flagThingy.SetBinding(FlagThingy.IsModifiedProperty, binding);

选项#2

// Control definition
public partial class FlagThingy : UserControl
{
    public static readonly DependencyProperty IsModifiedProperty = 
            DependencyProperty.Register("IsModified", typeof(bool), typeof(FlagThingy), new PropertyMetadata());

    public bool IsModified
    {
        get { return (bool)GetValue(IsModifiedProperty); }
        set { throw new Exception("An attempt ot modify Read-Only property"); }
    }
}
<controls:FlagThingy IsModified="{Binding Path=FlagIsModified, 
    ElementName=container, Mode=OneWayToSource}" />

选项#3 (真正的只读依赖属性)

System.ArgumentException: 'IsModified'属性无法进行数据绑定。

// Control definition
public partial class FlagThingy : UserControl
{
    private static readonly DependencyPropertyKey IsModifiedKey =
        DependencyProperty.RegisterReadOnly("IsModified", typeof(bool), typeof(FlagThingy), new PropertyMetadata());

    public static readonly DependencyProperty IsModifiedProperty = 
        IsModifiedKey.DependencyProperty;
}
<controls:FlagThingy x:Name="_flagThingy" />
// Binding Code
Same binding code...

Reflector给出了答案:

internal static BindingExpression CreateBindingExpression(DependencyObject d, DependencyProperty dp, Binding binding, BindingExpressionBase parent)
{
    FrameworkPropertyMetadata fwMetaData = dp.GetMetadata(d.DependencyObjectType) as FrameworkPropertyMetadata;
    if (((fwMetaData != null) && !fwMetaData.IsDataBindingAllowed) || dp.ReadOnly)
    {
        throw new ArgumentException(System.Windows.SR.Get(System.Windows.SRID.PropertyNotBindable, new object[] { dp.Name }), "dp");
    }
 ....

32
那其实是一个漏洞。 - Inferis
不错的研究。如果你没有这么好地阐述,我可能会走同样痛苦的路。同意@Inferis的观点。 - kevinarpe
1
这是一个 bug 吗?为什么一个只读 DependencyProperty 不允许使用 OneWayToSource 绑定? - Alex Hope O'Connor
这不是一个错误。它是经过设计和有充分记录的。这是由于绑定引擎与依赖属性系统协同工作的方式(绑定目标必须是DependencyProperty DP)。只有使用相关的DependencyPropertyKey才能修改只读DP。为了注册BindingExpression,引擎必须操作目标DP的元数据。由于DependencyPropertyKey被认为是私有的以保证公共写入保护,因此引擎将不得不忽略此键,导致无法在只读DP上注册绑定。 - BionicCode

26
这是WPF的设计限制,这是有意为之。在Connect上有相关报告:
OneWayToSource binding from a readonly dependency property 我创建了一个名为PushBinding的解决方案,可以动态地将只读依赖属性推送到源,我在这里发表过博客。下面的示例将从只读DP的ActualWidthActualHeight进行OneWayToSource绑定,绑定到DataContext的宽度和高度属性。
<TextBlock Name="myTextBlock">
    <pb:PushBindingManager.PushBindings>
        <pb:PushBinding TargetProperty="ActualHeight" Path="Height"/>
        <pb:PushBinding TargetProperty="ActualWidth" Path="Width"/>
    </pb:PushBindingManager.PushBindings>
</TextBlock>

PushBinding通过使用两个依赖属性Listener和Mirror来实现。Listener与TargetProperty单向绑定,在PropertyChangedCallback中,它更新了Mirror属性的值,Mirror属性则是单向绑定到指定的Binding源上(使用OneWayToSource绑定方式)。

下载演示项目,其中包含源代码和简单的用法示例。


有趣!我想到了一个类似的解决方案,称之为“Conduit” - Conduit根据您的设计具有两个依赖属性和两个单独的绑定。我使用的用例是在XAML中将普通属性绑定到普通属性。 - Daniel Paull
3
我发现你的MS Connect链接已经失效了。这是意味着微软已经在更新版本的.NET中修复了它,还是他们只是删除了它? - Tiny
@Tiny Connect似乎最终被遗弃了,这很不幸。它在许多地方都有链接。我认为它并没有特别说明问题是否得到解决。 - StayOnTarget
我喜欢这个解决方案 - 非常干净且易于实现。不过我有一个问题,正在考虑最干净的解决方法。我的视图模型属性已正确绑定,当我的用户控件首次构建时,vm上设置了正确的值(ActualWidth和ActualHeight)。但是当我将数据上下文设置为不同的视图模型时,推送绑定的值未在新vm上设置。我可以实现一个hacky修复此问题,但我想知道您的想法。 - David Kirkland
通过监听DataContextChanged事件解决,就在这行代码 ((FrameworkElement)targetObject).Loaded += delegate { TargetPropertyValueChanged(); }; 之后,然后在数据上下文更改时重新绑定 BindingOperations.SetBinding(this, TargetPropertyMirrorProperty, Binding); TargetPropertyValueChanged(); 假设我已经满意地解释了解决方案,你认为这是一个合理的更改吗?你能看到任何问题吗? - David Kirkland

5

Wrote this:

Usage:

<TextBox Text="{Binding Text}"
         p:OneWayToSource.Bind="{p:Paths From={x:Static Validation.HasErrorProperty},
                                         To=SomeDataContextProperty}" />

代码:

using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;

public static class OneWayToSource
{
    public static readonly DependencyProperty BindProperty = DependencyProperty.RegisterAttached(
        "Bind",
        typeof(ProxyBinding),
        typeof(OneWayToSource),
        new PropertyMetadata(default(Paths), OnBindChanged));

    public static void SetBind(this UIElement element, ProxyBinding value)
    {
        element.SetValue(BindProperty, value);
    }

    [AttachedPropertyBrowsableForChildren(IncludeDescendants = false)]
    [AttachedPropertyBrowsableForType(typeof(UIElement))]
    public static ProxyBinding GetBind(this UIElement element)
    {
        return (ProxyBinding)element.GetValue(BindProperty);
    }

    private static void OnBindChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((ProxyBinding)e.OldValue)?.Dispose();
    }

    public class ProxyBinding : DependencyObject, IDisposable
    {
        private static readonly DependencyProperty SourceProxyProperty = DependencyProperty.Register(
            "SourceProxy",
            typeof(object),
            typeof(ProxyBinding),
            new PropertyMetadata(default(object), OnSourceProxyChanged));

        private static readonly DependencyProperty TargetProxyProperty = DependencyProperty.Register(
            "TargetProxy",
            typeof(object),
            typeof(ProxyBinding),
            new PropertyMetadata(default(object)));

        public ProxyBinding(DependencyObject source, DependencyProperty sourceProperty, string targetProperty)
        {
            var sourceBinding = new Binding
            {
                Path = new PropertyPath(sourceProperty),
                Source = source,
                Mode = BindingMode.OneWay,
            };

            BindingOperations.SetBinding(this, SourceProxyProperty, sourceBinding);

            var targetBinding = new Binding()
            {
                Path = new PropertyPath($"{nameof(FrameworkElement.DataContext)}.{targetProperty}"),
                Mode = BindingMode.OneWayToSource,
                Source = source
            };

            BindingOperations.SetBinding(this, TargetProxyProperty, targetBinding);
        }

        public void Dispose()
        {
            BindingOperations.ClearAllBindings(this);
        }

        private static void OnSourceProxyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            d.SetCurrentValue(TargetProxyProperty, e.NewValue);
        }
    }
}

[MarkupExtensionReturnType(typeof(OneWayToSource.ProxyBinding))]
public class Paths : MarkupExtension
{
    public DependencyProperty From { get; set; }

    public string To { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var provideValueTarget = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
        var targetObject = (UIElement)provideValueTarget.TargetObject;
        return new OneWayToSource.ProxyBinding(targetObject, this.From, this.To);
    }
}

我还没有在样式和模板中测试过它,想必需要特殊处理。


3

这是另一种基于SizeObserver的附加属性解决方案,详细信息请参考将只读GUI属性推回到ViewModel中

public static class MouseObserver
{
    public static readonly DependencyProperty ObserveProperty = DependencyProperty.RegisterAttached(
        "Observe",
        typeof(bool),
        typeof(MouseObserver),
        new FrameworkPropertyMetadata(OnObserveChanged));

    public static readonly DependencyProperty ObservedMouseOverProperty = DependencyProperty.RegisterAttached(
        "ObservedMouseOver",
        typeof(bool),
        typeof(MouseObserver));


    public static bool GetObserve(FrameworkElement frameworkElement)
    {
        return (bool)frameworkElement.GetValue(ObserveProperty);
    }

    public static void SetObserve(FrameworkElement frameworkElement, bool observe)
    {
        frameworkElement.SetValue(ObserveProperty, observe);
    }

    public static bool GetObservedMouseOver(FrameworkElement frameworkElement)
    {
        return (bool)frameworkElement.GetValue(ObservedMouseOverProperty);
    }

    public static void SetObservedMouseOver(FrameworkElement frameworkElement, bool observedMouseOver)
    {
        frameworkElement.SetValue(ObservedMouseOverProperty, observedMouseOver);
    }

    private static void OnObserveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var frameworkElement = (FrameworkElement)dependencyObject;
        if ((bool)e.NewValue)
        {
            frameworkElement.MouseEnter += OnFrameworkElementMouseOverChanged;
            frameworkElement.MouseLeave += OnFrameworkElementMouseOverChanged;
            UpdateObservedMouseOverForFrameworkElement(frameworkElement);
        }
        else
        {
            frameworkElement.MouseEnter -= OnFrameworkElementMouseOverChanged;
            frameworkElement.MouseLeave -= OnFrameworkElementMouseOverChanged;
        }
    }

    private static void OnFrameworkElementMouseOverChanged(object sender, MouseEventArgs e)
    {
        UpdateObservedMouseOverForFrameworkElement((FrameworkElement)sender);
    }

    private static void UpdateObservedMouseOverForFrameworkElement(FrameworkElement frameworkElement)
    {
        frameworkElement.SetCurrentValue(ObservedMouseOverProperty, frameworkElement.IsMouseOver);
    }
}

在控件中声明附加属性

<ListView ItemsSource="{Binding SomeGridItems}"                             
     ut:MouseObserver.Observe="True"
     ut:MouseObserver.ObservedMouseOver="{Binding IsMouseOverGrid, Mode=OneWayToSource}">    

2
这里是绑定到Validation.HasError的另一种实现方式。
public static class OneWayToSource
{
    public static readonly DependencyProperty BindingsProperty = DependencyProperty.RegisterAttached(
        "Bindings",
        typeof(OneWayToSourceBindings),
        typeof(OneWayToSource),
        new PropertyMetadata(default(OneWayToSourceBindings), OnBinidngsChanged));

    public static void SetBindings(this FrameworkElement element, OneWayToSourceBindings value)
    {
        element.SetValue(BindingsProperty, value);
    }

    [AttachedPropertyBrowsableForChildren(IncludeDescendants = false)]
    [AttachedPropertyBrowsableForType(typeof(FrameworkElement))]
    public static OneWayToSourceBindings GetBindings(this FrameworkElement element)
    {
        return (OneWayToSourceBindings)element.GetValue(BindingsProperty);
    }

    private static void OnBinidngsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((OneWayToSourceBindings)e.OldValue)?.ClearValue(OneWayToSourceBindings.ElementProperty);
        ((OneWayToSourceBindings)e.NewValue)?.SetValue(OneWayToSourceBindings.ElementProperty, d);
    }
}

public class OneWayToSourceBindings : FrameworkElement
{
    private static readonly PropertyPath DataContextPath = new PropertyPath(nameof(DataContext));
    private static readonly PropertyPath HasErrorPath = new PropertyPath($"({typeof(Validation).Name}.{Validation.HasErrorProperty.Name})");
    public static readonly DependencyProperty HasErrorProperty = DependencyProperty.Register(
        nameof(HasError),
        typeof(bool),
        typeof(OneWayToSourceBindings),
        new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    internal static readonly DependencyProperty ElementProperty = DependencyProperty.Register(
        "Element",
        typeof(UIElement),
        typeof(OneWayToSourceBindings),
        new PropertyMetadata(default(UIElement), OnElementChanged));

    private static readonly DependencyProperty HasErrorProxyProperty = DependencyProperty.RegisterAttached(
        "HasErrorProxy",
        typeof(bool),
        typeof(OneWayToSourceBindings),
        new PropertyMetadata(default(bool), OnHasErrorProxyChanged));

    public bool HasError
    {
        get { return (bool)this.GetValue(HasErrorProperty); }
        set { this.SetValue(HasErrorProperty, value); }
    }

    private static void OnHasErrorProxyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.SetCurrentValue(HasErrorProperty, e.NewValue);
    }

    private static void OnElementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue == null)
        {
            BindingOperations.ClearBinding(d, DataContextProperty);
            BindingOperations.ClearBinding(d, HasErrorProxyProperty);
        }
        else
        {
            var dataContextBinding = new Binding
                                         {
                                             Path = DataContextPath,
                                             Mode = BindingMode.OneWay,
                                             Source = e.NewValue
                                         };
            BindingOperations.SetBinding(d, DataContextProperty, dataContextBinding);

            var hasErrorBinding = new Binding
                                      {
                                          Path = HasErrorPath,
                                          Mode = BindingMode.OneWay,
                                          Source = e.NewValue
                                      };
            BindingOperations.SetBinding(d, HasErrorProxyProperty, hasErrorBinding);
        }
    }
}

XAML中的用法

<StackPanel>
    <TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}">
        <local:OneWayToSource.Bindings>
            <local:OneWayToSourceBindings HasError="{Binding HasError}" />
        </local:OneWayToSource.Bindings>
    </TextBox>
    <CheckBox IsChecked="{Binding HasError, Mode=OneWay}" />
</StackPanel>

这个实现是针对绑定 Validation.HasError 的。


0

WPF不会使用CLR属性设置器,但似乎它基于此执行一些奇怪的验证。

也许在您的情况下这可以接受:

    public bool IsModified
    {
        get { return (bool)GetValue(IsModifiedProperty); }
        set { throw new Exception("An attempt ot modify Read-Only property"); }
    }

1
在这种情况下,不使用CLR属性。 - Inferis
你的意思是说你刚刚定义了DependencyProperty并且能够编写<controls:FlagThingy IsModified="..." />吗?对于我来说,如果我不添加CLR属性,它会显示:"XML命名空间中不存在'IsModified'属性"。 - alex2k8
1
我认为设计时使用clr属性,而运行时实际上直接访问依赖属性(如果它是一个)。 - meandmycode
CLR 属性在我的情况下是不必要的(我没有从代码中使用 IsModified),但它仍然存在(只有一个公共设置器)。在设计时和运行时,只需使用 DependencyProperty 注册即可正常工作。 - Inferis
绑定本身并未使用CLR属性,但在XAML中定义绑定时,必须将其转换为代码。我猜在这个阶段,XAML解析器看到IsModified属性是只读的,并抛出异常(即使在绑定创建之前)。 - alex2k8
很遗憾,这对于BCL类的预定义属性不起作用 :-/ - O. R. Mapper

0

嗯...我不确定我同意这些解决方案中的任何一个。你可以在属性注册中指定一个强制回调函数,忽略外部更改。例如,我需要实现一个只读的Position依赖属性来获取用户控件内MediaElement控件的位置。这是我的做法:

    public static readonly DependencyProperty PositionProperty = DependencyProperty.Register("Position", typeof(double), typeof(MediaViewer),
        new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal, OnPositionChanged, OnPositionCoerce));

    private static void OnPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctrl = d as MediaViewer;
    }

    private static object OnPositionCoerce(DependencyObject d, object value)
    {
        var ctrl = d as MediaViewer;
        var position = ctrl.MediaRenderer.Position.TotalSeconds;

        if (ctrl.MediaRenderer.NaturalDuration.HasTimeSpan == false)
            return 0d;
        else
            return Math.Min(position, ctrl.Duration);
    }

    public double Position
    {
        get { return (double)GetValue(PositionProperty); }
        set { SetValue(PositionProperty, value); }
    }

换句话说,只需忽略更改并返回由不具有公共修饰符的其他成员支持的值即可。-- 在上面的示例中,MediaRenderer实际上是私有的MediaElement控件。

太遗憾了,这对于 BCL 类的预定义属性不起作用。 :-/ - O. R. Mapper

0
我解决这个限制的方法是在我的类中只公开一个绑定属性,完全保持DependencyProperty私有。我实现了一个“PropertyBindingToSource”只写属性(这个不是DependencyProperty),可以在xaml中设置为绑定值。在这个只写属性的setter中,我调用BindingOperations.SetBinding来将绑定链接到DependencyProperty。
对于OP的具体示例,它看起来像这样:
FlatThingy实现:
public partial class FlatThingy : UserControl
{
    public FlatThingy()
    {
        InitializeComponent();
    }

    public Binding IsModifiedBindingToSource
    {
        set
        {
            if (value?.Mode != BindingMode.OneWayToSource)
            {
                throw new InvalidOperationException("IsModifiedBindingToSource must be set to a OneWayToSource binding");
            }

            BindingOperations.SetBinding(this, IsModifiedProperty, value);
        }
    }

    public bool IsModified
    {
        get { return (bool)GetValue(IsModifiedProperty); }
        private set { SetValue(IsModifiedProperty, value); }
    }

    private static readonly DependencyProperty IsModifiedProperty =
        DependencyProperty.Register("IsModified", typeof(bool), typeof(FlatThingy), new PropertyMetadata(false));

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        IsModified = !IsModified;
    }
}

请注意,静态只读 DependencyProperty 对象是私有的。在控件中,我添加了一个按钮,其点击事件由 Button_Click 处理。 在我的 window.xaml 中使用 FlatThingy 控件:
<Window x:Class="ReadOnlyBinding.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:ReadOnlyBinding"
    mc:Ignorable="d"
    DataContext="{x:Static local:ViewModel.Instance}"
    Title="MainWindow" Height="450" Width="800">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>

    <TextBlock Text="{Binding FlagIsModified}" Grid.Row="0" />
    <local:FlatThingy IsModifiedBindingToSource="{Binding FlagIsModified, Mode=OneWayToSource}" Grid.Row="1" />
</Grid>

请注意,我还实现了一个ViewModel用于绑定,但此处未显示。从上面的源代码中可以看到,它公开了一个名为“FlagIsModified”的DependencyProperty。
它非常好用,使我能够以松散耦合的方式将信息从视图推回到ViewModel中,并明确定义信息流的方向。

-2

你现在绑定方向不对。OneWayToSource将尝试在您创建的控件上更改IsModified时更新容器中的FlagIsModified。而你想要相反的效果,也就是让IsModified绑定到container.FlagIsModified上。为此,应该使用OneWay绑定模式。

<controls:FlagThingy IsModified="{Binding FlagIsModified, 
                                          ElementName=container, 
                                          Mode=OneWay}" />

枚举成员的完整列表:http://msdn.microsoft.com/en-us/library/system.windows.data.bindingmode.aspx


5
不,我想要的正是你所描述的情景,我不想去做的那个。FlagThingy.IsModified --> container.FlagIsModified - Inferis
3
因为提问者问题模糊而被扣分似乎有些过头了。 - JaredPar
6
@JaredPar:我看不出这个问题有什么含糊不清的地方。该问题陈述了以下三点:1)有一个只读的依赖属性IsModified,2)OP想要在XAML中声明对该属性的绑定,3)该绑定应该以单向到源(OneWayToSource)模式工作。你的解决方案在实践上不起作用,因为如问题所述,编译器不允许您在只读属性上声明绑定,在概念上也不适用,因为IsModified是只读的,因此它的值无法被绑定更改。 - O. R. Mapper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接