Wpf 将只读依赖属性数据绑定到Xaml中的ViewModel

Wpf 将只读依赖属性数据绑定到Xaml中的ViewModel,wpf,data-binding,mvvm,Wpf,Data Binding,Mvvm,我正在尝试将按钮的IsMouseOver只读依赖项属性数据绑定到视图模型中的布尔读/写属性 基本上,我需要将按钮的IsMouseOver属性值读取到视图模型的属性中 <Button IsMouseOver="{Binding Path=IsMouseOverProperty, Mode=OneWayToSource}" /> 我收到一个编译错误:'IsMouseOver'属性是只读的,无法从标记中设置。我做错了什么?没有错。这是一个-只读属性不能绑定到源OneWayToSour

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

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

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

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

没有错。这是一个-只读属性不能绑定到源
OneWayToSource
,除非源也是一个
dependencProperty


另一种选择是附加行为。

正如许多人提到的,这是WPF中的一个bug,最好的方法是使用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);
    }

}

这是我在寻求这个问题的总体解决方案时所采用的方法的一个粗略草案。它使用css样式的格式来指定要绑定到模型属性(从DataContext获取的模型)的依赖属性;这也意味着它将只在框架元素上工作。
我还没有完全测试过它,但是happy path对于我运行的几个测试用例来说效果很好

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;
        }
    }
}
公共类绑定信息
{
内部字符串sourceString=null;
public dependencProperty源{get;internal set;}
公共字符串targetProperty{get;private set;}
public bool isResolved=>source!=null;
公共绑定信息(字符串源、字符串目标)
{
this.sourceString=source;
this.targetProperty=target;
验证();
}
私有void validate()
{
//验证targetProperty是有效的c#属性访问路径
如果(!targetProperty.Split('.'))
.All(p=>Identifier.IsMatch(p)))
抛出新异常(“无效的目标属性-”+targetProperty);
//验证sourceString是否为[Class].[DependencyProperty]格式的字符串。
如果(!sourceString.Split('.'))
.All(p=>Identifier.IsMatch(p)))
抛出新异常(“无效的源属性-”+sourceString);
}
私有静态只读正则表达式标识符=新正则表达式(@“[\u a-z][\w]*$”,RegexOptions.IgnoreCase);
}
[TypeConverter(typeof(BindingInfoConverter))]
公共类绑定信息组
{
私有列表_infoList=新列表();
公共IEnumerable信息列表
{
获取{return _infoList.ToArray();}
设置
{
_infoList.Clear();
如果(值!=null)\u infoList.AddRange(值);
}
}
}
公共类BindingInfoConverter:TypeConverter
{
公共覆盖布尔CanConvertFrom(ITypeScriptorContext上下文,类型sourceType)
{
if(sourceType==typeof(string))返回true;
返回base.CanConvertFrom(上下文,sourceType);
}
//对于复杂到字符串的转换,重写CanConvertTo以返回true。
公共覆盖布尔CanConvertTo(ITypeDescriptorContext上下文,类型destinationType)
{
if(destinationType==typeof(string))返回true;
返回base.CanConvertTo(上下文,destinationType);
}
//重写ConvertFrom以将字符串转换为复杂的实例。
公共重写对象ConvertFrom(ITypeDescriptorContext上下文、CultureInfo区域性、对象值)
{
字符串文本=作为字符串的值;
返回新的BindingInfoGroup
{
InfoList=text.Split(新[]{';'},StringSplitOptions.RemoveEmptyEntries)
.选择(binfo=>
{
var parts=binfo.Split(新的[]{':'},StringSplitOptions.RemoveEmptyEntries);
如果(parts.Length!=2)抛出新异常(“无效绑定信息-”+binfo);
返回新的BindingInfo(零件[0].Trim(),零件[1].Trim());
})
};
}
//重写ConvertTo以将复杂实例转换为字符串。
公共重写对象转换为(ITypeDescriptorContext上下文、CultureInfo区域性、,
对象值,类型destinationType)
{
var bgroup=作为BindingInfoGroup的值;
返回bgroup.InfoList
.Select(bi=>$“{bi.sourceString}:{bi.targetProperty};”)
.Aggregate((n,p)=>n+=$“{p}”)
.Trim();
}
public override bool GetStandardValuesSupported(ITypeScriptorContext上下文)=>false;
}
公共类绑定
{
#区域字段
私有静态ConcurrentDictionary _属性=
新的ConcurrentDictionary();
#端区
#区域单向绑定
公共静态只读从属属性单向绑定属性=
DependencyProperty.RegisterAttached(“单向绑定”、typeof(BindingInfoGroup)、typeof(Bindings)、new FrameworkPropertyMetadata
{
DefaultValue=null,
PropertyChangedCallback=(x,y)=>
{
var fwe=x作为框架元素;
如果(fwe==null)返回;
//解析绑定
决心(fwe);
//添加更改代理
(GetOnewayBindings(fwe)?.InfoList??新绑定信息[0])
.Where(bi=>bi.isResolved)
托利斯先生()
.ForEach(bi=>
{
var descriptor=DependencyPropertyDescriptor.FromProperty(bi.source,fwe.GetType());
PropertyChangeHandler侦听器=null;
if(_Properties.TryGetValue(bi.source,out listener))
{
descriptor.RemoveValueChanged(fwe,listener.callb
<Grid ab:Bindings.OnewayBindings="IsMouseOver:mouseOver;">...</Grid>