2009-12-30 11 views
26

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

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

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

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

EDIT

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.

ответ

24

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

  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); 
} 
+1

Это работает красиво, но когда вы выполняете этот кусок кода? Элемент ItemsSource еще не установлен, когда вы обрабатываете это в DataContextChanged. – Wouter

+0

В моем экземпляре ItemSource привязан к свойству ViewModel, которое называется Results. У меня есть обработчик INotifyPrpertyChanged, который реагирует на изменение этого свойства. – dkackman

+0

Это был мой aproach, но я наткнулся на проблему. Как насчет проверки правильности? Вам нужно было обработать проверку строк в ExpandoObjects? – Ninglin

5

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

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

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

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

  • Создание отображения типов во время выполнения с помощью Reflection.Emit, я думаю, что можно создать общий конвертер значения, который будет принимать результаты запроса, создать новый тип (и сохранить кэшированный список) , и верните список объектов. Создание нового динамического типа будет следовать тому же алгоритму, как вы уже используете для создания ExpandoObjects

    MSDN on Reflection.Emit
    An old but useful article on codeproject

  • Использование динамических Linq - это, вероятно, проще быстрый способ сделать это.
    Using Dynamic Linq
    Getting around anonymous type headaches with dynamic linq

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

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

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

+0

Данные вопросы приходят из тегов внутри mp3-файлов, так что набор действительно не соответствует. И действительно, нет времени компилировать знания о том, кем они будут. Я могу обойти проблему согласованности свойств, просто прискорбно, что ExpandoObject непрозрачен для отражения (хотя я вижу, как это трудно решить). – dkackman

+0

В этом случае динамический linq может помочь, но вам может потребоваться двухпроходный подход. Разберите данные один раз, чтобы увидеть, какие теги встречаются, а затем еще раз, чтобы заполнить список новых объектов. Я думаю, проблема заключается в том, что если какой-либо mp3-файл имеет определенное свойство, то после сопоставления значений с объектами (динамическими или нет) все они должны иметь это свойство. – Egor

4

мой ответ от Dynamic column binding in Xaml

Я использовал подход, который следует за образец этого псевдокода

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

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

Тогда на самом деле использовать тип, сделать что-то вроде этого

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

Интересный подход. Скорее всего, мне придется сделать что-то подобное, но я бы хотел избежать фрагментов Emit. Использование типов Expando и Emitted представляется излишним. Спасибо за ссылку; это дало мне некоторые идеи. – dkackman

1

Хотя есть ответ принят на ОР, он использует 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) { 
    ... 
    } 
+0

Это не работает для меня при привязке 'ItemsSource' к' ObservableCollection ' – georgiosd

+0

Это недостающая часть: http: //www.reimers .dk/jacob-reimers-blog/auto-generate-datagrid-columns-from-dynamicobjects – georgiosd

+0

Эта ссылка теперь мертва; может ли кто-нибудь отправить полный ответ здесь? – Sphynx