scott lafoy
scott lafoy

Reputation: 1013

Cannot get attributes of implementing class

I have a behavior that takes the display name attribute and sets the column header of a data grid when it is auto generated. I works fine when the grid is bound to a collection of one specific type. If I have a collection of some base type it will not work although if I don't use my behavior it will have no problem auto generating columns of the derived class from the base class.

When the collection type is of the base class the only attributes that are found are from the base class, I want to be able to show the attributes from the implementing collection.

Ideas?

The problem is: DataGridAutoGeneratingColumnEventArgs.PropertyDescriptor is giving property information for the declared value type of the collection, not the actual item type.

behavior code:

        #region Setup

    /// <summary>
    /// Called when [attached].
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.AutoGeneratingColumn += HandleAutoGeneratingColumns;
    }

    /// <summary>
    /// Called when [detaching].
    /// </summary>
    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.AutoGeneratingColumn -= HandleAutoGeneratingColumns;
    }

    #endregion

    #region Helpers

    /// <summary>
    /// Handles the automatic generating columns.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="dataGridAutoGeneratingColumnEventArgs">The <see cref="DataGridAutoGeneratingColumnEventArgs"/> instance containing the event data.</param>
    private void HandleAutoGeneratingColumns(object sender, DataGridAutoGeneratingColumnEventArgs dataGridAutoGeneratingColumnEventArgs)
    {
        if (AssociatedObject != null)
        {
            var displayName = GetPropertyDisplayName(dataGridAutoGeneratingColumnEventArgs.PropertyDescriptor);

            if (!string.IsNullOrEmpty(displayName))
            {
                dataGridAutoGeneratingColumnEventArgs.Column.Header = displayName;
                dataGridAutoGeneratingColumnEventArgs.Column.Width = new DataGridLength(1, DataGridLengthUnitType.Star);
            }
            else
            {
                dataGridAutoGeneratingColumnEventArgs.Column.Visibility = Visibility.Collapsed;
            }
        }
    }


    /// <summary>
    /// Gets the display name of the property.
    /// </summary>
    /// <param name="descriptor">The descriptor.</param>
    /// <returns></returns>
    [CanBeNull]
    private static string GetPropertyDisplayName(object descriptor)
    {
        string returnValue = null;

        var propertyDescriptor = descriptor as PropertyDescriptor;
        if (propertyDescriptor != null)
        {
            var displayName = propertyDescriptor.Attributes[typeof(DisplayNameAttribute)] as DisplayNameAttribute;
            if (displayName != null && !Equals(displayName, DisplayNameAttribute.Default))
            {
                returnValue = displayName.DisplayName;
            }
        }
        else
        {
            var propertyInfo = descriptor as PropertyInfo;
            if (propertyInfo != null)
            {
                var attributes = propertyInfo.GetCustomAttributes(typeof(DisplayNameAttribute), true);
                foreach (var attribute in attributes)
                {
                    var displayName = attribute as DisplayNameAttribute;
                    if (displayName != null && !Equals(displayName, DisplayNameAttribute.Default))
                    {
                        returnValue = displayName.DisplayName;
                    }
                }
            }
        }

        return returnValue;
    }

Sample from one binding property

public class DefaultMatchedItems : ILookupItem
{
    #region Properties

    /// <summary>
    /// Gets or sets the first column value.
    /// </summary>
    [DisplayName("Long Name")]
    [CanBeNull]
    public string FirstColumnValue { get; set; }

    /// <summary>
    /// Gets or sets the second column value.
    /// </summary>
    [DisplayName("Short Name")]
    [CanBeNull]
    public string SecondColumnValue { get; set; }

    /// <summary>
    /// Gets or sets the third column value.
    /// </summary>
    [DisplayName("Abbreviation")]
    [CanBeNull]
    public string ThirdColumnValue { get; set; }

    /// <summary>
    /// Gets or sets the identifier.
    /// </summary>
    [Browsable(false)]
    [NotNull]
    public string Identifier { get; set; }


    public interface ILookupItem
{

    /// <summary>
    /// Determines whether [contains] [the specified value].
    /// </summary>
    /// <param name="value">The value.</param>
    /// <param name="ignoreCase">if set to <c>true</c> [ignore case].</param>
    /// <returns>
    ///   <c>true</c> if [contains] [the specified value]; otherwise, <c>false</c>.
    /// </returns>
    bool Contains(string value, bool ignoreCase = true);

    /// <summary>
    /// Gets the display value.
    /// </summary>
    /// <param name="identifier">The identifier.</param>
    /// <returns>The first non blank section of the matching value</returns>
    string GetDisplayValue(string identifier);

}

Upvotes: 0

Views: 294

Answers (2)

Mike Strobel
Mike Strobel

Reputation: 25623

How columns get populated

When a DataGrid auto-populates columns, it uses IItemProperties to interrogate its Items collection view about what properties are available. What you are seeing is a consequence of how those properties are resolved. The relevant logic is provided by the CollectionView class, and the steps are as follows:

  1. If the collection implements ITypedList, it will use ITypedList.GetItemProperties().
  2. If the collection implements IEnumerable<T> for any T other than System.Object, it will use TypeDescriptor.GetProperties(typeof(T)).
  3. If there are items in the collection, it grabs a 'representative item' from the beginning of the collection and:
    1. If the representative item implements ICustomTypeProvider, it uses ICustomTypeProvider.GetCustomType().GetProperties().
    2. It falls back to TypeDescriptor.GetProperties(representativeItem).

If the process fails on step (3) because no items are available for inspection, the DataGrid will defer column generation until items are added, at which point it will try again.

How this affects you

If your ItemsSource is an IEnumerable<ILookupItem>, then the columns will be generated by looking only at ILookupItem.

You can work around this by forcing the CollectionView to use a different strategy when resolving item properties. For example, you could bind to an IEnumerable<DefaultMatchedItems> or an IEnumerable<object>; have your collection implement ITypedList; or have ILookupItem implement ICustomTypeProvider.

Interestingly, while DataGrid relies on IItemProperties to resolve item properties, it will only use this interface to iterrogate its Items collection view. It will not attempt to probe its ItemsSource directly, even if it implements IItemProperties. I have always found that odd.

Upvotes: 2

DataGridAutoGeneratingColumnEventArgs.PropertyDescriptor is giving you property information for the declared item type of the collection bound to AssociatedObject.ItemsSource.

I can recreate this with the following classes, if I bind a collection of type ObservableCollection<ItemBase> (or List<ItemBase>, of course), and populate it with instances of ItemSub.

public class ItemBase
{
    [DisplayName("Base Foo")]
    public virtual String Foo { get; set; }
    [DisplayName("All Bar")]
    public virtual String Bar { get; set; }
}

public class ItemSub : Item
{
    [DisplayName("Sub Foo")]
    public override String Foo { get; set; }
}

For Foo, e.PropertyDescriptor.DisplayName is "Base Foo".

If I change the collection type to ObservableCollection<Object> (only for testing purposes -- collections of object generally aren't good practice), e.PropertyDescriptor.DisplayName is "Sub Foo". If I then change the type of the first item in the collection to ItemBase, I get "Base Foo".

So your best move may be to get the property name from the event args, but go to AssociatedObject.Items to get the runtime type of an actual item in the collection, and use the attributes from that type.

Upvotes: 0

Related Questions