Как динамически генерировать столбцы в WPF DataGrid?

Я пытаюсь отобразить результаты запроса в datagrid WPF. Типа, что ItemsSource я привязку к IEnumerable<dynamic>. Поскольку возвращаемые поля не определяются до выполнения, я не знаю тип данных до тех пор, пока запрос не будет оценен. Каждая "строка" возвращается как ExpandoObject с динамическими свойствами, представляющими поля.

Я надеялся, что AutoGenerateColumns (как показано ниже) сможет генерировать столбцы из ExpandoObject как и со статическим типом, но это не так кажется.

<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding Results}"/>

есть ли в любом случае, чтобы сделать это декларативно или мне нужно подключить императивно с некоторым c#?

редактировать

Ok это даст мне правильные столбцы:

// ExpandoObject implements IDictionary<string,object> 
IEnumerable<IDictionary<string, object>> rows = dataGrid1.ItemsSource.OfType<IDictionary<string, object>>();
IEnumerable<string> columns = rows.SelectMany(d => d.Keys).Distinct(StringComparer.OrdinalIgnoreCase);
foreach (string s in columns)
    dataGrid1.Columns.Add(new DataGridTextColumn { Header = s });

поэтому теперь просто нужно выяснить, как привязать столбцы к значениям IDictionary.

4 ответов


в конечном итоге мне нужно было сделать две вещи:

  1. создайте столбцы вручную из списка свойств, возвращаемых запросом
  2. настройка объекта привязки данных

после этого встроенная привязка данных сработала и работала нормально, и, похоже, не было никаких проблем с получением значений свойств из ExpandoObject.

<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Results}" />

и

// Since there is no guarantee that all the ExpandoObjects have the 
// same set of properties, get the complete list of distinct property names
// - this represents the list of columns
var rows = dataGrid1.ItemsSource.OfType<IDictionary<string, object>>();
var columns = rows.SelectMany(d => d.Keys).Distinct(StringComparer.OrdinalIgnoreCase);

foreach (string text in columns)
{
    // now set up a column and binding for each property
    var column = new DataGridTextColumn 
    {
        Header = text,
        Binding = new Binding(text)
    };

    dataGrid1.Columns.Add(column);
}

проблема здесь в том, что среда clr будет создавать столбцы для самого ExpandoObject - но нет никакой гарантии, что группа ExpandoObjects имеет одинаковые свойства между собой, нет правила для механизма, чтобы знать, какие столбцы необходимо создать.

возможно, что-то вроде анонимных типов Linq будет работать лучше для вас. Я не знаю, какой тип datagrid вы используете, но привязка должна быть одинаковой для всех из них. Вот простой пример для из Telerik управления DataGrid.
ссылка на форумы telerik

это на самом деле не динамично, типы должны быть известны во время компиляции, но это простой способ установить что - то вроде этого во время выполнения.

Если вы действительно не знаете, какие поля вы будете отображать, проблема становится немного более волосатой. Возможные решения:

С помощью dynamic linq вы можете создавать анонимные типы, используя строку во время выполнения , которую вы можете собрать из результатов запроса. Пример использования из второй ссылки:

var orders = db.Orders.Where("OrderDate > @0", DateTime.Now.AddDays(-30)).Select("new(OrderID, OrderDate)");

в любом случае основная идея состоит в том, чтобы каким-то образом установить itemgrid в коллекцию объектов, shared общие свойства могут быть найдено отражением.


ответ динамическая привязка столбцов в Xaml

я использовал подход, который в этот псевдокод

columns = New DynamicTypeColumnList()
columns.Add(New DynamicTypeColumn("Name", GetType(String)))
dynamicType = DynamicTypeHelper.GetDynamicType(columns)

DynamicTypeHelper.GetDynamicType () генерирует тип с простыми свойствами. См.этот пост для получения подробной информации о том, как создать такой тип

затем, чтобы фактически использовать тип, сделайте что-то вроде этого

Dim rows as List(Of DynamicItem)
Dim row As DynamicItem = CType(Activator.CreateInstance(dynamicType), DynamicItem)
row("Name") = "Foo"
rows.Add(row)
dataGrid.DataContext = rows

хотя есть принятый ответ OP, он использует AutoGenerateColumns="False" что не совсем то, что задал первоначальный вопрос. К счастью, его можно решить с помощью автоматически генерируемых столбцов. Ключом к решению является DynamicObject, которые могут иметь как статические, так и динамические свойства:

public class MyObject : DynamicObject, ICustomTypeDescriptor {
  // The object can have "normal", usual properties if you need them:
  public string Property1 { get; set; }
  public int Property2 { get; set; }

  public MyObject() {
  }

  public override IEnumerable<string> GetDynamicMemberNames() {
    // in addition to the "normal" properties above,
    // the object can have some dynamically generated properties
    // whose list we return here:
    return list_of_dynamic_property_names;
  }

  public override bool TryGetMember(GetMemberBinder binder, out object result) {
    // for each dynamic property, we need to look up the actual value when asked:
    if (<binder.Name is a correct name for your dynamic property>) {
      result = <whatever data binder.Name means>
      return true;
    }
    else {
      result = null;
      return false;
    }
  }

  public override bool TrySetMember(SetMemberBinder binder, object value) {
    // for each dynamic property, we need to store the actual value when asked:
    if (<binder.Name is a correct name for your dynamic property>) {
      <whatever storage binder.Name means> = value;
      return true;
    }
    else
      return false;
  }

  public PropertyDescriptorCollection GetProperties() {
    // This is where we assemble *all* properties:
    var collection = new List<PropertyDescriptor>();
    // here, we list all "standard" properties first:
    foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(this, true))
      collection.Add(property);
    // and dynamic ones second:
    foreach (string name in GetDynamicMemberNames())
      collection.Add(new CustomPropertyDescriptor(name, typeof(property_type), typeof(MyObject)));
    return new PropertyDescriptorCollection(collection.ToArray());
  }

  public PropertyDescriptorCollection GetProperties(Attribute[] attributes) => TypeDescriptor.GetProperties(this, attributes, true);
  public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
  public string GetClassName() => TypeDescriptor.GetClassName(this, true);
  public string GetComponentName() => TypeDescriptor.GetComponentName(this, true);
  public TypeConverter GetConverter() => TypeDescriptor.GetConverter(this, true);
  public EventDescriptor GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
  public PropertyDescriptor GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
  public object GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
  public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
  public EventDescriptorCollection GetEvents(Attribute[] attributes) => TypeDescriptor.GetEvents(this, attributes, true);
  public object GetPropertyOwner(PropertyDescriptor pd) => this;
}

на ICustomTypeDescriptor реализации, вы можете в основном использовать статические функции TypeDescriptor тривиальным образом. GetProperties() это тот, который требует реальной реализации: чтение существующие свойства и добавление динамических.

As PropertyDescriptor является абстрактным, вы должны унаследовать его:

public class CustomPropertyDescriptor : PropertyDescriptor {
  private Type componentType;

  public CustomPropertyDescriptor(string propertyName, Type componentType)
    : base(propertyName, new Attribute[] { }) {
    this.componentType = componentType;
  }

  public CustomPropertyDescriptor(string propertyName, Type componentType, Attribute[] attrs)
    : base(propertyName, attrs) {
    this.componentType = componentType;
  }

  public override bool IsReadOnly => false;

  public override Type ComponentType => componentType;
  public override Type PropertyType => typeof(property_type);

  public override bool CanResetValue(object component) => true;
  public override void ResetValue(object component) => SetValue(component, null);

  public override bool ShouldSerializeValue(object component) => true;

  public override object GetValue(object component) {
    return ...;
  }

  public override void SetValue(object component, object value) {
    ...
  }