【问题标题】:OneWayToSource binding from readonly property in XAMLXAML 中只读属性的 OneWayToSource 绑定
【发布时间】:2010-10-14 01:51:15
【问题描述】:

我正在尝试以OneWayToSource 作为模式绑定到Readonly 属性,但在XAML 中似乎无法做到这一点:

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

我明白了:

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

IsModifiedFlagThingy 上的只读DependencyProperty。我想将该值绑定到容器上的 FlagIsModified 属性。

要明确:

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

这是否可能仅使用 XAML?


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

【问题讨论】:

  • 但是如何将值设置为只读属性?
  • 你不能。这也不是我想要达到的目标。我正在尝试从只读属性IsModified 到读写属性FlagIsModified
  • 好问题。仅当容器是 DependencyObject 并且 FlagIsModified 是 DependencyProperty 时,您的解决方法才有效。
  • 很好的问题,但是我无法理解接受的答案。如果一些 WPF 大师能启发我更多信息,我将不胜感激 - 这是一个错误还是每个设计?
  • @Oskar 根据this 这是一个错误。不过目前还没有修复。

标签: wpf data-binding xaml readonly


【解决方案1】:

这是 WPF 的限制,这是设计使然。在此处连接报道:
OneWayToSource binding from a readonly dependency property

我做了一个解决方案,可以动态地将只读依赖属性推送到名为PushBinding 的源,我blogged about here。下面的示例将 OneWayToSource 从只读 DP 的 ActualWidthActualHeight 绑定到 DataContext 的 Width 和 Height 属性

<TextBlock Name="myTextBlock">
    <pb:PushBindingManager.PushBindings>
        <pb:PushBinding TargetProperty="ActualHeight" Path="Height"/>
        <pb:PushBinding TargetProperty="ActualWidth" Path="Width"/>
    </pb:PushBindingManager.PushBindings>
</TextBlock>

PushBinding 通过使用两个依赖属性,监听器和镜像来工作。侦听器绑定OneWay 到 TargetProperty,并在 PropertyChangedCallback 中更新绑定 OneWayToSource 的 Mirror 属性到绑定中指定的任何内容。

Demo Project can be Downloaded Here.
它包含源代码和简短的示例用法。

【讨论】:

  • 有趣!我想出了一个类似的解决方案并将其称为“管道”——根据您的设计,管道具有两个依赖属性和两个单独的绑定。我的用例是将普通旧属性绑定到 XAML 中的普通旧属性。
  • 我发现您的 MS Connect 链接不再有效。这是否意味着 MS 已在较新版本的 .NET 中修复了它,或者他们只是删除了它?
  • 不幸的是,@Tiny Connect 似乎最终被放弃了。它在许多地方都有联系。我认为这并没有具体暗示某个问题是否已得到解决。
  • 我喜欢这个解决方案 - 非常干净且易于实施。我确实有一个问题,我正在考虑最干净的方法来解决它。我正确绑定了视图模型属性,并且当我的用户控件首次构建时,在 vm 上设置了正确的值(ActualWidth 和 ActualHeight)。但是当我将数据上下文设置为不同的视图模型时,推送绑定的值不会在新 vm 上设置。我可以对此实施一个 hacky 修复,但我想知道您的想法。
  • 通过监听 DataContextChanged 事件解决,就在((FrameworkElement)targetObject).Loaded += delegate { TargetPropertyValueChanged(); }; 行之后,然后重新绑定数据上下文更改BindingOperations.SetBinding(this, TargetPropertyMirrorProperty, Binding); TargetPropertyValueChanged(); 假设我已经令人满意地解释了解决方案,你认为这是一个合理的改变,你能看出它有什么问题吗?
【解决方案2】:

我解决此限制的方法是在我的类中仅公开一个 Binding 属性,使 DependencyProperty 完全保持私有。我实现了一个“PropertyBindingToSource”只写属性(这不是 DependencyProperty),它可以设置为 xaml 中的绑定值。在这个只写属性的设置器中,我调用 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,您可以从上面的源代码中收集到。

效果很好,允许我以松散耦合的方式将信息从 View 推回 ViewModel,并明确定义信息流的方向。

【讨论】:

    【解决方案3】:

    这是另一个基于 SizeObserver 的附加属性解决方案,在此处详述 Pushing read-only GUI properties back into 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}">    
    

    【讨论】:

      【解决方案4】:

      这是绑定到 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

      【讨论】:

        【解决方案5】:

        这样写的:

        用法:

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

        还没有在样式和模板中测试过,估计需要特殊的大小写。

        【讨论】:

          【解决方案6】:

          嗯...我不确定我是否同意这些解决方案。如何在忽略外部变化的属性注册中指定强制回调?例如,我需要实现一个只读的 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 类的预定义属性:-/
          【解决方案7】:

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

          【讨论】:

          • 实际上这是一个错误。
          • 很好的研究。如果你没有在这里布置得这么好,我会走同样痛苦的道路。同意@Inferis。
          • 这是一个错误吗?为什么只读 DependencyProperty 不允许使用 OneWayToSource 绑定?
          • 不是一个错误。这是设计使然并且有据可查。这是因为绑定引擎与依赖属性系统一起工作的方式(绑定目标必须DependencyProperty DP)。只读 DP 只能使用关联的DependencyPropertyKey 进行修改。要注册BindingExpression,引擎必须操作目标 DP 的元数据。由于DependencyPropertyKey 被认为是私有的以保证公共写保护,因此引擎将不得不忽略此密钥,结果无法在只读 DP 上注册绑定。
          【解决方案8】:

          您现在正在以错误的方向进行绑定。每当 IsModified 更改您正在创建的控件时,OneWayToSource 将尝试更新容器上的 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

          【讨论】:

          • 不,我想要的正是你描述的我不想做的场景。 FlagThingy.IsModified --> container.FlagIsModified
          • 因为提问者有一个模棱两可的问题而被扣分似乎有点矫枉过正。
          • @JaredPar:我看不出这个问题有什么含糊之处。问题指出 1) 有一个只读依赖属性 IsIsModified,2) OP 希望在 XAML 中声明对该属性的绑定,并且 3) 绑定应该在 OneWayToSource 模式下工作。您的解决方案实际上不起作用,因为如问题中所述,编译器不允许您在只读属性上声明绑定,并且它在概念上不起作用,因为 IsModified 是只读的,因此它值不能更改(通过绑定)。
          【解决方案9】:

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

          可能在你的情况下这可以:

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

          【讨论】:

          • 在这种情况下不使用 CLR 属性。
          • 您的意思是您刚刚定义了 DependencyProperty 并且能够编写 ?对我来说,如果我不添加 CLR 属性,它会说:“XML 命名空间中不存在属性 'IsModified'”。
          • 我相信设计时使用 clr 属性,而运行时实际上直接进入依赖属性(如果它是一个)。
          • 在我的情况下,CLR 属性是不必要的(我不使用代码中的 IsModified),但它仍然存在(只有一个公共设置器)。只需依赖属性注册,设计时和运行时都可以正常工作。
          • 绑定本身不使用 CLR 属性,但是当您在 XAML 中定义绑定时,必须将其转换为代码。我猜在这个阶段 XAML 解析器看到 IsModified 属性是只读的,并抛出异常(甚至在创建绑定之前)。
          猜你喜欢
          • 2010-10-10
          • 2012-02-05
          • 1970-01-01
          • 1970-01-01
          • 2019-02-08
          • 2014-12-04
          • 1970-01-01
          • 2020-04-25
          相关资源
          最近更新 更多