views:

391

answers:

1

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?

+3  A: 

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, list);
        }
        return collection;
    }

    // Optional, see above note
    //public static void SetDisplayFilter(Control item, TabItemCollection value)
    //{
    //    item.SetValue(DisplayFilterProperty, value);
    //}
}
gix
Yes, I want to avoid the extra collection tag. The blog's suggestion worked insofar as the new GetDisplayFilterInternal() is called, but then I get System.ArgumentException: Object of type 'TabItem' cannot be converted to type 'System.Collections.IList'. Why is the parser still not calling IList.Add()? I put my code here, in case you want to check it: http://happynomad121.googlepages.com/ToolbarItem.cs
HappyNomad
Clarified my response, HTH!
gix
Using an explicit collection tag, my code has worked fine from the beginning. My question was about how to successfully omit the collection tag. Even after doing everything you suggested, I'm still getting the same parse-time error message.
HappyNomad
Then there must be something else wrong because the code posted works fine. See http://nopaste.org/p/adqfm5EPi for a short and complete example.
gix
The simple example you linked to works as you say. So where is the problem hiding? Here's a VS2008 project that demonstrates the issue I'm facing: http://happynomad121.googlepages.com/TabItemTest-v2.zipRun it and you can see the error message is still there.
HappyNomad
It seems like you cannot use resources this way. If you either don't use resources or an explicit collection it seems to work.
gix
You're right that if explicit collections are used then it works. However, I've already tested it without using resources, and it still doesn't work. This is a mystery.
HappyNomad