4

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));
    }
}
H.B.
  • 166,899
  • 29
  • 327
  • 400
0xbadf00d
  • 17,405
  • 15
  • 67
  • 107
  • That sounds extremely familiar... – H.B. May 28 '11 at 17:04
  • @Ben | I'm sorry. I've simplified the code, beacause I want that you can understand what my intention is. I've fixed that in my initial post. Thus, that wasn't the problem ;) – 0xbadf00d May 28 '11 at 17:19
  • Figured it out myself, but now its a more complete sample. Interesting problem, upvoted with the hope that you will get a good answer. – Ben May 28 '11 at 17:29

2 Answers2

2

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.

H.B.
  • 166,899
  • 29
  • 327
  • 400
  • Thanks for your answer. Your sample has another problem: If you use "new UIPropertyMetadata(new List())", you assign the same instance of the List<> to all instances of "Objects". That is probably not your intention, right? – 0xbadf00d May 28 '11 at 18:11
  • I don't think that is what happens. Also, what do you mean by "another problem", i don't see any, i just pointed out what you *should not do*. – H.B. May 28 '11 at 18:23
  • What do you mean with "your property needs to have the right type"? Will it not work with DependencyObject, which should be the base of almost everything I might wanna use as a referenced object? How do you think I would need to use x:Reference in my scenario? EDIT: What I meant with "another problem" was the mentioned one in addition to the problem you've explained. – 0xbadf00d May 28 '11 at 18:27
  • You were right about the `List` i suppose, changed it to `null`. By right type i mean that the XAML-parser needs to be able to convert the list to the property. You can test which types satisfy the condition but using ArrayList in XAML and IList as type is one combination that works. – H.B. May 28 '11 at 18:33
  • You can reference anything, i just meant that `x:Reference` does not require the target property to be DP like the binding does. E.g. if you still want to use a wrapper and you write `Object="{x:Reference button1}"` the object property can be a normal CLR property. – H.B. May 28 '11 at 18:35
  • Thanks, I've updated my initial post. It's working now. Do you think 'Snowbear JIM-compiler's approach is more straightforward than this one? – 0xbadf00d May 28 '11 at 18:44
  • No, i don't, you do not need a binding here, you just need a reference to an object so i think this fits those needs perfectly. It also is less redundant since it no longer requires containers. – H.B. May 28 '11 at 19:05
  • Hum, when will the References be populated? I cannot observe the ValueChanged Event, because at this time, the collection is still empty. – 0xbadf00d May 29 '11 at 07:38
  • Edited my initial post and posted a quite good looking solution. – 0xbadf00d May 29 '11 at 08:25
2

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.

Snowbear
  • 16,924
  • 3
  • 43
  • 67
  • Thanks for your answer. Regarding to OnObjectsChanged event handler: I totally agree with you. I've changed my code for this example and left something senseless. Are there any advantages of your approach compared to that one using x:Reference (just in your opinion)? – 0xbadf00d May 28 '11 at 18:46
  • By the way, this check can be written more clearly in this way: `if (objects.Any(d => d.Object == null))` – Snowbear May 28 '11 at 18:49
  • 1
    and regarding your question - no, I do not think my solution has any advantages over `x:Reference`. Just wanted to get the reason why your code doesn't work (I didn't know the answer in advance, so now I know more). – Snowbear May 28 '11 at 18:51
  • +1 | I quite agree with you. Thanks for sharing your reading on this issue. – 0xbadf00d May 28 '11 at 21:06