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


Answers

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

Peut-être que quelque chose comme les types anonymes de Linq fonctionnerait mieux pour vous. Je ne sais pas quel type de datagrid que 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 vraiment dynamique, les types doivent être connus au moment de la compilation - mais c'est un moyen simple de définir quelque chose comme ça à l'exécution.

Si vous n’avez vraiment aucune idée du type de champs que vous afficherez, le problème devient un peu plus complexe. Les solutions possibles sont les suivantes:

Avec linq dynamique, vous pouvez créer des types anonymes à l'aide d'une chaîne lors 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 en quelque sorte le itemgrid sur une collection d'objets dont les propriétés publiques partagées peuvent être trouvées par réflexion.

Question

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

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

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

Y a-t-il un moyen de le faire de manière déclarative ou dois-je m'imposer impérativement avec du C #?

MODIFIER

Ok ça va me donner 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 });

Il suffit donc maintenant de comprendre comment lier les colonnes aux valeurs IDictionary.




Bien qu'il y ait une réponse acceptée par l'OP, il utilise AutoGenerateColumns="False" ce qui n'est pas exactement ce que la question initiale demandait. Heureusement, il peut également être résolu avec des colonnes générées automatiquement. La clé de la solution est le DynamicObject qui peut avoir à la fois 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 celle 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) {
    ...
  }