Come faccio a generare dinamicamente le colonne in un WPF DataGrid?
-
22-09-2019 - |
Domanda
Sto tentando di visualizzare i risultati di una query in un DataGrid WPF. Il tipo ItemsSource sto vincolanti per è IEnumerable<dynamic>
. Come i campi restituiti non sono determinati fino al runtime non so il tipo di dati fino a quando la query viene valutata. Ogni "riga" viene restituito come ExpandoObject
con proprietà dinamiche rappresentano i campi.
E 'stata la mia speranza che AutoGenerateColumns
(come di seguito) sarebbe in grado di generare le colonne da un ExpandoObject
come fa con un tipo statico, ma non sembra.
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding Results}"/>
Esiste un modo per fare questo in modo dichiarativo o devo agganciare in imperativamente con un po 'C #?
Modifica
Ok questo otterrà me 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 });
Quindi, ora solo bisogno di capire come associare le colonne con i valori IDictionary.
Soluzione
In definitiva avevo bisogno di fare due cose:
- Genera le colonne manualmente dalla lista delle proprietà restituito dalla query
- Configurare un oggetto DataBinding
Dopo che i dati incorporati nel legare calci e funzionava bene e non sembra avere alcun problema di ottenere i valori delle proprietà fuori dal 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);
}
Altri suggerimenti
Il problema qui è che il CLR creerà colonne per l'ExpandoObject in sé - ma non v'è alcuna garanzia che un gruppo di ExpandoObjects condividono le stesse proprietà tra l'altro, nessuna regola per il motore per sapere quali colonne devono essere creati.
Forse qualcosa di simile Linq tipi anonimi avrebbe funzionato meglio per voi. Non so che tipo di un DataGrid che si sta utilizzando, ma vincolante dovrebbe dovrebbero essere identiche per tutti loro. Ecco un semplice esempio per il datagrid Telerik.
link forum Telerik
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 avete veramente idea di che tipo di campi che si esporrà il problema diventa un po 'più peloso. Le soluzioni possibili sono:
- La creazione di un mapping dei tipi in fase di esecuzione utilizzando Reflection.Emit, penso che sia possibile creare un convertitore di valore generico che avrebbe accettato i risultati della query, creare un nuovo tipo (e mantenere una lista memorizzata nella cache), e restituire un elenco di oggetti . Creazione di un nuovo tipo dinamico avrebbe seguito lo stesso algoritmo già utilizzata per la creazione dei ExpandoObjects
MSDN su Reflection.Emit
Una vecchia ma utile articolo su CodeProject
- tramite il Dynamic LINQ - questo è probabilmente il modo più semplice più veloce per farlo
uso dinamico Linq
Come tipo mal di testa intorno anonimi con dinamica LINQ
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.
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 di proprietà semplici. Vedere questo post 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
Anche se non c'è una risposta accettata dal PO, utilizza AutoGenerateColumns="False"
che non è esattamente quello che la domanda iniziale chiesto. Per fortuna, si può essere risolto con le colonne generate automaticamente pure. 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'attuazione ICustomTypeDescriptor
, è possibile per lo più utilizzare le funzioni statiche di TypeDescriptor
in modo banale. GetProperties()
è quella che richiede implementazione reale:. la lettura delle proprietà esistenti e aggiungendo i vostri dinamici
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) {
...
}