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

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

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

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

Есть ли какой-либо способ сделать это декларативно, или я должен обязательно подключиться с некоторым C#?

РЕДАКТИРОВАТЬ

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

// 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. Настройте объект DataBinding

После этого встроенная привязка данных заработала и работала нормально, и, похоже, не было проблем с извлечением значений свойств из 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 подойдет вам лучше. Я не знаю, какую сетку данных вы используете, но привязка должна быть одинаковой для всех из них. Вот простой пример для телерика данных.
ссылка на форумы Telerik

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

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

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

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

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

Мой ответ из динамической привязки столбцов в 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

Хотя ОП принимает принятый ответ, он использует 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() это та, которая требует реальной реализации: чтение существующих свойств и добавление ваших динамических.

Как 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) {
    ...
  }
Другие вопросы по тегам