web-dev-qa-db-fra.com

Comment générer dynamiquement des colonnes dans un WPF DataGrid?

Je tente d'afficher les résultats d'une requête dans une grille de données WPF. Le type ItemsSource que je lie est IEnumerable<dynamic>. Comme les champs retournés ne sont pas déterminés avant l'exécution, je ne connais pas le type des données avant l'évaluation de la requête. Chaque "ligne" est retournée sous la forme d'une ExpandoObject avec des propriétés dynamiques représentant les champs.

J'espérais que AutoGenerateColumns (comme ci-dessous) serait capable de générer des colonnes à partir de ExpandoObject comme c'est le cas avec un type statique, mais cela ne semble pas l'être.

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

Est-il possible de le faire de manière déclarative ou dois-je me connecter impérativement à du C #?

MODIFIER

Ok, cela me donnera les bonnes colonnes:

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

Alors maintenant, il suffit de comprendre comment lier les colonnes aux valeurs IDictionary.

29
dkackman

En fin de compte, je devais faire deux choses:

  1. Générez les colonnes manuellement à partir de la liste des propriétés renvoyées par la requête 
  2. Configurer un objet DataBinding

Après cela, la liaison de données intégrée a bien fonctionné et ne semblait pas avoir de problème pour extraire les valeurs de propriété de ExpandoObject.

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

et

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

Le problème ici est que clr créera des colonnes pour ExpandoObject lui-même - mais rien ne garantit qu'un groupe de ExpandoObjects partagent les mêmes propriétés entre elles, aucune règle ne permet au moteur de savoir quelles colonnes doivent être créées. 

Peut-être que quelque chose comme les types anonymes Linq fonctionnerait mieux pour vous. Je ne sais pas quel type de grille de données vous utilisez, mais la liaison devrait être identique pour tous. Voici un exemple simple pour la grille de données telerik.
lien vers les forums telerik

Ce n'est pas réellement dynamique, les types doivent être connus au moment de la compilation - mais c'est un moyen facile de définir quelque chose comme ceci au moment de l'exécution. 

Si vous ne savez vraiment pas quel type de champs vous allez afficher, le problème devient un peu plus poilu. Les solutions possibles sont:

Avec linq dynamique, vous pouvez créer des types anonymes à l’aide d’une chaîne au moment de l’exécution, que vous pouvez assembler à partir des résultats de votre requête. Exemple d'utilisation du deuxième lien:

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

Dans tous les cas, l’idée de base est de définir d’une manière ou d’une autre la grille d’éléments sur une collection d’objets dont partagé les propriétés publiques peuvent être trouvées par réflexion.

5
Egor

ma réponse de Liaison de colonne dynamique dans Xaml

J'ai utilisé une approche qui suit le modèle de ce pseudocode

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

DynamicTypeHelper.GetDynamicType () génère un type avec des propriétés simples. Voir cet article pour plus de détails sur la génération d'un tel type

Ensuite, pour utiliser réellement le type, faites quelque chose comme ça

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

Bien que l'OP ait accepté la réponse, celui-ci utilise AutoGenerateColumns="False", ce qui n'est pas exactement ce que la question initiale avait demandé. Heureusement, il peut également être résolu avec des colonnes générées automatiquement. La clé de la solution est la DynamicObject qui peut avoir des propriétés statiques et dynamiques:

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

Pour l'implémentation ICustomTypeDescriptor, vous pouvez principalement utiliser les fonctions statiques de TypeDescriptor de manière triviale. GetProperties() est celui qui nécessite une implémentation réelle: lecture des propriétés existantes et ajout de celles dynamiques.

Comme PropertyDescriptor est abstrait, vous devez en hériter:

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) {
    ...
  }
1
Gábor