2009-12-30 9 views
26

Sto tentando di visualizzare i risultati di una query in un datagrid WPF. Il tipo di elemento Items cui mi sto legando è IEnumerable<dynamic>. Poiché i campi restituiti non sono determinati fino al runtime, non conosco il tipo di dati fino a quando la query non viene valutata. Ogni "riga" viene restituita come ExpandoObject con proprietà dinamiche che rappresentano i campi.Come faccio a generare dinamicamente colonne in un DataGrid di WPF?

Era mia speranza che AutoGenerateColumns (come di seguito) sarebbe in grado di generare colonne da un ExpandoObject come fa con un tipo statico ma non sembra.

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

Esiste comunque la possibilità di farlo in modo dichiarativo o devo agganciare imperativamente con qualche C#?

EDIT

Ok questo mi otterrà le colonne corrette:

// 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 }); 

Così ora solo bisogno di capire come associare le colonne ai valori IDictionary.

risposta

24

definitiva avevo bisogno di fare due cose:

  1. Generare il colonne manualmente dall'elenco di proprietà restituite dalla query
  2. Impostazione di un oggetto DataBinding

Dopo che l'associazione dati incorporata è entrata in azione e ha funzionato correttamente e non sembra avere alcun problema a ottenere i valori delle proprietà fuori dallo ExpandoObject.

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

e

// 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

Funziona bene, ma quando esegui questo pezzo di codice? L'oggetto ItemsSource non è ancora impostato quando lo gestisci su DataContextChanged – Wouter

+0

Nella mia istanza ItemSource è associato a una proprietà ViewModel denominata Risultati. Ho un gestore di INotifyPrpertyChanged nella vista che reagisce alla modifica di quella proprietà. – dkackman

+0

Questo è stato il mio approccio ma sono incappato in un problema. Che ne pensi di Row Validation? Hai dovuto gestire la validazione delle righe su ExpandoObjects? – Ninglin

5

Il problema è che il clr creerà colonne per ExpandoObject stesso, ma non è garantito che un gruppo di ExpandoObjects condivida le stesse proprietà tra loro, nessuna regola per il motore per sapere quali colonne devono essere create .

Forse qualcosa come i tipi anonimi di Linq funzionerebbe meglio per voi. Non so che tipo di datagrid stai usando, ma il binding dovrebbe essere identico per tutti loro. Ecco un semplice esempio per il datagrid telerik.
link to telerik forums

Questo non è in realtà veramente dinamico, i tipi devono essere noti al momento della compilazione - ma questo è un modo facile di impostare qualcosa di simile in fase di esecuzione.

Se davvero non hai idea di quale tipo di campi mostrerai il problema diventa un po 'più peloso. Le soluzioni possibili sono:

Con LINQ dinamica è possibile creare tipi anonimi utilizzando una stringa in fase di esecuzione - che è possibile assemblare dai risultati della query. Esempio di utilizzo dal secondo link:

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

In ogni caso, l'idea di base è quella di impostare in qualche modo l'itemgrid a una collezione di oggetti la cui condiviso proprietà pubbliche può essere trovato dalla riflessione.

+0

I dati in questione provengono dai tag all'interno dei file mp3, in modo che il set non è davvero consistente. E in effetti non esiste una conoscenza del tempo di compilazione di ciò che saranno. Posso aggirare il problema della consistenza della proprietà, è solo un peccato che ExpandoObject sia opaco alla riflessione (anche se posso vedere come questo sia un problema difficile da risolvere). – dkackman

+0

In questo caso, linq dinamico può essere d'aiuto, ma potrebbe essere necessario un approccio a due passaggi. Analizzare i dati una volta per vedere quali tag sono stati rilevati e quindi un'altra volta per riempire l'elenco di nuovi oggetti. Immagino che il problema sia che se un file mp3 ha una proprietà definita, dopo aver mappato i valori sugli oggetti (dinamici o meno), tutti devono avere quella proprietà. – Egor

4

la mia risposta da Dynamic column binding in Xaml

ho usato un approccio che segue il modello di questo pseudocodice

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

DynamicTypeHelper.GetDynamicType() genera un tipo con proprietà semplici. Vedere this post per i dettagli su come generare un tale tipo

Quindi utilizzare effettivamente il tipo, fare qualcosa di simile

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

Approccio interessante. Probabilmente dovrò fare qualcosa di simile ma vorrei evitare i pezzi di Emit. L'uso di entrambi i tipi Expando ed Emitted sembra ridondante. Grazie per il link; mi ha dato alcune idee – dkackman

1

Anche se v'è una risposta accettata dal PO, utilizza AutoGenerateColumns="False" che non è esattamente quello che la domanda iniziale chiesto. Fortunatamente, può essere risolto anche con colonne generate automaticamente. La chiave per la soluzione è la DynamicObject che può avere proprietà statiche e dinamiche:

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; 
} 

Per l'ICustomTypeDescriptor implementazione, è possibile per lo più utilizzare le funzioni statiche di TypeDescriptor in modo banale. GetProperties() è quello che richiede un'implementazione reale: la lettura delle proprietà esistenti e l'aggiunta di quelle dinamiche.

Come PropertyDescriptor è astratta, si ha in possesso di questo:

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

Questo non sembra funzionare per me quando leghi 'ItemsSource' in un' ObservableCollection ' – georgiosd

+0

Questo è il pezzo mancante: http: //www.reimers .dk/jacob-reimers-blog/auto-generation-datagrid-columns-from-dynamicobjects – georgiosd

+0

Il link è ora morto; qualcuno potrebbe postare la risposta completa qui? – Sphynx

Problemi correlati