Reputation: 1013
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
Reputation: 25623
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:
ITypedList
, it will use ITypedList.GetItemProperties()
.IEnumerable<T>
for any T
other than System.Object
, it will use TypeDescriptor.GetProperties(typeof(T))
.ICustomTypeProvider
, it uses ICustomTypeProvider.GetCustomType().GetProperties()
.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.
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
Reputation: 37057
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