在类型集合的AttachedProperty中绑定到另一个元素

时间:2011-05-28 16:15:13

标签: wpf binding dependency-properties attached-properties

我想创建一个Type Collection的AttachedProperty,它包含对其他现有元素的引用,如下所示:

<Window x:Class="myNamespace.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:myNamespace"
        Title="MainWindow" Height="350" Width="525">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <ContentPresenter>
            <ContentPresenter.Content>
                <Button>
                    <local:DependencyObjectCollectionHost.Objects>
                        <local:DependencyObjectCollection>
                            <local:DependencyObjectContainer Object="{Binding ElementName=myButton}"/>
                        </local:DependencyObjectCollection>
                    </local:DependencyObjectCollectionHost.Objects>
                </Button>
            </ContentPresenter.Content>
        </ContentPresenter>
        <Button x:Name="myButton" Grid.Row="1"/>
    </Grid>
</Window>

因此,我创建了一个名为ObjectContainer的泛型类,以获得使用Binding实现此目的的可能性:

public class ObjectContainer<T> : DependencyObject
    where T : DependencyObject
{
    static ObjectContainer()
    {
        ObjectProperty = DependencyProperty.Register
        (
            "Object",
            typeof(T),
            typeof(ObjectContainer<T>),
            new PropertyMetadata(null)
        );
    }

    public static DependencyProperty ObjectProperty;

    [Bindable(true)]
    public T Object
    {
        get { return (T)this.GetValue(ObjectProperty); }
        set { this.SetValue(ObjectProperty, value); }
    }
}


public class DependencyObjectContainer : ObjectContainer<DependencyObject> { }
public class DependencyObjectCollection : Collection<DependencyObjectContainer> { }


public static class DependencyObjectCollectionHost
{
    static DependencyObjectCollectionHost()
    {
        ObjectsProperty = DependencyProperty.RegisterAttached
        (
            "Objects",
            typeof(DependencyObjectCollection),
            typeof(DependencyObjectCollectionHost),
            new PropertyMetadata(null, OnObjectsChanged)
        );
    }

    public static DependencyObjectCollection GetObjects(DependencyObject dependencyObject)
    {
        return (DependencyObjectCollection)dependencyObject.GetValue(ObjectsProperty);
    }

    public static void SetObjects(DependencyObject dependencyObject, DependencyObjectCollection value)
    {
        dependencyObject.SetValue(ObjectsProperty, value);
    }

    public static readonly DependencyProperty ObjectsProperty;

    private static void OnObjectsChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var objects = (DependencyObjectCollection)e.NewValue;

        if (objects.Count != objects.Count(d => d.Object != null))
            throw new ArgumentException();
    }
}

我无法在Collection中建立任何绑定。我想我已经知道了,问题是什么。 Collection中的元素没有与Binding相关的DataContext。但是,我不知道我能对付它做什么。

修改 修复了Button缺少的Name属性。 注意:我知道绑定不起作用,因为每个未明确声明Source的Binding将使用它的DataContext作为它的Source。就像我已经提到的那样:我的Collection中没有这样的DataContext,并且没有VisualTree,其中不存在的FrameworkElement可能是其中的一部分;)

过去可能有人遇到类似的问题,并找到了合适的解决方案。

EDIT2与H.B.s帖子有关: 通过对集合中的项目进行以下更改,它现在似乎可以正常工作:

<local:DependencyObjectContainer Object="{x:Reference myButton}"/>

有趣的行为: 当调用OnObjectsChanged事件处理程序时,该集合包含零元素......我认为这是因为元素的创建(在InitializeComponent方法中完成)尚未完成。

顺便说一下。正如你H.B.表示使用x:Reference时不需要使用Container类。使用x时是否有任何缺点:我在第一时间没有看到的参考?

EDIT3 解决方案: 我添加了一个自定义附加事件,以便在集合发生变化时收到通知。

public class DependencyObjectCollection : ObservableCollection<DependencyObject> { }

public static class ObjectHost
{
    static KeyboardObjectHost()
    {
        ObjectsProperty = DependencyProperty.RegisterAttached
        (
            "Objects",
            typeof(DependencyObjectCollection),
            typeof(KeyboardObjectHost),
            new PropertyMetadata(null, OnObjectsPropertyChanged)
        );

        ObjectsChangedEvent = EventManager.RegisterRoutedEvent
        (
            "ObjectsChanged",
            RoutingStrategy.Bubble,
            typeof(RoutedEventHandler),
            typeof(KeyboardObjectHost)
        );
    }

    public static DependencyObjectCollection GetObjects(DependencyObject dependencyObject)
    {
        return (DependencyObjectCollection)dependencyObject.GetValue(ObjectsProperty);
    }

    public static void SetObjects(DependencyObject dependencyObject, DependencyObjectCollection value)
    {
        dependencyObject.SetValue(ObjectsProperty, value);
    }

    public static void AddObjectsChangedHandler(DependencyObject dependencyObject, RoutedEventHandler h)
    {
        var uiElement = dependencyObject as UIElement;

        if (uiElement != null)
            uiElement.AddHandler(ObjectsChangedEvent, h);
        else
            throw new ArgumentException(string.Format("Cannot add handler to object of type: {0}", dependencyObject.GetType()), "dependencyObject");
    }

    public static void RemoveObjectsChangedHandler(DependencyObject dependencyObject, RoutedEventHandler h)
    {
        var uiElement = dependencyObject as UIElement;

        if (uiElement != null)
            uiElement.RemoveHandler(ObjectsChangedEvent, h);
        else
            throw new ArgumentException(string.Format("Cannot remove handler from object of type: {0}", dependencyObject.GetType()), "dependencyObject");
    }

    public static bool CanControlledByKeyboard(DependencyObject dependencyObject)
    {
        var objects = GetObjects(dependencyObject);
        return objects != null && objects.Count != 0;
    }

    public static readonly DependencyProperty ObjectsProperty;
    public static readonly RoutedEvent ObjectsChangedEvent;

    private static void OnObjectsPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        Observable.FromEvent<NotifyCollectionChangedEventArgs>(e.NewValue, "CollectionChanged")
        .DistinctUntilChanged()
        .Subscribe(args =>
        {
            var objects = (DependencyObjectCollection)args.Sender;

            if (objects.Count == objects.Count(d => d != null)
                OnObjectsChanged(dependencyObject);
            else
                throw new ArgumentException();
        });
    }

    private static void OnObjectsChanged(DependencyObject dependencyObject)
    {
        RaiseObjectsChanged(dependencyObject);
    }

    private static void RaiseObjectsChanged(DependencyObject dependencyObject)
    {
        var uiElement = dependencyObject as UIElement;
        if (uiElement != null)
            uiElement.RaiseEvent(new RoutedEventArgs(ObjectsChangedEvent));
    }
}

2 个答案:

答案 0 :(得分:2)

您可以在.NET 4中使用x:Reference,它比ElementName更“智能”,与绑定不同,它不需要目标成为依赖属性。

您甚至可以删除容器类,但您的属性需要具有正确的类型,以便ArrayList可以直接转换为属性值,而不是将整个列表添加为项目。直接使用x:References工作。

xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
<local:AttachedProperties.Objects>
    <col:ArrayList>
        <x:Reference>button1</x:Reference>
        <x:Reference>button2</x:Reference>
    </col:ArrayList>
</local:AttachedProperties.Objects>
public static readonly DependencyProperty ObjectsProperty =
            DependencyProperty.RegisterAttached
            (
            "Objects",
            typeof(IList),
            typeof(FrameworkElement),
            new UIPropertyMetadata(null)
            );
public static IList GetObjects(DependencyObject obj)
{
    return (IList)obj.GetValue(ObjectsProperty);
}
public static void SetObjects(DependencyObject obj, IList value)
{
    obj.SetValue(ObjectsProperty, value);
}

进一步将x:References写为

<x:Reference Name="button1"/>
<x:Reference Name="button2"/>

会导致一些更好的错误。

答案 1 :(得分:2)

我认为答案可以在以下两个链接中找到:

Binding.ElementName Property
XAML Namescopes and Name-related APIs

尤其是第二种状态:

  

FrameworkElement 具有FindName,RegisterName和UnregisterName方法。如果您调用这些方法的对象拥有XAML名称范围,则这些方法将调用相关XAML名称范围的方法。否则,检查父元素以查看它是否拥有XAML名称范围,并且此过程以递归方式继续,直到找到XAML名称范围(由于XAML处理器行为,保证在根处有XAML名称范围)。 FrameworkContentElement具有类似的行为,但没有FrameworkContentElement将拥有XAML名称范围。这些方法存在于FrameworkContentElement上,因此最终可以将调用转发到 FrameworkElement父元素。

因此,您的示例中的问题是由于您的类最多只有DependencyObjects但其中没有一个是FrameworkElement。不是FrameworkElement,它无法提供Parent属性来解析Binding.ElementName中指定的名称。

但这不是结束。为了解析Binding.ElementName您的容器中的名称,不仅应该是FrameworkElement,还应该FrameworkElement.Parent。填充附加属性不会设置此属性,您的实例应该是按钮的逻辑子项,因此它将能够解析名称。

因此,我必须对您的代码进行一些更改才能使其正常工作(解析ElementName),但在此状态下,我认为它不符合您的需求。我正在粘贴下面的代码,以便您可以使用它。

public class ObjectContainer<T> : FrameworkElement
    where T : DependencyObject
{
    static ObjectContainer()
    {
        ObjectProperty = DependencyProperty.Register("Object", typeof(T), typeof(ObjectContainer<T>), null);
    }

    public static DependencyProperty ObjectProperty;

    [Bindable(true)]
    public T Object
    {
        get { return (T)this.GetValue(ObjectProperty); }
        set { this.SetValue(ObjectProperty, value); }
    }
}


public class DependencyObjectContainer : ObjectContainer<DependencyObject> { }

public class DependencyObjectCollection : FrameworkElement
{
    private object _child;
    public Object Child
    {
        get { return _child; }
        set
        {
            _child = value;
            AddLogicalChild(_child);
        }
    }
}

public static class DependencyObjectCollectionHost
{
    static DependencyObjectCollectionHost()
    {
        ObjectsProperty = DependencyProperty.RegisterAttached
        (
            "Objects",
            typeof(DependencyObjectCollection),
            typeof(DependencyObjectCollectionHost),
            new PropertyMetadata(null, OnObjectsChanged)
        );
    }

    public static DependencyObjectCollection GetObjects(DependencyObject dependencyObject)
    {
        return (DependencyObjectCollection)dependencyObject.GetValue(ObjectsProperty);
    }

    public static void SetObjects(DependencyObject dependencyObject, DependencyObjectCollection value)
    {
        dependencyObject.SetValue(ObjectsProperty, value);
    }

    public static readonly DependencyProperty ObjectsProperty;

    private static void OnObjectsChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        ((Button) dependencyObject).Content = e.NewValue;
        var objects = (DependencyObjectCollection)e.NewValue;

//      this check doesn't work anyway. d.Object was populating later than this check was performed
//      if (objects.Count != objects.Count(d => d.Object != null))
//          throw new ArgumentException();
    }
}

可能你仍然可以通过实施INameScope interface及其FindName方法来实现这一点,但我还没有尝试过这样做。