Skapa ett anpassat sorteringsgränssnitt (C#)

av Scott Mitchell

Ladda ned PDF

När du visar en lång lista med sorterade data kan det vara till stor hjälp att gruppera relaterade data genom att introducera avgränsarrader. I den här självstudien får vi se hur du skapar ett sådant sorteringsanvändargränssnitt.

Inledning

När du visar en lång lista med sorterade data där det bara finns en handfull olika värden i den sorterade kolumnen kan en slutanvändare ha svårt att urskilja var, exakt, skillnadens gränser uppstår. Det finns till exempel 81 produkter i databasen, men bara nio olika kategorival (åtta unika kategorier plus NULL alternativet). Tänk på fallet med en användare som är intresserad av att undersöka de produkter som omfattas av kategorin Skaldjur. Från en sida som listar alla produkter i en enda GridView kan användaren bestämma att hennes bästa val är att sortera resultaten efter kategori, som grupperar alla Seafood-produkter tillsammans. Efter sortering efter kategori måste användaren sedan jaga igenom listan och leta efter var fisk- och skaldjursgrupperade produkter börjar och slutar. Eftersom resultaten sorteras alfabetiskt efter kategorinamnet är det inte svårt att hitta fisk- och skaldjursprodukterna, men det krävs fortfarande en noggrann genomsökning av listan över objekt i rutnätet.

För att hjälpa till att markera gränserna mellan sorterade grupper använder många webbplatser ett användargränssnitt som lägger till en avgränsare mellan sådana grupper. Avgränsare som de som visas i bild 1 gör det möjligt för en användare att snabbare hitta en viss grupp och identifiera dess gränser, samt fastställa vilka distinkta grupper som finns i data.

Varje kategorigrupp är tydligt identifierad

Bild 1: Varje kategorigrupp är tydligt identifierad (Klicka om du vill visa en bild i full storlek)

I den här självstudien får vi se hur du skapar ett sådant sorteringsanvändargränssnitt.

Steg 1: Skapa en standard, sorterbar GridView

Innan vi utforskar hur du utökar GridView för att tillhandahålla det förbättrade sorteringsgränssnittet ska vi först skapa ett standard, sorterbart GridView som listar produkterna. Börja med att öppna sidan CustomSortingUI.aspx i PagingAndSorting mappen. Lägg till en GridView på sidan, ange dess ID egenskap till ProductListoch binda den till en ny ObjectDataSource. Konfigurera ObjectDataSource att använda ProductsBLL klassens GetProducts() -metod för att välja poster.

Konfigurera sedan GridView så att den bara innehåller ProductName, CategoryName, SupplierNameoch UnitPrice BoundFields och CheckBoxField som upphört. Slutligen konfigurerar du GridView så att det stöder sortering genom att markera kryssrutan Aktivera sortering i GridViews smarta tagg (eller genom att ange dess AllowSorting egenskap till true). När du har gjort dessa tillägg på CustomSortingUI.aspx sidan bör deklarativ markering se ut ungefär så här:

<asp:GridView ID="ProductList" runat="server" AllowSorting="True"
    AutoGenerateColumns="False" DataKeyNames="ProductID"
    DataSourceID="ObjectDataSource1" EnableViewState="False">
    <Columns>
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category"
            ReadOnly="True" SortExpression="CategoryName" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier"
            ReadOnly="True" SortExpression="SupplierName" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:C}"
            HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice" />
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts"
    TypeName="ProductsBLL"></asp:ObjectDataSource>

Ta en stund att se våra framsteg hittills i en webbläsare. Bild 2 visar det sorterbara GridView när dess data sorteras efter kategori i alfabetisk ordning.

Sortable GridView-data sorteras efter kategori

Bild 2: Sorterbara GridView-data sorteras efter kategori (Klicka om du vill visa en bild i full storlek)

Steg 2: Utforska tekniker för att lägga till avgränsarrader

När det allmänna, sorterbara GridView är klart är allt som återstår att kunna lägga till avgränsarraderna i GridView före varje unik sorterad grupp. Men hur kan sådana rader matas in i GridView? I princip måste vi iterera genom GridView-raderna, avgöra var skillnaderna uppstår mellan värdena i den sorterade kolumnen och sedan lägga till lämplig avgränsarrad. När du tänker på det här problemet verkar det naturligt att lösningen finns någonstans i GridViews RowDataBound händelsehanterare. Som vi beskrev i självstudien Anpassad formatering baserat på data används den här händelsehanteraren ofta när du använder formatering på radnivå baserat på raddata. Händelsehanteraren är dock RowDataBound inte lösningen här, eftersom rader inte kan läggas till i GridView programmatiskt från den här händelsehanteraren. GridView-samlingen Rows är i själva verket skrivskyddad.

För att lägga till ytterligare rader i GridView har vi tre alternativ:

  • Lägg till dessa metadataavgränsningsrader till faktiska data som är bundna till GridView
  • När GridView har bundits till data lägger du till ytterligare TableRow instanser i GridView-kontrollsamlingen
  • Skapa en anpassad serverkontroll som utökar GridView-kontrollen och åsidosätter de metoder som ansvarar för att konstruera GridView-strukturen

Att skapa en anpassad serverkontroll skulle vara den bästa metoden om den här funktionen behövdes på många webbsidor eller på flera webbplatser. Det skulle dock innebära en hel del kod och en grundlig utforskning av djupet i GridViews interna arbete. Därför ska vi inte överväga det alternativet för den här självstudien.

De andra två alternativen som lägger till avgränsarrader till de faktiska data som är bundna till GridView och manipulerar GridView-kontrollsamlingen efter att den har bundits – angriper problemet på ett annat sätt och förtjänar en diskussion.

Lägga till rader i data som är bunden till GridView

När GridView är bundet till en datakälla skapas en GridViewRow för varje post som returneras av datakällan. Därför kan vi mata in de avgränsarrader som behövs genom att lägga till avgränsarposter i datakällan innan vi binder den till GridView. Bild 3 illustrerar det här konceptet.

En teknik innebär att lägga till avgränsarrader i datakällan

Bild 3: En teknik innebär att lägga till avgränsarrader i datakällan

Jag använder termer "avgränsarposter" i citattecken eftersom det inte finns några särskilda avgränsarposter; istället måste vi på något sätt markera att en viss post i datakällan fungerar som en avgränsare snarare än en normal datarad. I våra exempel binder vi en ProductsDataTable instans till GridView, som består av ProductRows. Vi kan flagga en post som en avgränsarrad genom att ange dess CategoryID egenskap till -1 (eftersom ett sådant värde inte kunde finnas normalt).

För att använda den här tekniken behöver vi utföra följande steg:

  1. Hämta data programmatiskt för att binda till GridView (en ProductsDataTable instans)
  2. Sortera data baserat på GridView-egenskaper SortExpression och SortDirection -egenskaper
  3. Iterera genom ProductsRows i ProductsDataTable, och leta efter var skillnaderna ligger i den sorterade kolumnen.
  4. Vid varje gruppgräns infogar du en avgränsningspostinstans ProductsRow i DataTable, en med CategoryID inställd på -1 (eller vilket namn som beslutades för att markera en post som en avgränsningspost)
  5. När du har matat in avgränsarraderna binder du programmatiskt data till GridView

Utöver dessa fem steg behöver vi även tillhandahålla en händelsehanterare för GridView-händelsen RowDataBound . Här skulle vi kontrollera varje DataRow och avgöra om det var en avgränsarrad, där CategoryID-inställningen var -1. I så fall skulle vi förmodligen vilja justera dess formatering eller texten som visas i cellerna.

Att använda den här tekniken för att mata in sorteringsgruppens gränser kräver lite mer arbete än vad som beskrivs ovan, eftersom du också måste ange en händelsehanterare för GridView-händelsen Sorting och hålla reda på SortExpression värdena och SortDirection .

Manipulatora GridView-kontrollsamlingen efter att den har blivit databindad.

I stället för att skicka meddelanden till data innan de binds till GridView kan vi lägga till avgränsarrader efter att data har bundits till GridView. Processen med databindning bygger upp GridView-kontrollhierarkin, som i själva verket bara är en Table instans som består av en samling rader, som var och en består av en cellsamling. Mer specifikt innehåller GridView-kontrollsamlingen ett Table objekt i roten, en GridViewRow (som härleds från TableRow-klassen) för varje post i DataSource som är bunden till GridView och ett TableCell objekt i varje GridViewRow instans för varje datafält i DataSource.

Om du vill lägga till avgränsarrader mellan varje sorteringsgrupp kan vi direkt ändra den här kontrollhierarkin när den har skapats. Vi kan vara säkra på att GridView-kontrollhierarkin har skapats för sista gången när sidan återges. Den här metoden åsidosätter klassens PageRender metod, vilket gör att GridViews slutliga kontrollhierarki uppdateras för att inkludera de avgränsarrader som behövs. Bild 4 illustrerar den här processen.

En alternativ teknik manipulerar GridView-kontrollhierarkin

Bild 4: En alternativ teknik manipulerar GridView-kontrollhierarkin (Klicka om du vill visa en bild i full storlek)

I den här självstudien använder vi den här senare metoden för att anpassa sorteringsanvändarupplevelsen.

Anmärkning

Koden jag presenterar i den här självstudien baseras på exemplet i Teemu Keiskis blogginlägg, Playing a Bit with GridView Sort Grouping.

Steg 3: Lägga till avgränsarrader i GridView-kontrollhierarkin

Eftersom vi bara vill lägga till avgränsarraderna i GridView-kontrollhierarkin efter att dess kontrollhierarki har skapats och skapats för sista gången på det sidbesöket, vill vi utföra det här tillägget i slutet av sidlivscykeln, men innan den faktiska GridView-kontrollhierarkin har renderats i HTML. Den senaste möjliga punkten där vi kan åstadkomma detta är Page klassens Render händelse, som vi kan åsidosätta i vår kod-bakom-klass med hjälp av följande metodsignatur:

protected override void Render(HtmlTextWriter writer)
{
    // Add code to manipulate the GridView control hierarchy
    base.Render(writer);
}

När klassens Page ursprungliga Render metod anropas återges base.Render(writer) var och en av kontrollerna på sidan, vilket genererar markering baserat på deras kontrollhierarki. Därför är det absolut nödvändigt att vi båda anropar base.Render(writer), så att sidan återges och att vi ändrar GridView-kontrollhierarkin innan vi anropar base.Render(writer), så att avgränsarraderna har lagts till i GridView-kontrollhierarkin innan den återges.

För att mata in sortgruppsrubrikerna måste vi först se till att användaren har begärt att data sorteras. Som standard sorteras inte GridView-innehållet och därför behöver vi inte ange några gruppsorteringshuvuden.

Anmärkning

Om du vill att GridView ska sorteras efter en viss kolumn när sidan först läses in, anropar du GridView:s Sort metod vid första sidbesöket (men inte vid efterföljande uppdateringar av sidan). För att uppnå detta, lägg till det här anropet i händelsehanteraren Page_Load inom en villkorssats if (!Page.IsPostBack). Se tillbaka till handledningen Datasideindelning och sorteringsrapport för mer information om Sort metoden.

Om vi antar att data har sorterats är nästa uppgift att avgöra vilken kolumn data sorterades efter och sedan genomsöka raderna som söker efter skillnader i kolumnens värden. Följande kod säkerställer att data har sorterats och hittar kolumnen som data har sorterats efter:

protected override void Render(HtmlTextWriter writer)
{
    // Only add the sorting UI if the GridView is sorted
    if (!string.IsNullOrEmpty(ProductList.SortExpression))
    {
        // Determine the index and HeaderText of the column that
        //the data is sorted by
        int sortColumnIndex = -1;
        string sortColumnHeaderText = string.Empty;
        for (int i = 0; i < ProductList.Columns.Count; i++)
        {
            if (ProductList.Columns[i].SortExpression.CompareTo(ProductList.SortExpression)
                == 0)
            {
                sortColumnIndex = i;
                sortColumnHeaderText = ProductList.Columns[i].HeaderText;
                break;
            }
        }
        // TODO: Scan the rows for differences in the sorted column�s values
}

Om GridView ännu inte har sorterats har egenskapen GridView inte angetts SortExpression . Därför vill vi bara lägga till avgränsarrader om den här egenskapen har något värde. Om det gör det måste vi nu fastställa indexet för kolumnen som data sorterades efter. Detta uppnås genom att loopa igenom GridView-samlingen Columns och söka efter kolumnen vars SortExpression egenskap är lika med GridView-egenskapen SortExpression . Förutom kolumnindexet, hämtar vi även HeaderText-egenskapen, som används när avgränsarraderna visas.

Med indexet för kolumnen som data sorteras efter är det sista steget att räkna upp raderna i GridView. För varje rad måste vi avgöra om värdet för den sorterade kolumnen skiljer sig från föregående rads sorterade kolumnvärde. I så fall måste vi mata in en ny GridViewRow instans i kontrollhierarkin. Detta görs med följande kod:

protected override void Render(HtmlTextWriter writer)
{
    // Only add the sorting UI if the GridView is sorted
    if (!string.IsNullOrEmpty(ProductList.SortExpression))
    {
        // ... Code for finding the sorted column index removed for brevity ...
        // Reference the Table the GridView has been rendered into
        Table gridTable = (Table)ProductList.Controls[0];
        // Enumerate each TableRow, adding a sorting UI header if
        // the sorted value has changed
        string lastValue = string.Empty;
        foreach (GridViewRow gvr in ProductList.Rows)
        {
            string currentValue = gvr.Cells[sortColumnIndex].Text;
            if (lastValue.CompareTo(currentValue) != 0)
            {
                // there's been a change in value in the sorted column
                int rowIndex = gridTable.Rows.GetRowIndex(gvr);
                // Add a new sort header row
                GridViewRow sortRow = new GridViewRow(rowIndex, rowIndex,
                    DataControlRowType.DataRow, DataControlRowState.Normal);
                TableCell sortCell = new TableCell();
                sortCell.ColumnSpan = ProductList.Columns.Count;
                sortCell.Text = string.Format("{0}: {1}",
                    sortColumnHeaderText, currentValue);
                sortCell.CssClass = "SortHeaderRowStyle";
                // Add sortCell to sortRow, and sortRow to gridTable
                sortRow.Cells.Add(sortCell);
                gridTable.Controls.AddAt(rowIndex, sortRow);
                // Update lastValue
                lastValue = currentValue;
            }
        }
    }
    base.Render(writer);
}

Den här koden börjar med att programmatiskt referera till Table objektet som finns i roten i GridView-kontrollhierarkin och skapa en strängvariabel med namnet lastValue. lastValue används för att jämföra den aktuella radens sorterade kolumnvärde med värdet för föregående rad. Sedan räknas GridView-samlingen Rows upp och för varje rad lagras värdet för den sorterade kolumnen i variabeln currentValue .

Anmärkning

För att fastställa värdet för den specifika radens sorterade kolumn använder jag cellens Text egenskap. Detta fungerar bra för BoundFields, men fungerar inte som önskat för TemplateFields, CheckBoxFields och så vidare. Vi ska titta på hur du tar hänsyn till alternativa GridView-fält inom kort.

Variablerna currentValue och lastValue jämförs sedan. Om de skiljer sig åt måste vi lägga till en ny avgränsarrad i kontrollhierarkin. Detta uppnås genom att fastställa indexet GridViewRow för i Table objektets Rows samling, skapa nya GridViewRow instanser och TableCell sedan lägga till TableCell och GridViewRow till kontrollhierarkin.

Observera att den ensamma avgränsarraden TableCell är formaterat så att den sträcker sig över hela bredden på GridView, är formaterat med CSS-klassen SortHeaderRowStyle och har sin Text-egenskap inställd så att den visar både sorteringsgruppens namn (till exempel Kategori) och gruppens värde (till exempel Drycker). Slutligen uppdateras lastValue till värdet currentValue.

Den CSS-klass som används för att formatera sorteringsgruppens rubrikrad SortHeaderRowStyle måste anges i Styles.css filen. Använd gärna de stilinställningar som tilltalar dig; Jag använde följande:

.SortHeaderRowStyle
{
    background-color: #c00;
    text-align: left;
    font-weight: bold;
    color: White;
}

Med den aktuella koden lägger sorteringsgränssnittet till sortgruppsrubriker vid sortering efter boundfield (se bild 5, som visar en skärmbild när du sorterar efter leverantör). Men när du sorterar efter någon annan fälttyp (till exempel en CheckBoxField eller TemplateField) går det inte att hitta sortgruppsrubrikerna (se bild 6).

Sorteringsgränssnittet innehåller sorteringsgrupprubriker vid sortering efter BoundFields

Bild 5: Sorteringsgränssnittet innehåller sorteringsgrupprubriker vid sortering efter BoundFields (Klicka om du vill visa en bild i full storlek)

Sidhuvudena för sorteringsgrupper saknas när du sorterar ett CheckBoxField

Bild 6: Sidhuvudena för sorteringsgrupper saknas vid sortering av en kryssrutaFält (Klicka om du vill visa en bild i full storlek)

Anledningen till att sortgruppens rubriker saknas vid sortering efter en CheckBoxField är att koden för närvarande bara TableCell använder egenskapen s Text för att fastställa värdet för den sorterade kolumnen för varje rad. För CheckBoxFields är egenskapen TableCell s Text en tom sträng; istället är värdet tillgängligt via en CheckBox-webbkontroll som finns inom TableCell s Controls-samlingen.

För att hantera andra fälttyper än BoundFields behöver vi utöka koden där variabeln currentValue har tilldelats för att kontrollera om det finns en kryssruta i TableCell s-samlingen Controls . I stället för att använda currentValue = gvr.Cells[sortColumnIndex].Textersätter du den här koden med följande:

string currentValue = string.Empty;
if (gvr.Cells[sortColumnIndex].Controls.Count > 0)
{
    if (gvr.Cells[sortColumnIndex].Controls[0] is CheckBox)
    {
        if (((CheckBox)gvr.Cells[sortColumnIndex].Controls[0]).Checked)
            currentValue = "Yes";
        else
            currentValue = "No";
    }
    // ... Add other checks here if using columns with other
    //      Web controls in them (Calendars, DropDownLists, etc.) ...
}
else
    currentValue = gvr.Cells[sortColumnIndex].Text;

Den här koden undersöker den sorterade kolumnen TableCell för den aktuella raden för att avgöra om det finns några kontroller i Controls samlingen. Om det finns, och den första kontrollen är en kryssruta, är variabeln currentValue inställd på Ja eller Nej, beroende på egenskapen CheckBox Checked . Annars hämtas värdet från TableCell egenskapen s Text . Den här logiken kan replikeras för att hantera sortering för alla TemplateFields som kan finnas i GridView.

Med den kodtillägg som nämns ovan visas nu sorteringsgruppsrubrikerna när man sorterar efter kryssrutan 'Avslutad' (se figur 7).

Sidhuvudena för sorteringsgrupper finns nu när du sorterar ett kryssrutafält

Bild 7: Sidhuvudena för sorteringsgrupper finns nu när du sorterar ett kryssfält (Klicka om du vill visa en bild i full storlek)

Anmärkning

Om du har produkter med NULL databasvärden för fälten CategoryID, SupplierIDeller UnitPrice visas dessa värden som tomma strängar i GridView som standard, vilket innebär att avgränsarradens text för dessa produkter med NULL värden kommer att läsas som Kategori: (det vill säga det finns inget namn efter Kategori: som med Kategori: Drycker ). Om du vill att ett värde ska visas här kan du antingen ange egenskapen BoundFields NullDisplayText till den text som du vill visa eller så kan du lägga till en villkorssats i render-metoden currentValue när du tilldelar egenskapen till avgränsarens rad.Text

Sammanfattning

GridView innehåller inte många inbyggda alternativ för att anpassa sorteringsgränssnittet. Men med lite lågnivåkod går det att justera GridView-kontrollhierarkin för att skapa ett mer anpassat gränssnitt. I den här självstudien såg vi hur du lägger till en sorteringsgruppavgränsningsrad för en sorterbar GridView, som enklare identifierar distinkta grupper och dessa gruppers gränser. Ytterligare exempel på anpassade sorteringsgränssnitt finns i blogginlägget Scott Guthrie s A Few ASP.NET 2.0 GridView Sorting Tips and Tricks .

Lycka till med programmerandet!

Om författaren

Scott Mitchell, författare till sju ASP/ASP.NET-böcker och grundare av 4GuysFromRolla.com, har arbetat med Microsofts webbtekniker sedan 1998. Scott arbetar som oberoende konsult, tränare och författare. Hans senaste bok är Sams Teach Yourself ASP.NET 2.0 på 24 timmar. Han kan nås på mitchell@4GuysFromRolla.com.