2011-02-05 110 views
3

我試圖將一個按鈕的IsMouseOver只讀依賴項屬性數據綁定到我的視圖模型中的布爾型讀/寫屬性。將XAML中的只讀依賴屬性數據綁定到ViewModel

基本上我需要按鈕的IsMouseOver屬性值讀取到視圖模型的屬性。

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

我得到一個編譯錯誤:「IsMouseOver」屬性是隻讀的並且不能從標記中設置。我在做什麼錯?

回答

5

沒有錯。這是一個limitation of WPF - 只讀屬性無法綁定OneWayToSource,除非源也是DependencyProperty

另一種方法是附加行爲。

+0

能否請你澄清你的「除非源也是一個DependencyProperty」的意思。我很確定你不能這麼做,或者我在這裏誤會了嗎? – 2011-02-05 01:15:02

0

許多人都提到過,這是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); 
    } 

} 
0

下面是我尋求這個問題的一般解決方案時所採取的粗略草案。它採用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>