MSDN Magazin > Home > Ausgaben > 2008 > April >  Cutting Edge: Tipps und Tricks zu ListView
Cutting Edge
Tipps und Tricks zu ListView
Dino Esposito

In der Ausgabe des letzten Monats wurde das ListView-Steuerelement vorgestellt, bei dem es sich um eine neue Ergänzung der ASP.NET 3.5-Steuerelement-Toolbox handelt. Hier eine kurze Wiederholung: ListView ist eine verbesserte Version des DataList-Steuerelements, das mehr Kontrolle über generiertes Markup, Unterstützung für die Auslagerung und vollständige Integration mit dem auf Datenquellen basierenden Bindungsmodell bietet.
Dieser Artikel geht über die Grundlagen von ListView-Vorlagen und Datenbindung hinaus, um Features zu implementieren, die bei realen Seiten recht häufig sind, aber etwas zusätzliche Codierung erfordern. Sie erfahren, wie verschachtelte ListView-Steuerelemente zum Erstellen hierarchischer Ansichten von Daten verwendet werden und wie sich das Ereignismodell von ListView durch Ableiten einer benutzerdefinierten ListView-Klasse erweitern lässt.
Insbesondere wird das Ereignismodell so verfeinert, dass Sie für verschiedene Gruppen von gebundenen Datenelementen unterschiedliche Vorlagen verwenden können. So können Sie beispielsweise eine unterschiedliche Vorlage für alle Datenelemente in einem Dataset verwenden, die bestimmten vorgegebenen Kriterien entsprechen. Dabei geht es nicht lediglich um die unterschiedliche Gestaltung bestimmter Elemente – dies kann durch Behandlung des ItemDataBound-Ereignisses problemlos in jedem beliebigen Ansichtsteuerelement durchgeführt werden.
Zumeist werden Menüs als eine Folge von <li>-Tags implementiert, die mithilfe von CSS gestaltet werden. Das Rendern eines flachen Menüs stellt keine besonderen Bindungsprobleme dar, doch was geschieht, wenn Sie ein oder mehrere Untermenüs benötigen? In diesem Fall können Sie entweder das integrierte Menüsteuerelement verwenden oder mithilfe eines ListView-Steuerelements den Schwerpunkt auf ein stärker benutzerdefiniertes Rendering legen. Beachten Sie auch, dass das Menüsteuerelement standardmäßig eine auf Tabellen basierende Ausgabe verwendet, die sich von der stärker CSS-kompatiblen Ausgabe eines ListView-Steuerelements unterscheidet. (Um eine CSS-kompatible Ausgabe für ein Menüsteuerelement zu erhalten, müssen Sie das CSS-Steuerelementadaptertoolkit installieren und konfigurieren, das Sie von www.asp.net herunterladen können).

Erstellen eines hierarchischen Menüs
Viele Webanwendungen bieten links oder rechts auf der Seite ein vertikales Menü an. Dieses Menü ermöglicht es dem Benutzer, zu Seiten zu navigieren, die zwei oder mehr Ebenen tief verschachtelt sind. Das ASP.NET-Menüsteuerelement ist hier definitiv eine nützliche Option. Ich neige jedoch dazu, das Menüsteuerelement nur zu verwenden, wenn eine hierarchische Datenquelle (in der Regel eine XML-Datei) zur Eingabe in das Menü vorhanden ist und wenn Popupuntermenüs erstellt werden müssen.
Bei einer statischen, aus mehreren Ebenen bestehenden Elementliste entscheide ich mich für ein Repeater-Steuerelement, um von einem UI-Entwurfsteam erstelltes Markup auszugeben. In ASP.NET 3.5 ist das gewünschte Repeater-Steuerelement das ListView-Steuerelement.
Sehen Sie sich beispielsweise das Menü in Abbildung 1 an. Es wird in der kostenlosen CoffeeNCream-HTML-Vorlage dargestellt, die Sie von oswd.org herunterladen können. Auf der Beispielseite wurde einfach das HTML-Markup in eine ASP.NET-Masterseite integriert.
Abbildung 1 Ein Standardmenü (Klicken Sie zum Vergrößern auf das Bild)
Der HTML-Quellcode eines Elements des Menüs auf der rechten Seite sieht folgendermaßen aus:
<h1>Something</h1>
<ul>
    <li><a href="#">pellentesque</a></li>
    <li><a href="#">sociis natoque</a></li>
    <li><a href="#">semper</a></li>
    <li><a href="#">convallis</a></li>
</ul>
Wie Sie sehen, ist auf oberster Ebene eine Zeichenfolge enthalten, gefolgt von einer Liste von Links. Sie verwenden ein erstes ListView-Steuerelement, um die H1-Elemente zu erstellen, und dann ein verschachteltes ListView-Steuerelement (oder ein ähnliches, datengebundenes Steuerelement), um die Liste von Links zu rendern. Der erste Schritt besteht im Abrufen von Daten zum Auffüllen des Menüs. Im Idealfall verwenden Sie eine Sammlung der folgenden Pseudotypobjekte zum Generieren der einzelnen Elemente:
class MenuItem {
  public string Title;
  public Collection<Link> Links;
}

class Link  {
  public string Url;
  public string Text;
}
Eine geeignete Methode zum Auffüllen einer MenuItem-Sammlung besteht im Rendern von Informationen aus einer XML-Datei. Dies ist ein mögliches Schema für das Dokument:
<Data>
  <RightMenuItems>
     <MenuItem>
       <Title>Something</Title>
       <Link url="..." text="pellentesque" />
         :
     </MenuItem>
  </RightMenuItems>
</Data>
Nachfolgend wird veranschaulicht, wie LINQ to XML zum Laden und Verarbeiten des Inhalts verwendet wird:
var doc = XDocument.Load(Server.MapPath("dataMap.xml"));
var menu = (from e in doc.Descendants("RightMenuItems")
            select e).First();
var menuLinks = from mi in menu.Descendants("MenuItem")
                select new
                {
                   Title = mi.Value,
                   Links = (...)
                };
Nach dem Laden des Dokuments wählen Sie den ersten Knoten mit der Bezeichnung „RightMenuItems“ aus und rufen dann alle seine untergeordneten MenuItem-Elemente ab. Der Inhalt jedes MenuItem-Knotens wird in einen neuen anonymen Typ mit zwei Eigenschaften (Titel und Links) geladen. Wie würden Sie die Linkssammlung auffüllen? Hier finden Sie den Code dazu:
Links = (from l in mi.Descendants("Link") 
         select new {Url=l.Attribute("url").Value, 
                     Text=l.Attribute("text").Value})
Der nächste Schritt besteht im Binden dieser zusammengesetzten Daten an die Benutzeroberfläche. Wie bereits erwähnt, verwenden Sie ein äußeres ListView-Steuerelement zum Rendern des Titels und ein zweites, verschachteltes ListView-Steuerelement zum Rendern der Liste untergeordneter Links (siehe Abbildung 2). Beachten Sie, dass das innerste ListView-Steuerelement mithilfe der Eval-Methode an Daten gebunden sein muss. Andere Ansätze funktionieren nicht:
<asp:ListView runat="server" ID="RightMenuItems"
  ItemPlaceholderID="PlaceHolder2">
  <LayoutTemplate>
      <asp:PlaceHolder runat="server" ID="PlaceHolder2" /> 
  </LayoutTemplate>

  <ItemTemplate>
    <h1><%# Eval("Title") %></h1>

      <asp:ListView runat="server" ID="subMenu"
        ItemPlaceholderID="PlaceHolder3"
          DataSource='<%# Eval("Links") %>'>
          <LayoutTemplate>
            <ul>
              <asp:PlaceHolder runat="server" ID="PlaceHolder3" /> 
            </ul>
          </LayoutTemplate>
          <ItemTemplate>
            <li>
              <a href='<%# Eval("Url") %>'><%# Eval("Text") %></a>
            </li>
          </ItemTemplate>
      </asp:ListView>
  </ItemTemplate>
</asp:ListView>

<asp:ListView runat="server" ID="subMenu" 
    ItemPlaceholderID="PlaceHolder3"
    DataSource='<%# Eval("Links") %>'>
    ...
</asp:ListView>
Sie starten den Datenbindungsprozess durch Anfügen von Daten an das ListView-Steuerelement der obersten Ebene. Dadurch wird der ListView-Text einschließlich des verschachtelten ListView-Steuerelements vollständig gerendert. Sie könnten theoretisch das ItemDataBound-Ereignis des übergeordneten ListView-Steuerelements abfangen, die Steuerelementstruktur durchlaufen, einen Verweis auf das untergeordnete ListView-Steuerelement abrufen und diesen programmgesteuert an die Daten binden. In diesem Fall wird keine Ausnahme ausgelöst, doch der Bindungsbefehl im inneren ListView-Steuerelement geht verloren, da er zu spät ausgelöst wird, um sich auf das Rendering auszuwirken. Ein Datenbindungsausdruck dagegen wird während eines Datenbindungsereignisses automatisch genau zum richtigen Zeitpunkt des Steuerelementlebenszyklus ausgewertet. So wird sichergestellt, dass die Daten richtig an die Benutzeroberfläche gebunden werden.

Erstellen einer hierarchischen Ansicht
Dasselbe Modell zum Auffüllen eines hierarchischen Menüs kann für das Erstellen einer hierarchischen Datenansicht eingesetzt werden. In diesem Fall besteht eine Alternativoption darin, ein TreeView-Steuerelement zum Vorbereiten einer Darstellung der Daten auf mehreren Ebenen zu verwenden. Für die Datenbindung auf einem TreeView-Steuerelement ist jedoch eine hierarchische Datenquelle erforderlich. Das Verwenden von verschachtelten ListView-Steuerelementen bietet Ihnen mehr Flexibilität sowohl bezüglich der Struktur der Datenquelle als auch der sich ergebenden Benutzeroberfläche. Im Folgenden werden diese Konzepte näher betrachtet.
Angenommen Sie müssen ein hierarchisches Datenraster erstellen, in dem Kunden, Bestellungen und Bestelldetails gemäß vorhandener Tabellenbeziehungen angezeigt werden. Wie würden Sie die Daten abrufen und an das Steuerelement binden? Sehen Sie sich den Code in Abbildung 3 an. Sie können LINQ to SQL verwenden, um Daten problemlos in ein Objektmodell zu laden, das sich zur Aufnahme einer Datenhierarchie anbietet. Beachten Sie, dass Sie beim Ausführen einer Abfrage in LINQ to SQL tatsächlich nur die Daten abrufen, die ausdrücklich angefordert wurden. Anders ausgedrückt: nur die erste Ebene des Diagramms wird abgerufen, und verwandte Objekte werden nicht automatisch zur selben Zeit geladen.
Public Class DataCache
{
    public IEnumerable GetCustomers()
    {
        NorthwindDataContext db = new NorthwindDataContext();
        DataLoadOptions opt = new DataLoadOptions();
        opt.LoadWith<Customer>(c => c.Orders);
        opt.LoadWith<Order>(o => o.Order_Details);
        db.LoadOptions = opt;

        var data = from c in db.Customers
                   select new { c.CompanyName, c.Orders };

        return data.ToList();
    }

    public int GetCustomersCount()
    {
        // Return the number of customers
        NorthwindDataContext db = new NorthwindDataContext();
        return db.Customers.Count();  
    }

    public IEnumerable GetCustomers(int maxRows, int startRowIndex)
    {
        if (maxRows < 0)
            return GetCustomers();

        NorthwindDataContext db = new NorthwindDataContext();
        DataLoadOptions opt = new DataLoadOptions();
        opt.LoadWith<Customer>(c => c.Orders);
        opt.LoadWith<Order>(o => o.Order_Details);
        db.LoadOptions = opt;

        var data = (from c in db.Customers
                    select new { 
                      c.CompanyName, 
                      c.Orders 
                    }).Skip(startRowIndex).Take(maxRows);
        return data.ToList();
    }
}
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;

Die DataLoadOptions-Klasse kann zum Ändern des Standardverhaltens des LINQ to SQL-Moduls verwendet werden, sodass Daten, auf die von einer bestimmten Beziehung verwiesen wird, sofort geladen werden. Durch den Code in Abbildung 3 wird sichergestellt, dass Bestellungen zusammen mit Kunden und dass Details zusammen mit Bestellungen geladen werden.
Die LoadWith-Methode lädt Daten gemäß der angegebenen Beziehung. Die AssociateWith-Methode kann dann das Filtern verwandter, zuvor abgerufener Objekte zulassen, wie hier dargestellt:
opt.AssociateWith<Customer>(
     c => c.Orders.Where(o => o.OrderDate.Value.Year == 1997));
In diesem Beispiel werden beim Abrufen von Kundendaten nur Bestellungen aus dem Jahr 1997 im Voraus abgerufen. Die AssociateWith-Methode wird verwendet, wenn Sie verwandte Daten im Voraus abrufen oder wenn Sie einen Filter anwenden müssen. Es bleibt Ihnen überlassen, sicherzustellen, dass zwischen Tabellen keine zyklischen Verweise bestehen, wenn Sie beispielsweise Bestellungen für einen Kunden und dann den Kunden für eine Bestellung laden, wie hier dargestellt:
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer> (c => c.Orders);
opt.LoadWith<Order> (o => o.Customer); 
Nun sind alle Daten vorhanden, und Sie können sich mit der Bindung befassen. In diesem Fall erfüllt ein ListView-Steuerelement auf zwei Ebenen den Zweck recht gut. Sie binden das ListView-Steuerelement der obersten Ebene an die Sammlung von Kundenobjekten und das innerste ListView-Steuerelement an die Bestelleigenschaft jedes gebundenen Kundenobjekts. Der Code in Abbildung 4 zeigt das Markup für eine hierarchische Ansicht über drei Ebenen, bei der Kunden auf der ersten Ebene angezeigt und von der ItemTemplate-Eigenschaft des äußersten ListView-Steuerlements gerendert werden. Das eingebettete ListView-Steuerelement ist somit an die Bestellungen gebunden. Schließlich enthält das ItemTemplate-Element des eingebetteten ListView-Steuerelements ein GridView-Steuerelement zum Aufführen der Einzelheiten jeder Bestellung.
<asp:ListView ID="ListView1" runat="server" 
  DataSourceID="ObjectDataSource1"
  ItemPlaceholderID="lvItemPlaceHolder">

  <LayoutTemplate>
    <asp:PlaceHolder runat="server" ID="lvItemPlaceHolder" />
  </LayoutTemplate>

  <ItemTemplate>
    <asp:Panel runat="server" ID="panelCustomerInfo"
      cssclass="customerInfo"> 
      <%# Eval("CompanyName") %>
    </asp:Panel>    
    <asp:panel runat="server" ID="panelCustomerDetails"
      cssclass="customerDetails">
      <asp:ListView runat="server" 
        DataSource='<%# Eval("Orders") %>' 
        ItemPlaceholderID="lvOrdersItemPlaceHolder">

        <LayoutTemplate>
          <ul>
            <asp:PlaceHolder runat="server" 
              ID="lvOrdersItemPlaceHolder" />
          </ul>
        </LayoutTemplate>

        <ItemTemplate>
          <li>
            Order #<%# Eval("OrderID") %> 
            <span class="orderDate"> 
              placed on <%#
              ((DateTime)Eval("OrderDate")).ToString
              ("ddd, dd MMM yyyy") %> 
            </span>
            <span class="orderEmployee"> 
              managed by <b><%# Eval("Employee.LastName") %></b>
            </span>
            <asp:GridView runat="server" 
              DataSource='<%# Eval("Order_Details") %>' 
              SkinID="OrderDetailsGridSkin" >
            </asp:GridView>
          </li>
        </ItemTemplate>
      </asp:ListView>
    </asp:panel>
  </ItemTemplate>
</asp:ListView>


Verbessern der Benutzererfahrung mit Extendern
Die Benutzeroberfläche, die Sie mit dem Code in Abbildung 4 erhalten, ist leider nicht besonders ansprechend. Da eine hierarchische Datenansicht erstellt wird, wäre ein Panel zum Erweitern/Reduzieren eine besonders geeignete Lösung für eine verbesserte Benutzererfahrung. Das ASP.NET AJAX Control Toolkit bietet einen fertigen Extender, der auf ein Panel-Serversteuerelement angewendet wird und den Informationen für alle Kunden und Bestellungen einen Dropdowneffekt hinzufügt.
Verwenden Sie das CollapsiblePanelExtender-Steuerelement zum Definieren eines Panels in der Seitensteuerelementstruktur, das über ein Skript erweitert und reduziert wird. Selbstverständlich müssen Sie als Seitenentwickler kein JavaScript schreiben. Das gesamte, zum Erweitern und Reduzieren des Panels erforderliche Skript wird automatisch vom Extendersteuerelement eingefügt. Werfen wir einen Blick auf die Eigenschaften, die Sie auf dem Extender festlegen wollen:
<act:CollapsiblePanelExtender runat="server" ID="CollapsiblePanel1"  
     TargetControlID="panelCustomerDetails" 
     Collapsed="true"
     ScrollContents="true"
     SuppressPostback="true"
     ExpandedSize="250px"
     ImageControlID="Image1"
     ExpandedImage="~/images/collapse.jpg"
     CollapsedImage="~/images/expand.jpg"
     ExpandControlID="Image1"
     CollapseControlID="Image1">
</act:CollapsiblePanelExtender>
Am Code in Abbildung 4 sind einige geringfügige Änderungen erforderlich, um den reduzierbaren Panelextender zu unterstützen. Insbesondere sollten Sie das Panel mit der Bezeichnung panelCustomerInfo bearbeiten, um die Schaltfläche hinzuzufügen, die zum Erweitern und Reduzieren der untergeordneten Ansicht verwendet wird. Dies ist eine Möglichkeit, das Markup des Panels neu zu schreiben:
<asp:Panel ID="panelCustomerInfo" runat="server"> 
  <div class="customerInfo">
    <div style="float: left;"><%# Eval("CompanyName") %></div>
    <div style="float: right; vertical-align: middle;">
      <asp:ImageButton ID="Image1" runat="server" 
               ImageUrl="~/images/expand.jpg"
               AlternateText="(Show Orders...)"/>
    </div>
  </div>
</asp:Panel> 
Die Schaltfläche wird mithilfe eines rechtsbündigen Bilds in derselben Zeile wie der Kundenname gerendert. Die TargetControlID-Eigenschaft auf dem Extender verweist auf das Panel auf der Seite, das reduziert und erweitert wird. Dies ist der Bereich, in dem die Bestellungen und Bestelldetails physisch enthalten sind. Wie Sie in Abbildung 4 sehen, handelt es sich um das Panel mit der Bezeichnung „panelCustomerDetails“.
Die ExpandControlID- und CollapseControlID-Attribute zeigen die ID der Elemente an, auf die geklickt wird, um das Zielpanel zu erweitern bzw. zu reduzieren. Wenn Sie planen, zur Wiedergabe des Panelzustands verschiedene Bilder zu verwenden, müssen Sie auch die ID eines Image-Steuerelements angeben. Diese Informationen gehören zum ImageControlID-Attribut. Das ImageControlID-Attribut ist mit zwei anderen Eigenschaften (CollapsedImage und ExpandedImage) verknüpft. Diese enthalten die URL der Bilder.
Die ExpandedSize-Eigenschaft legt die für das erweiterte Panel zulässige maximale Höhe in Pixeln fest. Standardmäßig wird Inhalt, der diese Höhe überschreitet, einfach abgeschnitten. Wenn Sie die ScrollContents-Eigenschaft jedoch auf „true“ setzen, wird eine vertikale Bildlaufleiste hinzugefügt, sodass der Benutzer einen Bildlauf durch den gesamten Inhalt durchführen kann.
Schließlich ermöglicht die Collapsed Boolean-Eigenschaft das Einrichten des Anfangszustands des Panels, und SuppressPostback zeigt an, ob die Erweiterung des Panels ein rein clientseitiger Vorgang sein sollte. Wenn SuppressPostback „true“ ist, wird kein Postback zum Erweitern oder Reduzieren des Panels verwendet. Dies bedeutet, dass für die angezeigten Daten keine Updates möglich sind. Für relativ statische Daten, die sich nicht oft ändern, ist dies definitiv die bestmögliche Auswahl, da Seitenflimmern und Netzwerkverkehr verringert werden. Wenn Sie jedoch Daten im Steuerelement dynamischer anzeigen müssen, können Sie das Flimmern mithilfe eines UpdatePanel-Steuerelements verringern. Abbildung 5 zeigt die resultierende Benutzeroberfläche einer Datenansicht mit drei Ebenen.
Abbildung 5 Datenansicht mit drei Ebenen (Klicken Sie zum Vergrößern auf das Bild)

DataPager und ListView
Das ListView-Steuerelement bietet über das neue DataPager-Steuerelement Auslagerungsfunktionen. Das DataPager-Steuerelement ist ein Allzwecksteuerelement für die Auslagerung, das von jedem datengebundenen Steuerelement verwendet werden kann, das die IPageableItemContainer-Schnittstelle implementiert. Ab ASP.NET 3.5 ist das ListView-Steuerelement das einzige Steuerelement, das diese Schnittstelle unterstützt.
Das DataPager-Steuerelement kann eine integrierte oder vorlagenbasierte Benutzeroberfläche anzeigen. Wenn der Benutzer mit einem Klick auf eine neue Seite wechselt, ruft das DataPager-Steuerelement eine Methode auf der IPageableItemContainer-Schnittstelle auf. Von dieser Methode wird erwartet, dass sie interne Variablen im ausgelagerten Steuerelement festlegt, sodass nur eine spezifische Datenseite während des nächsten Datenbindungsvorgangs angezeigt wird.
Es stellt sich heraus, dass das Auswählen der richtigen Datenseite weiterhin vom datengebundenen Steuerelement vorgenommen werden muss, in diesem Fall ListView. Genau wie andere „Ansichten“-Steuerelemente in ASP.NET verwendet das ListView-Steuerelement externen Code zum Auslagern. Wenn Daten durch die Datenquelleneigenschaft gebunden sind, sollte der Benutzercode die ausgelagerten Daten bereitstellen. Andernfalls sollten Sie das Datenquellen-Steuerelement richtig für die Unterstützung der Auslagerung konfigurieren, wenn Daten durch ein Datenquellen-Steuerelement gebunden sind.
Sowohl das LinqDataSource- als auch das ObjectDataSource-Steuerelement bieten integrierte Auslagerungsfunktionen. Das LinqDataSource-Steuerelement verfügt über die AutoPage-Eigenschaft zum Aktivieren oder Deaktivieren der Standardauslagerung. Bei hierarchischen Daten müssen Sie außerdem sicherstellen, dass für den LINQ-Datenkontext richtige Ladeoptionen festgelegt wurden. Die Programmierschnittstelle von LinqDataSource hat keine Eigenschaften zum Festlegen der LoadOptions-Eigenschaft auf dem Datenkontextobjekt. Doch durch Behandeln des ContextCreated-Ereignisses können Sie auf den neu erstellten Datenkontext zugreifen und ihn nach Belieben konfigurieren.
void LinqDataSource1_ContextCreated(
    object sender, LinqDataSourceStatusEventArgs e)
{
    // Get a reference to the data context
    DataContext db = e.Result as DataContext;

    if (db != null)
    {
       DataLoadOptions opt = new DataLoadOptions();
       opt.LoadWith<Customer>(c => c.Orders);
       opt.LoadWith<Order>(o => o.Employee);
       opt.LoadWith<Order>(o => o.Order_Details);
       db.LoadOptions = opt;
    }
}
Als Alternative dazu können Sie das ObjectDataSource-Steuerelement zum Bereitstellen von Daten und Implementieren einer Auslagerungslogik verwenden. Dann können Sie im Geschäftsobjekt entweder LINQ to SQL oder einfach ADO.NET für den Datenzugriff verwenden.
Ein Problem, dem ich bei der gleichzeitigen Verwendung von DataPager und ListView begegnet bin, sollte jedoch erwähnt werden. Anfänglich lag mir eine Inhaltsseite mit ListView und DataPager vor, die im selben Inhaltsplatzhalter gehostet wurden. Ich habe mithilfe der PagedControlID-Eigenschaft auf das ListView-Steuerelement im DataPager verwiesen, wie nachfolgend dargestellt. Es hat gut funktioniert:
<asp:DataPager ID="DataPager1" runat="server" 
     PagedControlID="ListView1"
     PageSize="5" 
     EnableViewState="false">
  <Fields>
     <asp:NextPreviousPagerField 
        ShowFirstPageButton="true" 
        ShowLastPageButton="true" />
  </Fields>
</asp:DataPager>
Als Nächstes verschob ich DataPager in einen anderen Inhaltsbereich auf derselben Masterseite. Plötzlich konnte das DataPager- nicht mit dem ListView-Steuerelement kommunizieren. Das Problem ist auf den vom DataPager-Steuerelement verwendeten Algorithmus zur Suche des ausgelagerten Steuerelements zurückzuführen. Dieser Algorithmus funktioniert nicht, wenn die beiden Steuerelemente von verschiedenen Benennungscontainern gehostet werden. Zur Problemumgehung müssen Sie das ausgelagerte Steuerelement mithilfe seiner vollständigen, eindeutigen ID identifizieren (einschließlich der Benennungscontainerinformationen). Leider können Sie diese Informationen nicht einfach deklarativ festlegen.
Sie können keine Codeblöcke im ASP-Stil verwenden, da sie wie Literale behandelt werden, wenn sie zum Festlegen einer Eigenschaft eines Serversteuerelements verwendet werden. Sie können auch keinen Datenbindungsausdruck <%#... %> verwenden, da der Ausdruck zu spät für die Anforderungen des DataPager-Steuerelements evaluiert wird. Das Load-Ereignis tritt zu spät ein und würde dazu führen, dass das DataPager-Steuerelement eine Ausnahme auslöst. Die einfachste Problemumgehung besteht darin, die PagedControlID-Eigenschaft programmgesteuert wie folgt im Init-Ereignis der Seite festzulegen:
protected void Page_Init(object sender, EventArgs e)
{
   DataPager1.PagedControlID = ListView1.UniqueID;
}

Mehrere Elementvorlagen
Genau wie andere vorlagenbasierte und datengebundene Steuerelemente wiederholt das ListView-Steuerelement dieselbe Elementvorlage für jedes gebundene Datenelement. Was geschieht, wenn Sie es gegen eine bestimmte Teilmenge an Elementen austauschen wollen? In all den Jahren, in denen ich mich mit der ASP.NET-Programmierung beschäftigt habe, musste ich nie mehr als eine Elementvorlage verwenden. Mehrmals habe ich die Darstellung einer kleinen Gruppe von Elementen in DataGrid- und GridView-Steuerelementen auf der Basis von Laufzeitbedingungen angepasst. Dabei wurde jedoch immer ein anderer Satz an Stilattributen angewendet.
Nur in sehr wenigen Fällen wurden der vorhandenen Vorlage programmgesteuert neue Steuerelemente (größtenteils Label-Steuerelemente oder Tabellenzellen) hinzugefügt. In datengebundenen Steuerelementen, die Datenbindungsereignisse auslösen, ist dies keine besonders schwierige Aufgabe, zumindest dann nicht, wenn Sie die interne Struktur der von Ihnen geänderten Steuerelemente gut kennen.
Obwohl das programmgesteuerte Einfügen von Steuerelementen eine Lösung ist, die in der Praxis wirklich gut funktioniert, war ich nie so recht davon überzeugt. So beschloss ich, einen anderen Weg zu gehen, als ein Kunde mich darum bat, ein auf ListView basierendes Menü auf einer Webseite zu ändern. In einem Menü ähnlich wie dem in Abbildung 1 dargestellten mussten die Elemente eines Untermenüs horizontal statt vertikal gerendert werden.
Das ListView-Steuerelement generiert sein Markup durch Durchlaufen der Datenquelle und Anwenden des folgenden Algorithmus. Zuerst wird geprüft, ob ein Elementtrennzeichen erforderlich ist. Wenn dies der Fall ist, instanziiert es die Vorlage und erstellt das Datenelementobjekt. Das Datenelementobjekt ist der Container der Elementvorlage und enthält Informationen über den Index des Elements in der Ansicht und der gebundenen Datenquelle. Beim Instanziieren der Elementvorlage wird das ItemCreated-Ereignis ausgelöst. Der nächste Schritt besteht in der Datenbindung. Nach Abschluss dieses Schritts wird das ItemDataBound-Ereignis ausgelöst.
Wie Sie sehen, gibt es kein öffentliches Ereignis, das Sie behandeln können und das programmgesteuert ein Ändern der Vorlage für jedes Element zulässt. Sie können die Vorlage im Init- oder Load-Seitenereignis ändern, doch dies würde dann für alle gebundenen Elemente gelten. Wenn Sie ItemCreated behandeln und die ItemTemplate-Eigenschaft dort festlegen, wirkt sich die Änderung auf das nächste Element aus, aber nicht auf das Element, das derzeit verarbeitet wird. Es wäre ein ItemCreating-Ereignis erforderlich, aber ein solches Ereignis wird nicht vom ListView-Steuerelement ausgelöst. Die Lösung besteht darin, ein eigenes ListView-Steuerelement zu erstellen, wie in Abbildung 6 dargestellt.
namespace Samples.Controls
{
  public class ListViewItemCreatingEventArgs : EventArgs
  {
    private int _dataItemIndex;
    private int _displayIndex;

    public ListViewItemCreatingEventArgs(int dataItemIndex,
                                         int displayIndex) {
      _dataItemIndex = dataItemIndex;
      _displayIndex = displayIndex;
    }

    public int DisplayIndex {
      get { return _displayIndex; }
      set { _displayIndex = value; }
    }

    public int DataItemIndex {
      get { return _dataItemIndex; }
      set { _dataItemIndex = value; }
    }
  }

  public class ListView : System.Web.UI.WebControls.ListView
  {
    public event EventHandler<ListViewItemCreatingEventArgs>
                                               ItemCreating;

    protected override ListViewDataItem CreateDataItem(int
                           dataItemIndex, int displayIndex) {
      // Fire a NEW event: ItemCreating
      if (ItemCreating != null)
        ItemCreating(this, new ListViewItemCreatingEventArgs
                             (dataItemIndex, displayIndex));

      // Call the base method
      return base.CreateDataItem(_dataItemIndex, displayIndex);
    }
  }
}

Durch Überschreiben der CreateDataItem-Methode haben Sie die Möglichkeit, den Code kurz vor dem Instanziieren der Elementvorlage auszuführen. Die CreateDataItem-Methode wird in der ListView-Klasse als geschützt und virtuell deklariert. Wie Sie in Abbildung 6 sehen, ist das Überschreiben der Methode recht einfach. Zuerst lösen Sie ein benutzerdefiniertes ItemCreating-Ereignis aus und rufen anschließend die Basismethode auf.
Das ItemCreating-Ereignis gibt ein paar Ganzzahlen an den Benutzercode zurück – den absoluten Index des Elements in der Datenquelle und den seitenspezifischen Index. Bei einer Seitengröße von beispielsweise 10 enthält dataItemIndex 11 Elemente und displayIndex enthält 1 Element, wenn das ListView-Steuerelement am Rendern des ersten Elements der zweiten Seite arbeitet. Um das neue ItemCreating-Ereignis zu verwenden, deklarieren Sie einfach die Methode und den Handler auf Ihrem benutzerdefinierten ListView-Steuerelement, wie hier im Code dargestellt:
<x:ListView runat="server" ID="ListView1" 
   ItemPlaceholderID="itemPlaceholder"
   DataSourceID="ObjectDataSource1"
   OnItemCreating="ListView1_ItemCreating">
   <LayoutTemplate>
      <div>
         <asp:PlaceHolder runat="server" ID="itemPlaceholder" /> 
      </div>
   </LayoutTemplate>
</x:ListView>
In Ihrem Code können Sie das Ereignis wie folgt behandeln:
void ListView1_ItemCreating(
     object sender, ListViewItemCreatingEventArgs e)
{
    string url = "standard.ascx";
    if (e.DisplayIndex % DataPager1.PageSize == 0)
        url = "firstItem.ascx";

    ListView1.ItemTemplate = Page.LoadTemplate(url);
}
Hier werden zwei verschiedene Benutzersteuerelemente zum Rendern der Datenelemente eingesetzt. Das spezifische Benutzersteuerelement wird vom Anzeigeindex festgelegt. Alle Elemente mit Ausnahme des ersten haben dieselbe Vorlage. Abbildung 7 zeigt die Seite in Aktion.
Abbildung 7 Mehrere Elementvorlagen (Klicken Sie zum Vergrößern auf das Bild)
Wenn Sie die Komplexität allgemeiner realer Seiten in Betracht ziehen, scheint diese Lösung zu einfach. Meistens müssen Sie verschiedene Vorlagen verwenden, die auf dem anzuzeigenden Inhalt basieren. Das benutzerdefinierte ListView-Steuerelement muss weiter verbessert werden, um die Elementvorlage innerhalb des Datenbindungsprozesses zu ändern. Sehen Sie sich den Code in Abbildung 8 an.
namespace Samples.Controls
{
  public class ListViewItemCreatingEventArgs : EventArgs
  {
    private int _dataItemIndex;
    private int _displayIndex;

    public ListViewItemCreatingEventArgs(int dataItemIndex,
                                         int displayIndex) {
      _dataItemIndex = dataItemIndex;
      _displayIndex = displayIndex;
    }

    public int DisplayIndex {
      get { return _displayIndex; }
      set { _displayIndex = value; }
    }

    public int DataItemIndex {
      get { return _dataItemIndex; }
      set { _dataItemIndex = value; }
    }
  }

  public class ListView : System.Web.UI.WebControls.ListView
  {
    public event EventHandler<ListViewItemCreatingEventArgs>
     ItemCreating;

    private int _displayIndex;
    private bool _shouldInstantiate = false;

    protected override void InstantiateItemTemplate(Control container,
     int displayIndex) {
      if (_shouldInstantiate) {
        base.InstantiateItemTemplate(container, displayIndex);
        _shouldInstantiate = false;
      }
    }

    protected override ListViewDataItem CreateDataItem(int
     dataItemIndex, int displayIndex) {
      // Fire a NEW event: ItemCreating
      if (ItemCreating != null)
        ItemCreating(this, new
          ListViewItemCreatingEventArgs(dataItemIndex,
          displayIndex));

      // Cache for later
      _displayIndex = displayIndex;

      // Call the base method
      return base.CreateDataItem(_dataItemIndex, displayIndex);
    }

    protected override void OnItemCreated(ListViewItemEventArgs e) {
      base.OnItemCreated(e);

      // You can proceed with template instantiation now
      _shouldInstantiate = true;
      InstantiateItemTemplate(e.Item, _displayIndex);
    }
  }
}

Die CreateDataItem-Methode löst das ItemCreating-Ereignis aus und nimmt den Anzeigeindex zur späteren Verwendung in den Zwischenspeicher auf. Außerdem wird die InstantiateItemTemplate-Methode überschrieben, sodass die eigentliche Vorlageninstanziierung verzögert wird. Zu diesem Zweck wird ein privates boolesches Kennzeichen verwendet. Wie bereits erwähnt, startet das ListView-Steuerelement den Datenbindungsprozess nach Instanziieren der Elementvorlage.
In der im Code in Abbildung 8 dargestellten Implementierung werden die Elementvorlagen erst instanziiert, wenn das ItemCreated-Ereignis ausgelöst wird. Bei Auslösung des ItemCreated-Ereignisses wird das Datenelementobjekt über die DataItem-Eigenschaft an den ListView-Elementcontainer gebunden. Durch Behandeln des ItemCreated-Ereignisses in Ihrem Code können Sie entscheiden, welche Elementvorlage auf der Grundlage des gebundenen Datenelements verwendet werden soll, wie Sie hier sehen:
protected override void OnItemCreated(ListViewItemEventArgs e)
{
   base.OnItemCreated(e);

   _shouldInstantiate = true;
   InstantiateItemTemplate(e.Item, _displayIndex);
}
In diesem Fall löst die Basismethode das ItemCreated-Ereignis für die Seite aus. Danach setzt das benutzerdefinierte ListView-Steuerelement das boolesche Kennzeichen zurück und ruft die Methode zum Instanziieren der Elementvorlage auf. Die Elementvorlage wird somit ein bisschen später als im integrierten ListView-Steuerelement instanziiert, aber Sie können die ItemTemplate-Eigenschaft für jedes Element programmgesteuert im ItemCreated-Ereignishandler festlegen, nachdem Sie sich den Inhalt des gebundenen Datenelements angesehen haben (siehe Abbildung 9). Abbildung 10 zeigt eine Beispielseite, auf der eine blaue Vorlage für Männer und eine rosafarbene Vorlage für Frauen verwendet wird.
void ListView1_ItemCreated(object sender, ListViewItemEventArgs e)
{
    // Grab a reference to the data item
    ListViewDataItem currentItem = (e.Item as ListViewDataItem);
    Employee emp = (Employee) currentItem.DataItem;
    if (emp == null)
        return;

    // Apply your logic here
    string titleOfCourtesy = emp.TitleOfCourtesy.ToLower();
    string url = "forgentlemen.ascx";
    if (titleOfCourtesy == "ms." || titleOfCourtesy == "mrs.")
        url = "forladies.ascx";

    // Set the item template to use
    Samples.ListView ctl = (sender as Samples.Controls.ListView);
    ctl.ItemTemplate = Page.LoadTemplate(url);
}

Abbildung 10 Ein Standardmenü (Klicken Sie zum Vergrößern auf das Bild)

Zusammenfassung
Letztendlich ist das neue ASP.NET 3.5-ListView-Steuerelement eine neu gestaltete Version des DataList-Steuerelements, das es seit ASP.NET 1.0 gibt. Das ListView-Steuerelement bietet eine stärkere Kontrolle über das generierte Markup sowie eine umfassende Unterstützung für Datenquellenobjekte.
In diesem Artikel wurde beschrieben, wie verschachtelte ListView-Steuerelemente zum Erstellen von auslagerbaren Datenansichten für mehrere Ebenen verwendet werden können und wie sich der Standardrenderingprozess des ListView-Steuerelements durch Ableiten eines benutzerdefinierten Steuerelements und Überschreiben einiger Methoden ändern lässt. Das Endergebnis ist ein Steuerelement, das mehrere Elementvorlagen unterstützt. Kein anderes datengebundenes ASP.NET-Steuerelement bietet ein solches Maß an Flexibilität.

Senden Sie Fragen und Kommentare für Dino Esposito (in englischer Sprache) an cutting@microsoft.com.


Dino Esposito ist IDesign-Architekt und Autor von Programming ASP.NET 3.5 Core Reference. Er lebt in Italien und ist ein weltweit gefragter Referent bei Branchenveranstaltungen. Sie erreichen ihn unter der Adresse cutting@microsoft.com. Seinen Blog finden Sie unter weblogs.asp.net/despos.

Page view tracker