Gridview ASP.NET avec en source une collection d'elements "interface'

J'ai été confronté ce vendredi au travail à une petite question "classique" que les développeurs posent quand ils remarquent un "problème" avec l'utilisation des GridView .... à savoir ... comment afficher dans une GridView une collection d'éléments de types différents ... Voici la réponse dans ce post !

Prenons par exemple une interface décrivant un profil .. Nous afficherons donc dans la vue une grille avec le nom et le prénom.

public interface IMonInterface
{
    string Nom { get; set; }
    string Prenom { get; set; }
}

Nous allons implémenter cette interface dans deux classes différentes ... A et B ... Nous aurions pu dire "Homme" / "Femme" ... Ce n'est pas important !

public class A : IMonInterface
{
    public string Nom { get; set; }
    public string Prenom { get; set; }
    public int Age { get; set; }
}
 
public class B : IMonInterface
{
    public string Nom { get; set; }
    public string Prenom { get; set; }
    public bool Sexe { get; set; }
}

Afin de rendre les données dans la grille ASP.NET 2.0, nous remplissons la liste et nous fournissons cette liste a notre grille.

protected void Page_Load(object sender, EventArgs e)
{
    List<IMonInterface> macollection = new List<IMonInterface>()
    {
        new B(){ Nom="Alfred", Prenom="Jean", Sexe= true},                
        new B(){ Nom="Alfredo", Prenom="Jeanot", Sexe= false},
        new A(){ Nom="Jean", Prenom="Conrad", Age=18}
    };
    gv.DataSource = macollection;
    gv.DataBind();
}

Que ce soit en AutoGenerateColumns ou en BoundField (ci-dessous), nous aurons une exception de type : "L'accesseur de propriété 'Nom' sur l'objet 'WebApplicationAjax4.A' a levé l'exception suivante :'L'objet ne correspond pas au type cible.'".... Il faut donc comprendre que le type joue un rôle important dans la reflection pour le remplissage d'une GridView ...

<asp:BoundField DataField="Nom" />

Mais comment faire pour que cela fonctionne ? Simple ... Il faut réflechir au comportement du BoundField et au comportement du ItemTemplate ... On se rend vite compte que si on implémente le code ci-dessous .... l'utilisation du "Eval" nous permettra de rendre via un autre mécanisme l'information ... Le type n'étant plus pris en compte de la même façon, le rendu fonctionne parfaitement ... Voici donc une solution au problème ! Vous en avez une autre ? Un petit commentaire est le bienvenu ... !

<asp:GridView ID="gv" runat="server" AutoGenerateColumns="False">
    <Columns>
        <asp:TemplateField>
            <ItemTemplate>
                <asp:Label runat="server" ID="nomID" Text='<%# Eval("Nom")%>' />
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField>
            <ItemTemplate>
                <asp:Label runat="server" ID="prenomID" Text='<%# Eval("Prenom")%>' />
            </ItemTemplate>
        </asp:TemplateField>
    </Columns>
</asp:GridView>

Commentaires

1. Le dimanche, octobre 11 2009, 22:59 par Pierre-Emmanuel Dautreppe

Oui bien sur ! :-)
C'est une solution, mais pas la bonne ;-)

Il faut savoir que c'est une limitation (connue) du GridView qui se base en effet sur les types concrets pour découvrir les valeurs des propriétés à afficher provenant d'une liste.

La vraie solution est de faire en sorte que la collection d'objets à binder implémente une simple interface nommée ITypedList. Cette interface possède deux seules méthodes. Une est totalement obsolète et était utilisée par le DataGrid. L'autre permet au GridView de connaître les types à afficher.

C'est à mon avis la solution la plus clean.
Pour cela, faire en sorte que l'application utilise une collection de base dans toute votre application et que cette collection fasse le boulot. Bref un vrai jeu d'enfant avec un petit framework dans votre application.

Ah oui, dernier point, cette interface est bien sûr implémentée par défaut dans les collections typées de notre propre framework ;-)

Autre façon de bypasser le mécanisme (plutôt que de modifier l'aspx de tous les gridview) est de créer une simple classe qui implémente ton interface et qui prend un seul paramètre dans ton constructeur : cette même interface. Toutes les méthodes / propriétés de ton interface vont simplement rediriger vers cette variable privée. Bref une encapsulation pour bypasser cette faiblesse.
On en reparle demain au stand up !

2. Le dimanche, octobre 11 2009, 23:24 par Thierry Thoua

Le problème du ITypedList est qu'il necessite souvent la création d'une collection propre héritant de BindingList<T> ou de ObservableCollection<T> pour la représentation à l'UI ... Il n'est pas necessaire d'implémenter cet interface au niveau de l'api business.

Je récupère une facture ... je récupère pas du comportement lié au rendu quand je demande ma collection de facture client + fournisseur.

Dès lors, à mes yeux que ce soit ma solution (qui mélange déjà UI et business) ou ta seconde (qui est la plus propre à mes yeux) .. me semblent "propre". La mienne évite la consommation d'une classe en plus et la tienne (la seconde solution) offre la possibilité d'une séparation entre ce qui est "business" et ce qui est UI.

3. Le dimanche, octobre 11 2009, 23:35 par Pierre-Emmanuel Dautreppe

Effectivement, tu as besoin d'une classe "collection" custom qui fait le travail.
Je préfère cependant la première solution, surtout si tu as cette collection de base dans une couche framework. ça me semble logique qu'elle implémente cette fonctionnalité.

4. Le dimanche, octobre 11 2009, 23:39 par Thierry Thoua

Ouip ... Moi j'hésiterais. Je pense que j'envisagerais peut être la solution deux en reprenant l'idée du DataView .. Elle reçoit dans son constructeur une DataTable ... Ca pourrait très bien etre une CollectionView qui hériterait de ITypedList et recevrait dans le ctor la collection "couche basse". le CollectionView<T> utiliserait son "T" pour définir les propriétés visibles.

5. Le lundi, octobre 12 2009, 11:27 par SuperDamz

Salut,
Il me semble qu'en implémentant ICustomTypeDescriptor non pas sur la collection, mais sur le type des éléments, le tour sera joué.

Par contre, dans le cas du AutoGeneratesColumns, le Gridview utilise deux techniques différentes pour identifier le type des éléments :
- Si la collection possède une propriété Item[int], il va utiliser le type de cette propriété. C'est le cas des IList<T> mais ça marche aussi avec n'importe quoi d'autre (duck typing).
- Sinon, il va récupérer le premier élément de l'énumérable ; du coup, c'est son type concret qui est utilisé. Et cela, même si on est sur un IEnumerable<T>... Pas terrible en général, et même carrément gênant si la collection contient des dérivés différents de T.

6. Le mercredi, octobre 14 2009, 21:52 par Pierre-Emmanuel Dautreppe

Et oui SuperDamz ! ICustomTypeDescriptor doit faire le boulot, mais il est "impensable" d'implémenter cette interface sur les types qui peuvent entrer dans la composition d'une liste.

D'ailleurs pour corriger ce que je disais plus haut, le ITypedList fait le boulot, mais uniquement dans un contexte WindowsForms. Cette interface est tout simplement ignorée dans le GridView.

Alors plusieurs autres possibilitées. L'exemple de la classe wrapper proposée ci-dessus est possible. Cependant j'en préfère une autre : ajouter un provider pour les objets de la collection. On peut en effet implémenter un TypeDescriptorProvider pour l'interface qui nous intéresse et faire des TypeDescriptor.AddProvider pour chacune des instances de la collection.

ATTENTION de ne pas faire ça sur les types concernées puisque cela pourrait avoir des conséquences sur le reste de l'application. Cependant sur les instances (si elles ont des durées de vie courtes - ce qui devrait être le cas lors du binding sur une grille), alors c'est tout à fait acceptable.

Qu'en pensez-vous ?

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.

Fil des commentaires de ce billet

Page top