2009-12-30 28 views
26

Ich versuche, die Ergebnisse einer Abfrage in einem WPF-Datagrid anzuzeigen. Der ItemsSource-Typ, an den ich bin, ist IEnumerable<dynamic>. Da die zurückgegebenen Felder erst zur Laufzeit ermittelt werden, ist der Typ der Daten erst bekannt, wenn die Abfrage ausgewertet wurde. Jede "Zeile" wird als ExpandoObject mit dynamischen Eigenschaften zurückgegeben, die die Felder darstellen.Wie erzeuge ich dynamisch Spalten in einem WPF DataGrid?

Es war meine Hoffnung, AutoGenerateColumns (wie unten) wäre in der Lage, Spalten von einem ExpandoObject zu generieren, wie es mit einem statischen Typ tut, aber es scheint nicht zu.

Gibt es sowieso, um dies deklarativ zu tun oder muss ich zwingend mit einigen C# einhängen?

EDIT

Ok das wird mir die richtigen Spalten erhalten:

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

So, jetzt müssen nur herausfinden, wie die Spalten an die IDictionary Werte zu binden.

Antwort

24

Letztlich brauchte ich zwei Dinge zu tun:

  1. generieren die Spalten manuell aus der Liste der von der Abfrage zurückgegebenen Eigenschaften
  2. Einrichten eines DataBinding-Objekts

Danach trat die integrierte Datenbindung ein und funktionierte gut und es schien kein Problem zu bestehen, die Eigenschaftswerte aus ExpandoObject zu entfernen.

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

und

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

Das funktioniert gut, aber wann führen Sie diesen Code aus? Die ItemsSource ist noch nicht festgelegt, wenn Sie dies in DataContextChanged verarbeiten. – Wouter

+0

In meiner Instanz ist ItemSource an eine ViewModel-Eigenschaft gebunden, die als Ergebnis bezeichnet wird. Ich habe einen INotifyPrpertyChanged-Handler in der Ansicht, der darauf reagiert, dass sich diese Eigenschaft ändert. – dkackman

+0

Das war mein Ansatz, aber ich stolperte über ein Problem. Was ist mit Zeilenvalidierung? Mussten Sie die Zeilenvalidierung auf den ExpandoObjects behandeln? – Ninglin

5

Das Problem hier ist, dass die CLR Spalten für das ExpandoObject selbst erstellen wird - aber es gibt keine Garantie, dass eine Gruppe von ExpandoObjects die gleichen Eigenschaften untereinander teilen, keine Regel für die Engine zu wissen, welche Spalten erstellt werden müssen .

Vielleicht würde etwas wie Linq anonyme Typen besser für Sie arbeiten. Ich weiß nicht, welche Art von Datagrid Sie verwenden, aber die Bindung sollte für alle identisch sein. Hier ist ein einfaches Beispiel für das Telerik DataGrid.

Dies ist eigentlich nicht wirklich dynamisch, die Typen müssen zur Kompilierzeit bekannt sein - aber dies ist eine einfache Möglichkeit, etwas zur Laufzeit einzustellen.

Wenn Sie wirklich keine Ahnung haben, welche Art von Feldern Sie anzeigen, wird das Problem etwas haariger. Mögliche Lösungen sind:

Mit dynamischem Linq können Sie anonyme Typen erstellen eine Zeichenfolge zur Laufzeit mit - die Sie aus den Ergebnissen der Anfrage zusammenstellen können. Beispiel für die Verwendung von dem zweiten Link:

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

In jedem Fall ist die Grundidee irgendwie die itemgrid auf eine Sammlung von Objekten, auf deren gemeinsamen öffentliche Eigenschaften kann durch Reflexion zu finden.

+0

Die betreffenden Daten aus den Tags in MP3-Dateien kommen, so dass der Satz ist in der Tat nicht konsistent. Und in der Tat gibt es keine kompilierte Zeit, was sie wissen werden. Ich kann das Problem der Eigenschaftskonsistenz umgehen, es ist nur bedauerlich, dass ExpandoObject für Reflexion undurchsichtig ist (obwohl ich sehen kann, dass dies ein schwieriges Problem zu lösen ist). – dkackman

+0

In diesem Fall kann dynamische linq helfen, aber Sie benötigen möglicherweise einen Zwei-Durchlauf-Ansatz. Analysieren Sie die Daten einmal, um zu sehen, welche Tags gefunden wurden, und dann ein weiteres Mal, um die Liste neuer Objekte zu füllen. Ich vermute, das Problem ist, dass, wenn eine mp3-Datei eine definierte Eigenschaft hat, nachdem Sie die Werte Objekten (dynamisch oder nicht) zugeordnet haben, alle von ihnen diese Eigenschaft haben müssen. – Egor

4

meine Antwort von Dynamic column binding in Xaml

mir einen Ansatz verwendet habe, die das Muster dieses Pseudo-Code

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

DynamicTypeHelper.GetDynamicType() erzeugt eine Art mit einfachen Eigenschaften folgen. Siehe this post für die Details, wie eine solche Art zu erzeugen

Dann tatsächlich die Art zu verwenden, so etwas wie dieses

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

Interessanter Ansatz. Ich werde wahrscheinlich etwas ähnliches tun müssen, möchte aber die Emit-Stücke vermeiden. Die Verwendung von Expando und Emitted scheint redundant zu sein. Danke für den Link; Es hat mir einige Ideen gegeben. – dkackman

1

Obwohl es von der OP eine akzeptierte Antwort ist, verwendet es AutoGenerateColumns="False", die nicht genau das, was die ursprüngliche Frage gefragt. Glücklicherweise kann es auch mit automatisch generierten Spalten gelöst werden. Der Schlüssel zur Lösung ist die DynamicObject, die sowohl statische als auch dynamische Eigenschaften haben:

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

Für die ICustomTypeDescriptor Implementierung Sie meist die statischen Funktionen von TypeDescriptor in trivialer Weise nutzen können. GetProperties() ist derjenige, der echte Implementierung erfordert: Lesen der vorhandenen Eigenschaften und Hinzufügen Ihrer dynamischen Eigenschaften.

Als PropertyDescriptor abstrakt ist, muss man es erben:

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

Dies scheint nicht zu funktionieren, wenn 'ItemsSource' an eine' ObservableCollection' gebunden wird ' – georgiosd

+0

Dies ist das fehlende Stück: http: //www.reimers .dk/jacob-reimers-blog/automatisch generierende-datagrid-columns-from-dynamicobjects – georgiosd

+0

Dieser Link ist jetzt tot; Könnte jemand die vollständige Antwort hier posten? – Sphynx