Databind a read only dependency property to ViewModel in Xaml

I'm trying to databind a Button's IsMouseOver read-only dependency property to a boolean read/write property in my view model.

Basically I need the Button's IsMouseOver property value to be read to a view model's property.

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

I'm getting a compile error: 'IsMouseOver' property is read-only and cannot be set from markup. What am I doing wrong?


No mistake. This is a limitation of WPF - a read-only property cannot be bound OneWayToSource unless the source is also a DependencyProperty .

An alternative is an attached behavior.


As many people have mentioned, this is a bug in WPF and the best way is to do it is attached property like Tim/Kent suggested. Here is the attached property I use in my project. I intentionally do it this way for readability, unit testability, and sticking to MVVM without codebehind on the view to handle the events manually everywhere.

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

}

Here's a rough draft of what i resorted to while seeking a general solution to this problem. It employs a css-style formatting to specify Dependency-Properties to be bound to model properties (models gotten from the DataContext); this also means it will work only on FrameworkElements.
I haven't thoroughly tested it, but the happy path works just fine for the few test cases i ran.

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

And to use the XAML...

<Grid ab:Bindings.OnewayBindings="IsMouseOver:mouseOver;">...</Grid>
链接地址: http://www.djcxy.com/p/50466.html

上一篇: 在数据绑定flowdocument中插入分隔符(空白行)

下一篇: 在Xaml中将只读依赖项属性数据绑定到ViewModel