【问题标题】:Databind a read only dependency property to ViewModel in Xaml将只读依赖属性数据绑定到 Xaml 中的 ViewModel
【发布时间】:2011-02-05 00:37:50
【问题描述】:

我正在尝试将 Button 的 IsMouseOver 只读依赖属性数据绑定到我的视图模型中的布尔读/写属性。

基本上我需要将 Button 的 IsMouseOver 属性值读取到视图模型的属性中。

<Button IsMouseOver="{Binding Path=IsMouseOverProperty, Mode=OneWayToSource}" />

我收到一个编译错误:'IsMouseOver' 属性是只读的,不能从标记中设置。我做错了什么?

【问题讨论】:

    标签: wpf data-binding mvvm


    【解决方案1】:

    没有错。这是limitation of WPF - 只读属性不能绑定OneWayToSource,除非源也是DependencyProperty

    另一种选择是附加行为。

    【讨论】:

    • 您能否澄清“除非源也是 DependencyProperty”的意思。我很确定你也不能这样做,还是我在这里弄错了?
    • +1,我试过了,它确实像你说的那样工作:) 似乎只能从后面的代码而不是 Xaml 中工作。很高兴知道!
    • 非常感谢肯特。这很有帮助。顺便说一句.. 很高兴听到像您这样的人的消息.. 我遇到了您的许多有用的技术文章。非常感谢。
    • 顺便说一句,MS Connect 的链接已损坏。
    【解决方案2】:

    正如许多人所提到的,这是 WPF 中的一个错误,最好的方法是使用 Tim/Kent 建议的附加属性。这是我在项目中使用的附加属性。我故意这样做是为了可读性、单元可测试性以及坚持使用 MVVM,而不需要在视图上进行代码隐藏,以便在任何地方手动处理事件。

    public interface IMouseOverListener
    {
        void SetIsMouseOver(bool value);
    }
    public static class ControlExtensions
    {
        public static readonly DependencyProperty MouseOverListenerProperty =
            DependencyProperty.RegisterAttached("MouseOverListener", typeof (IMouseOverListener), typeof (ControlExtensions), new PropertyMetadata(OnMouseOverListenerChanged));
    
        private static void OnMouseOverListenerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var element = ((UIElement)d);
    
            if(e.OldValue != null)
            {
                element.MouseEnter -= ElementOnMouseEnter;
                element.MouseLeave -= ElementOnMouseLeave;
            }
    
            if(e.NewValue != null)
            {
                element.MouseEnter += ElementOnMouseEnter;
                element.MouseLeave += ElementOnMouseLeave;
            }
        }
    
        public static void SetMouseOverListener(UIElement element, IMouseOverListener value)
        {
            element.SetValue(MouseOverListenerProperty, value);
        }
    
        public static IMouseOverListener GetMouseOverListener(UIElement element)
        {
            return (IMouseOverListener) element.GetValue(MouseOverListenerProperty);
        }
    
        private static void ElementOnMouseLeave(object sender, MouseEventArgs mouseEventArgs)
        {
            var element = ((UIElement)sender);
            var listener = GetMouseOverListener(element);
            if(listener != null)
                listener.SetIsMouseOver(false);
        }
    
        private static void ElementOnMouseEnter(object sender, MouseEventArgs mouseEventArgs)
        {
            var element = ((UIElement)sender);
            var listener = GetMouseOverListener(element);
            if (listener != null)
                listener.SetIsMouseOver(true);
        }
    
    }
    

    【讨论】:

      【解决方案3】:

      这是我在寻求解决此问题的一般解决方案时所采用的粗略草案。它采用 css 样式的格式来指定要绑定到模型属性(从 DataContext 获得的模型)的 Dependency-Properties;这也意味着它只适用于 FrameworkElements。
      我还没有彻底测试它,但是对于我运行的几个测试用例来说,快乐的路径工作得很好。

      public class BindingInfo
      {
          internal string sourceString = null;
          public DependencyProperty source { get; internal set; }
          public string targetProperty { get; private set; }
      
          public bool isResolved => source != null;
      
          public BindingInfo(string source, string target)
          {
              this.sourceString = source;
              this.targetProperty = target;
              validate();
          }
          private void validate()
          {
              //verify that targetProperty is a valid c# property access path
              if (!targetProperty.Split('.')
                                 .All(p => Identifier.IsMatch(p)))
                  throw new Exception("Invalid target property - " + targetProperty);
      
              //verify that sourceString is a [Class].[DependencyProperty] formatted string.
              if (!sourceString.Split('.')
                               .All(p => Identifier.IsMatch(p)))
                  throw new Exception("Invalid source property - " + sourceString);
          }
      
          private static readonly Regex Identifier = new Regex(@"[_a-z][_\w]*$", RegexOptions.IgnoreCase);
      }
      
      [TypeConverter(typeof(BindingInfoConverter))]
      public class BindingInfoGroup
      {
          private List<BindingInfo> _infoList = new List<BindingInfo>();
          public IEnumerable<BindingInfo> InfoList
          {
              get { return _infoList.ToArray(); }
              set
              {
                  _infoList.Clear();
                  if (value != null) _infoList.AddRange(value);
              }
          }
      }
      
      public class BindingInfoConverter: TypeConverter
      {
          public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
          {
              if (sourceType == typeof(string)) return true;
              return base.CanConvertFrom(context, sourceType);
          }
      
          // Override CanConvertTo to return true for Complex-to-String conversions. 
          public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
          {
              if (destinationType == typeof(string)) return true;
              return base.CanConvertTo(context, destinationType);
          }
      
          // Override ConvertFrom to convert from a string to an instance of Complex. 
          public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
          {
              string text = value as string;
              return new BindingInfoGroup
              {
                  InfoList = text.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
                                 .Select(binfo =>
                                 {
                                     var parts = binfo.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
                                     if (parts.Length != 2) throw new Exception("invalid binding info - " + binfo);
                                     return new BindingInfo(parts[0].Trim(), parts[1].Trim());
                                 })
              };
          }
      
          // Override ConvertTo to convert from an instance of Complex to string. 
          public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture,
                                           object value, Type destinationType)
          {
              var bgroup = value as BindingInfoGroup;
              return bgroup.InfoList
                           .Select(bi => $"{bi.sourceString}:{bi.targetProperty};")
                           .Aggregate((n, p) => n += $"{p} ")
                           .Trim();
          }
      
          public override bool GetStandardValuesSupported(ITypeDescriptorContext context) => false;
      }
      
      public class Bindings
      {
          #region Fields
          private static ConcurrentDictionary<DependencyProperty, PropertyChangeHandler> _Properties = 
                         new ConcurrentDictionary<DependencyProperty, PropertyChangeHandler>();
      
          #endregion
      
      
          #region OnewayBindings
          public static readonly DependencyProperty OnewayBindingsProperty =
              DependencyProperty.RegisterAttached("OnewayBindings", typeof(BindingInfoGroup), typeof(Bindings), new FrameworkPropertyMetadata
              {
                  DefaultValue = null,
                  PropertyChangedCallback = (x, y) =>
                  {
                      var fwe = x as FrameworkElement;
                      if (fwe == null) return;
      
                      //resolve the bindings
                      resolve(fwe);
      
                      //add change delegates
                      (GetOnewayBindings(fwe)?.InfoList ?? new BindingInfo[0])
                      .Where(bi => bi.isResolved)
                      .ToList()
                      .ForEach(bi =>
                      {
                          var descriptor = DependencyPropertyDescriptor.FromProperty(bi.source, fwe.GetType());
                          PropertyChangeHandler listener = null;
                          if (_Properties.TryGetValue(bi.source, out listener))
                          {
                              descriptor.RemoveValueChanged(fwe, listener.callback); //cus there's no way to check if it had one before...
                              descriptor.AddValueChanged(fwe, listener.callback);
                          }
                      });
                  }
              });
      
          private static void resolve(FrameworkElement element)
          {
              var bgroup = GetOnewayBindings(element);
              bgroup.InfoList
                    .ToList()
                    .ForEach(bg =>
                    {
                        //source
                        var sourceParts = bg.sourceString.Split('.');
                        if (sourceParts.Length == 1)
                        {
                            bg.source = element.GetType()
                                               .baseTypes() //<- flattens base types, including current type
                                               .SelectMany(t => t.GetRuntimeFields()
                                                                 .Where(p => p.IsStatic)
                                                                 .Where(p => p.FieldType == typeof(DependencyProperty)))
                                               .Select(fi => fi.GetValue(null) as DependencyProperty)
                                               .FirstOrDefault(dp => dp.Name == sourceParts[0])
                                               .ThrowIfNull($"Dependency Property '{sourceParts[0]}' was not found");
                        }
                        else
                        {
                            //resolve the dependency property [ClassName].[PropertyName]Property - e.g FrameworkElement.DataContextProperty
                            bg.source = Type.GetType(sourceParts[0])
                                            .GetField(sourceParts[1])
                                            .GetValue(null)
                                            .ThrowIfNull($"Dependency Property '{bg.sourceString}' was not found") as DependencyProperty;
                        }
      
                        _Properties.GetOrAdd(bg.source, ddp => new PropertyChangeHandler { property = ddp }); //incase it wasnt added before.
                    });
          }
      
      
          public static BindingInfoGroup GetOnewayBindings(FrameworkElement source) 
              => source.GetValue(OnewayBindingsProperty) as BindingInfoGroup;
          public static void SetOnewayBindings(FrameworkElement source, string value) 
              => source.SetValue(OnewayBindingsProperty, value);
          #endregion
      
      }
      
      public class PropertyChangeHandler
      {
          internal DependencyProperty property { get; set; }
      
          public void callback(object obj, EventArgs args)
          {
              var fwe = obj as FrameworkElement;
              var target = fwe.DataContext;
              if (fwe == null) return;
              if (target == null) return;
      
              var bg = Bindings.GetOnewayBindings(fwe);
              if (bg == null) return;
              else bg.InfoList
                     .Where(bi => bi.isResolved)
                     .Where(bi => bi.source == property)
                     .ToList()
                     .ForEach(bi =>
                     {
                         //transfer data to the object
                         var data = fwe.GetValue(property);
                         KeyValuePair<object, PropertyInfo>? pinfo = resolveProperty(target, bi.targetProperty);
                         if (pinfo == null) return;
                         else pinfo.Value.Value.SetValue(pinfo.Value.Key, data);
                     });
      
          }
          private KeyValuePair<object, PropertyInfo>? resolveProperty(object target, string path)
          {
              try
              {
                  var parts = path.Split('.');
                  if (parts.Length == 1) return new KeyValuePair<object, PropertyInfo>(target, target.GetType().GetProperty(parts[0]));
                  else //(parts.Length>1)
                      return resolveProperty(target.GetType().GetProperty(parts[0]).GetValue(target),
                                             string.Join(".", parts.Skip(1)));
              }
              catch (Exception e) //too lazy to care :D
              {
                  return null;
              }
          }
      }
      

      并且要使用 XAML...

      <Grid ab:Bindings.OnewayBindings="IsMouseOver:mouseOver;">...</Grid>
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-12-18
        • 1970-01-01
        • 2011-06-14
        • 2011-06-25
        • 2017-07-25
        • 2019-12-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多