在类型集合的 AttachedProperty 内绑定到其他元素

发布于 2024-11-09 23:12:41 字数 7138 浏览 4 评论 0原文

我想创建一个类型集合的 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 中的元素没有与 Binding 相关的 DataContext。然而,我不知道我能做什么来对抗它。

编辑: 修复了按钮缺失的名称属性。 注意:我知道绑定无法工作,因为每个未显式声明 Source 的 Binding 都会使用它的 DataContext 作为 Source。就像我已经提到的:我们的 Collection 中没有这样的 DataContext,也没有 VisualTree,其中不存在的 FrameworkElement 可能是其中的一部分;)

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

与 HBs 帖子相关的 EDIT2: 通过对集合中的项目进行以下更改,它现在似乎可以工作:

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

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

顺便提一句。正如您 HB 所说,使用 x:Reference 时不需要使用 Container 类。使用 x:Reference 时是否有我一开始没有看到的缺点?

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

I want to create an AttachedProperty of Type Collection, which contains references to other existing elements, as shown below:

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

Therefore I've created a generic class, called ObjectContainer, to gain the possibility to do so with 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();
    }
}

I'm not able to establish any binding within the Collection. I think I've already figured out, what the problem is. The elements in the Collection have no DataContext related to the Binding. However, I've no clue what I can do against it.

EDIT:
Fixed the missing Name Property of the Button.
Note: I know that the binding cannot work, because every Binding which doesn't declare a Source explicitly will use it's DataContext as it's Source. Like I already mentioned: We don't have such a DataContext within my Collection and there's no VisualTree where the non-existing FrameworkElement could be part of ;)

Maybe someone had a similiar problem in the past and found a suitable solution.

EDIT2 related to H.B.s post:
With the following change to the items within the collection it seems to work now:

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

Interesting behavior:
When the OnObjectsChanged Event-Handler is called, the collection contains zero elements ... I assume that's because the creation of the elements (done within the InitializeComponent method) hasn't finished yet.

Btw. As you H.B. said the use of the Container class is unnecessary when using x:Reference. Are there any disadvantages when using x:Reference which I don't see at the first moment?

EDIT3 Solution:
I've added a custom Attached Event in order to be notified, when the Collection changed.

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

白龙吟 2024-11-16 23:12:41

您可以在 . NET 4 中,它比 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 as

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

会导致一些更好的错误。

You can use x:Reference in .NET 4, it's "smarter" than ElementName and unlike bindings it does not require the target to be a dependency property.

You can even get rid of the container class, but your property needs to have the right type so the ArrayList can directly convert to the property value instead of adding the whole list as an item. Using x:References directly will not work.

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

Further writing the x:References as

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

will cause some more nice errors.

蘸点软妹酱 2024-11-16 23:12:41

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

Binding.ElementName属性
XAML 名称范围和名称相关 API

尤其是第二个状态:

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 接口 及其 FindName 方法,但我还没有尝试这样做。

I think the answer can be found in the following two links:

Binding.ElementName Property
XAML Namescopes and Name-related APIs

Especially the second states:

FrameworkElement has FindName, RegisterName and UnregisterName methods. If the object you call these methods on owns a XAML namescope, the methods call into the methods of the relevant XAML namescope. Otherwise, the parent element is checked to see if it owns a XAML namescope, and this process continues recursively until a XAML namescope is found (because of the XAML processor behavior, there is guaranteed to be a XAML namescope at the root). FrameworkContentElement has analogous behaviors, with the exception that no FrameworkContentElement will ever own a XAML namescope. The methods exist on FrameworkContentElement so that the calls can be forwarded eventually to a FrameworkElement parent element.

So the issue in your sample caused by the fact that your classes are DependencyObjects at most but none of them is FrameworkElement. Not being a FrameworkElement it cannot provide Parent property to resolve name specified in Binding.ElementName.

But this isn't end. In order to resolve names from Binding.ElementName your container not only should be a FrameworkElement but it should also have FrameworkElement.Parent. Populating attached property doesn't set this property, your instance should be a logical child of your button so it will be able to resolve the name.

So I had to make some changes into your code in order to make it working (resolving ElementName), but at this state I do not think it meets your needs. I'm pasting the code below so you can play with it.

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

Probably you still can make this working by implementing INameScope interface and its FindName method particularly but I haven't tried doing that.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文