Attached property of type list
Asked Answered
O

1

19

I want to create an attached property that can be used with this syntax:

<Button>
  <Image .../>
  <ui:ToolbarItem.DisplayFilter>
    <TabItem .../>
    <TabItem .../>
    <TabItem .../>
  </ui:ToolbarItem.DisplayFilter>
</Button> 

This is my attempt at doing so:

public class ToolbarItem
{
  /// <summary>
  /// Identifies the DisplayFilter attached property. 
  /// </summary>
  public static readonly DependencyProperty DisplayFilterProperty =
    DependencyProperty.RegisterAttached(
     "DisplayFilter",
     typeof( IList ),
     typeof( ToolbarItem )
    );

  public static IList GetDisplayFilter( Control item ) {
    return (IList)item.GetValue( DisplayFilterProperty );
  }

  public static void SetDisplayFilter( Control item, IList value ) {
    item.SetValue( DisplayFilterProperty, value );
  }
}

This, however, is causing an exception at parse-time -- System.ArgumentException: TabItem is not a valid value for property 'DisplayFilter'. So how do I configure my attached property so that I can use the desired XAML syntax?

Oxidize answered 19/9, 2009 at 16:34 Comment(0)
M
35

Remember that XAML is basically just a shorthand form of object creation. So to create a collection/list as the value for the attached DisplayFilter property you would have to enclose those TabItems inside another collection tag. If you don't want to do that, which is understandable, you have to initialize the collection the first time the property is accessed.

There is just one problem with this: The getter method is skipped by the XAML reader as an optimization. You can prevent this behavior by choosing a different name for the name argument to the RegisterAttached call:

DependencyProperty.RegisterAttached("DisplayFilterInternal", ...)

Then the property getter will be called and you can check for null. You can read more about that in this blog post.

Edit: Seems like the linked blog post isn't that clear. You change only the name of the string passed to RegisterAttached, not the name of the static get/set methods:

public static readonly DependencyProperty DisplayFilterProperty =
    DependencyProperty.RegisterAttached(
        "DisplayFilterInternal",
        typeof(IList),
        typeof(ToolbarItem));

public static TabItemCollection GetDisplayFilter(Control item)
{ ... }

public static void SetDisplayFilter(Control item, IList value)
{ ... }

You have to initialize the collection in the GetDisplayFilter method:

public static TabItemCollection GetDisplayFilter(Control item)
{
    var collection = (IList)item.GetValue(DisplayFilterProperty);
    if (collection == null) {
        collection = new List<object>();
        item.SetValue(DisplayFilterProperty, collection);
    }
    return collection;
}

It seems that you only add TabItem elements to that collection. Then you can make the collection type-safe, but using IList<T> does not work since the XAML parser cannot invoke the generic method Add(T). Collection<T> and List<T> also implement the non-generic IList interface and can be used in this case. I would suggest to create a new collection type in case you want to do some changes to the collection in the future:

public class TabItemCollection : Collection<TabItem>
{
}

If you don't care about setting the collection explicitly like this:

<ui:ToolbarItem.DisplayFilter>
    <ui:TabItemCollection>
        <TabItem/>
    </ui:TabItemCollection>
</ui:ToolbarItem.DisplayFilter>

you can remove the SetDisplayFilter method.

To summarize:

public class TabItemCollection : Collection<TabItem>
{
}

public class ToolbarItem
{
    public static readonly DependencyProperty DisplayFilterProperty =
        DependencyProperty.RegisterAttached(
            "DisplayFilterInternal", // Shadow the name so the parser does not skip GetDisplayFilter
            typeof(TabItemCollection),
            typeof(ToolbarItem));

    public static TabItemCollection GetDisplayFilter(Control item)
    {
        var collection = (TabItemCollection)item.GetValue(DisplayFilterProperty);
        if (collection == null) {
            collection = new TabItemCollection();
            item.SetValue(DisplayFilterProperty, collection);
        }
        return collection;
    }

    // Optional, see above note
    //public static void SetDisplayFilter(Control item, TabItemCollection value)
    //{
    //    item.SetValue(DisplayFilterProperty, value);
    //}
}
Mongol answered 19/9, 2009 at 17:2 Comment(8)
Then there must be something else wrong because the code posted works fine. See nopaste.org/p/adqfm5EPi for a short and complete example.Mongol
It seems like you cannot use resources this way. If you either don't use resources or an explicit collection it seems to work.Mongol
@gix: This works for me, but the collection which is passed to the setter is empty. When are the elements defined in the XAML supposed to end up in the list?Paramount
K, I just noticed it myself by using an ObservableCollection. The objects are added only after the setter call.Paramount
@gix: Do you think making it Freezable, similar to Int32Collection (see Remarks) would work for use as a resource?Garibold
The same approach was taken here: blogs.msdn.com/b/johngossman/archive/2008/07/28/…Oxidize
Also, making DisplayFilterProperty public is optional - XAML won't use it anywayGoalkeeper
I think this may no longer work, now many years later. My property is ButtonAreaItems, registered as ButtonAreaItemsInternal. I get a XamlParseException, with inner exception: "InvalidOperationException: Property path is not valid. 'DialogHelper' does not have a public property named 'ButtonAreaItems'."Alithea

© 2022 - 2024 — McMap. All rights reserved.