Привязка в AttachedProperty типа коллекции к другому элементу - PullRequest
4 голосов
/ 28 мая 2011

Я хочу создать AttachedProperty типа Collection, который содержит ссылки на другие существующие элементы, как показано ниже:

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

Я не могу установить привязку в Коллекции. Я думаю, что я уже понял, в чем проблема. Элементы в коллекции не имеют DataContext, связанного с привязкой. Однако я понятия не имею, что я могу сделать против этого.

EDIT: Исправлено отсутствующее свойство Name кнопки. Примечание: я знаю, что привязка не может работать, потому что каждая привязка, которая явно не объявляет источник, будет использовать свой DataContext в качестве источника. Как я уже упоминал: у нас нет такого DataContext в моей коллекции, и нет никакого VisualTree, частью которого может быть несуществующий FrameworkElement;)

Может быть, кто-то имел подобную проблему в прошлом и нашел подходящее решение.

РЕДАКТИРОВАТЬ 2, связанных с H.B.s сообщение: Со следующим изменением предметов в коллекции это, кажется, теперь работает:

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

Интересное поведение: Когда вызывается обработчик события OnObjectsChanged, коллекция содержит ноль элементов ... Я предполагаю, что это потому, что создание элементов (выполненных в методе InitializeComponent) еще не завершено.

Btw. Как и ты Х.Б. сказал, что использование класса Container не является необходимым при использовании x: Reference. Есть ли какие-либо недостатки при использовании 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));
    }
}

Ответы [ 2 ]

2 голосов
/ 28 мая 2011

Я думаю, что ответ можно найти по следующим двум ссылкам:

Свойство 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, в частности, но у меня нетне пытался это сделать.

2 голосов
/ 28 мая 2011

Вы можете использовать x:Reference в .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 как

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

вызовет еще несколькоприятные ошибки.

...