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 le ExpandoObject lui-même - mais il n'y a aucune garantie qu'un groupe d'ExpandoObjects partage les mêmes propriétés entre elles, aucune règle pour que le moteur sache 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 genre de DataGrid 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 ça à l'exécution.

Si vous n'avez vraiment aucune idée du type de champs que 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 en utilisant une chaîne à l'exécution - que vous pouvez assembler à partir des résultats de votre requête. Exemple d'utilisation du second 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 le itemgrid à une collection d'objets dont les propriétés publiques partagées peuvent être trouvées par réflexion.

Question

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

C'était mon espoir que AutoGenerateColumns (comme ci-dessous) serait capable de générer des colonnes à partir d'un ExpandoObject comme il le fait avec un type statique, mais il ne semble pas.

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

Y at-il de toute façon à le faire de façon déclarative ou dois-je impérativement me connecter avec 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, juste besoin de comprendre comment lier les colonnes aux valeurs IDictionary.




Bien qu'il y ait une réponse acceptée par l'OP, elle utilise AutoGenerateColumns="False" ce qui n'est pas exactement ce que la question initiale a demandé. Heureusement, il peut être résolu avec des colonnes générées automatiquement. La clé de la solution est 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 utiliser principalement les fonctions statiques de TypeDescriptor de manière triviale. GetProperties() est celui qui nécessite une implémentation réelle: lire les propriétés existantes et ajouter celles dynamiques.

Comme PropertyDescriptor est abstrait, vous devez l'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) {
    ...
  }