in

dotNet Umbria

Il primo User Group in Umbria sul mondo .Net

Articoli

Articoli vari degli iscritti a DotNetUmbria

Paginazione Lato Server Parte 1 - Oggetti che supportano la paginazione

Questa serie di articoli si pone l'obbiettivo d'illustrare le possibili soluzioni ai problemi che sorgono allorquando si espongono al mondo web, con l'ausilio di ASP .Net 2.0, liste molto consistenti.

Qualora si provi (forse lo si è già fatto) a collegare direttamente una tabella contente un consistente numero di record, più di diecimila ad esempio, ad una GridView e di seguito si tenti di demandare la paginazione direttamente al client, gestita cioè dalla GridView, si noterà un consistente e a volte fatale rallentamento del render della pagina.

La ragione del rallentamento è che, nonostante l'oggetto mostri solo il numero di record stabilito, il server invia tutti i record al client e quest'ultimo li renderizza tutti, nascondendo la porzione di dati che in quel momento non ritiene necessaria.

Se il numero di record considerato rimane nell'ordine delle migliaia, bhe,  forse si potrebbe optare per soluzioni pragmatiche e decidere di non sprecare tempo per gestire la paginazione "server side";  se tuttavia il numero di record si sposta sulla scala delle decine di migliaia, in questo caso la paginazione lato server si rende assolutamente necessaria.

L'obbiettivo del primo scenario che ho intenzione di analizzare consiste nel mostrare una serie di dati in forma tabellare; l'oggetto GridView fa pertanto al caso nostro.

A questo articolo, e a quello che seguiranno, è allegato un progetto di esempio, GPExamples.zip che potete scaricare liberamente, nel quale gradualmente verranno implementate le logiche.
Il progetto contiene degli esempi il cui solo scopo è quello di mostrare alcune delle possibili tecniche di paginazione lato server e sebbene sia presente un certo livello di astrazione e di suddivisione in strati, esso non vuole essere un esempio di applicazione distribuita.

Il database di riferimento sarà AdventureWorks scaricabile liberamente dal Download Center Microsoft 

Il primo passo da compiere è quello di creare un nuovo sito web da Visual Studio 2005 a cui aggiungere due cartelle "DB" e "Oggetti" all'interno della cartella App_Code

All'interno della cartella Oggetti andremo a definire un tipo per i prodotti che chiameremo Prodotto.cs definito come segue

[code language="C#"]

using System;
using System.Collections.Generic;
using System.Text;
namespace GPExamples.Oggetti
{
    /// <summary>
    /// il Tipo contenente la definizione del Prodotto
    /// </summary>
    [Serializable]
    public class Prodotto
    {
        #region Proprietà
        private int _idProdotto;
        /// <summary>
        /// Restituisce o imposta (Gets or sets) l'ID Univoco del Prodotto.
        /// </summary>
        public int idProdotto
        {
            get { return _idProdotto; }
            set { _idProdotto = value; }
        }
        private string _nomeProdotto;
        /// <summary>
        /// Restituisce o imposta (Gets or sets) il nome del Prodotto.
        /// </summary>
        public string nomeProdotto
        {
            get { return _nomeProdotto; }
            set { _nomeProdotto = value; }
        }
        private string _descProdotto;
        /// <summary>
        /// Restituisce o imposta (Gets or sets) la descrizione del Prodotto.
        /// </summary>
        public string descProdotto
        {
            get { return _descProdotto; }
            set { _descProdotto = value; }
        }
        private string _modelloProdotto;
        /// <summary>
        /// Restituisce o imposta (Gets or sets) il modello del Prodotto.
        /// </summary>
        public string modelloProdotto
        {
            get { return _modelloProdotto; }
            set { _modelloProdotto = value; }
        }
        #endregion
        #region Costruttori
        /// <summary>
        /// Costruttore di default per Prodotto
        /// </summary>
        public Prodotto()
        {
            //
            // TODO: Add constructor logic here
            //
        }
        /// <summary>
        /// Overload del costruttore Prodotti
        /// </summary>
        /// <param name="idProdotto">ID Univoco del Prodotto</param>
        /// <param name="nomeProdotto">Nome del Prodotto</param>
        /// <param name="descProdotto">Descrizione del Prodotto</param>
        /// <param name="modelloProdotto">Modello del Prodotto</param>
        public Prodotto(int idProdotto, string nomeProdotto, string descProdotto, string modelloProdotto)
        {
            this._idProdotto = idProdotto;
            this._nomeProdotto = nomeProdotto;
            this._descProdotto = descProdotto;
            this._modelloProdotto = modelloProdotto;
        }
        #endregion
    }
}

[/code]

e un oggetto di "facade" che chiameremo ProdottiFacade definito come segue

[code language="C#"]

using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using GPExamples.DB;

namespace GPExamples.Oggetti
{
    /// <summary>
    /// Summary description for ProdottiFacade
    /// </summary>
    // [DataObject(true)] Questo attributo indica che questa classe è adatta per creare oggetti ObjectDataSource a design time
    [DataObject(true)]
    public static class ProdottiFacade
    {
        private static ProdottiDao prodottiDao = new ProdottiDao();
        private static int _numOrdini;

        static ProdottiFacade()
        {
            //
            // TODO: Add constructor logic here
            //
        }

        /// <summary>
        /// Restituisce (Gets) una lista di Prodotti paginati e ordinati secondo i parametri passati
        /// </summary>
        /// <param name="startRowIndex">il numero iniziale della riga da restituire</param>
        /// <param name="maximumRows">il numero totale di righe da restituire</param>
        /// <param name="sortColumn">il nome della proprietà della classe Prodotto secondo la quale ordianre</param>
        /// <param name="sortDirection">la direzione dell'ordinamento ASC o DESC</param>
        /// <returns>una lista di prodotti</returns>
        [DataObjectMethod(DataObjectMethodType.Select)]
        public static IList<Prodotto> GetProdottiPaginati(int startRowIndex, int numRows, string sortColumn, string sortDirection)
        {
            int sortParam = sortDirection == "ASC" ? 1 : -1;

            if (!string.IsNullOrEmpty(sortColumn))
            {
                sortColumn = sortColumn.Split(' ')[0];
            }
            else
            {
                sortColumn = "";
            }

            return prodottiDao.GetProdottiPaginati(startRowIndex, numRows, sortColumn, sortParam);
        }

        /// <summary>
        /// Restituisce (Gets) una intero che rappresenta il numero totale di prodotti
        /// </summary>
        /// <param name="startRowIndex">il numero iniziale della riga da restituire</param>
        /// <param name="maximumRows">il numero totale di righe da restituire</param>
        /// <param name="sortColumn">il nome della proprietà della classe Prodotto secondo la quale ordianre</param>
        /// <param name="sortDirection">la direzione dell'ordinamento ASC o DESC</param>
        /// <returns>un intero che rappresenta il numero totale di prodotti</returns>
        [DataObjectMethod(DataObjectMethodType.Select)]
        public static int GetProdottiPaginatiCount(int startRowIndex, int numRows, string sortColumn, string sortDirection)
        {
            return prodottiDao.GetProdottiPaginatiCount();
        }

    }
}

[/code]

L'oggetto ProdottiFacade espone dei metodi GetProdottiPaginati e  GetProdottiPaginatiCount che verranno invocati dall'ObjectDataSource al quale sarà collegata la GridView in questo modo saremo noi a decidere cosa e sopratutto quanti record inviare al client. Tuttavia l'ObjectDataSource e la GridView saranno ancora in grado di esporre la paginazione infatti grazie al metodo GetProdottiPaginatiCount sono in grado di conoscere il numero totale dei record con il quale calcolare il numero delle pagine in modo automatico e trasparente per lo sviluppatore.

A loro volta i metodi di ProdottiFacede invocano dei metodi dell'oggetto ProdottoDao (DAO è l'acronimo di Data Access Object) che si occuperà di recuperare i dati dalla base di dati.
ProdottoDao, nella cartella DB, è definito come segue:

[code language="C#"]

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Data.Common;
using GPExamples.Oggetti;

namespace GPExamples.DB
{
    /// <summary>
    /// classe per l'accesso ai dati
    /// </summary>
    public class ProdottiDao
    {
        public IList<Prodotto> GetProdottiPaginati(int startRowIndex, int numRows, string sortColumn, int sortDirection)
        {
            IList<Prodotto> list = new List<Prodotto>();
            using (SqlConnection connection = new SqlConnection())
            {
                connection.ConnectionString = "Data Source=PC-GIANPAOLO\\SQLGIANPAOLO;Initial Catalog=AdventureWorks;Integrated Security=True";
                using (SqlCommand command = new SqlCommand())
                {
                    command.Connection = connection;
                    command.CommandText = "GetProdottiPaginati";
                    SqlParameter[] sqlParams = new SqlParameter[4];
                    sqlParams[0] = new SqlParameter("@StartRowIndex", SqlDbType.Int, 0);
                    sqlParams[0].Value = startRowIndex;
                    sqlParams[1] = new SqlParameter("@NumRows", SqlDbType.Int, 0);
                    sqlParams[1].Value = numRows;
                    sqlParams[2] = new SqlParameter("@sortDirection", SqlDbType.Int, 0);
                    sqlParams[2].Value = sortDirection;
                    sqlParams[3] = new SqlParameter("@sortColumn", SqlDbType.NVarChar, 25);
                    sqlParams[3].Value = sortColumn;

                    command.CommandType = CommandType.StoredProcedure;

                    foreach (SqlParameter sqlParameter in sqlParams)
                    {
                        command.Parameters.Add(sqlParameter);
                    }

                    using (SqlDataAdapter adapter = new SqlDataAdapter())
                    {
                        adapter.SelectCommand = command;

                        //TODO: Aggiugere controllo in caso di quesry errata o dataset vuoto
                        DataSet ds = new DataSet();
                        adapter.Fill(ds);

                        foreach (DataRow dr in ds.Tables[0].Rows)
                        {
                            list.Add(new Prodotto((int)dr["ProductID"], dr["Name"].ToString(), dr["Description"].ToString(), dr["ProductModel"].ToString()));
                        }
                        //return ds.Tables[0];
                    }
                }
            }

            return list;
        }

        public int GetProdottiPaginatiCount()
        {
            using (SqlConnection connection = new SqlConnection())
            {
                connection.ConnectionString = "Data Source=PC-GIANPAOLO\\SQLGIANPAOLO;Initial Catalog=AdventureWorks;Integrated Security=True";
                using (SqlCommand command = new SqlCommand())
                {
                    command.Connection = connection;
                    command.CommandText = "GetProdottiPaginatiCount";
                    command.CommandType = CommandType.StoredProcedure;

                    SqlParameter paramReturnValue = new SqlParameter();
                    paramReturnValue.ParameterName = "@return_value";
                    paramReturnValue.SqlDbType = SqlDbType.Int;
                    paramReturnValue.Direction = ParameterDirection.ReturnValue;

                    command.Parameters.Add(paramReturnValue);
                    connection.Open();
                    command.ExecuteScalar();
                    return (int)command.Parameters["@return_value"].Value;
                }
            }
        }

    }
}

[/code]

I due metodi di ProdottoDao GetProdottiPaginati e  GetProdottiPaginatiCount Non fanno altro che andare ad invocare due SoterdProcedures:

GetProdottiPaginati sfrutta la nuova funzione ROW_NUMBER presente in SQL Server 2005 che restituisce il numero sequenzioale, a partire da 1, per ogni riga in un set di risultati, per maggiori informazioni potete cosulatare MSDN online, quindi data la nostra query potremo decidere quanti risultati restituire ed a partire da qule numero di riga.
La Stored Procedure è definita come segue:

[code language="SQL"]

USE [AdventureWorks]
GO
/****** Object:  StoredProcedure [dbo].[GetProdottiPaginati]    Script Date: 12/05/2007 17:53:23 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[GetProdottiPaginati]
    -- Parametri della Stored Procedure
    @StartRowIndex    INT,            --la riga iniziale da restituire
    @NumRows        INT,            --il numero di righe compelssivo da restituire
    @sortDirection    INT,            --la direzione di sorting ASC o DESC
    @sortColumn    NVarChar(25)    --il nome della colonna di sorting
AS

DECLARE @Direzione NVarChar(4)
SET @Direzione = CASE @sortDirection
    WHEN -1 THEN 'DESC'
    ELSE 'ASC'
    END
print @Direzione
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    SET NOCOUNT ON;
    WITH    PRODOTTI AS (
            SELECT ROW_NUMBER() OVER
            (ORDER BY
                CASE WHEN @sortColumn = 'idProdotto' AND @sortDirection = -1 THEN p.ProductID END DESC,
                CASE WHEN @sortColumn = 'idProdotto' AND @sortDirection = 1 THEN p.ProductID END ASC,   
                CASE WHEN @sortColumn = 'nomeProdotto' AND @sortDirection = -1 THEN p.Name END DESC,
                CASE WHEN @sortColumn = 'nomeProdotto' AND @sortDirection = 1 THEN p.Name END ASC,
                CASE WHEN @sortColumn = 'descProdotto' AND @sortDirection = -1 THEN pd.Description END DESC,
                CASE WHEN @sortColumn = 'descProdotto' AND @sortDirection = 1 THEN pd.Description END ASC,
                CASE WHEN @sortColumn = 'modelloProdotto' AND @sortDirection = -1 THEN pm.Name END DESC,
                CASE WHEN @sortColumn = 'modelloProdotto' AND @sortDirection = 1 THEN pm.Name END ASC
            )
                      AS Row, p.ProductID, p.Name, pm.Name AS ProductModel, pmx.CultureID, pd.Description
            FROM      Production.Product AS p INNER JOIN
                      Production.ProductModel AS pm ON p.ProductModelID = pm.ProductModelID INNER JOIN
                      Production.ProductModelProductDescriptionCulture AS pmx ON pm.ProductModelID = pmx.ProductModelID INNER JOIN
                      Production.ProductDescription AS pd ON pmx.ProductDescriptionID = pd.ProductDescriptionID
            WHERE      pmx.CultureID = 'en')

    SELECT    ProductID, [Name], ProductModel, [Description]
    FROM    PRODOTTI
    WHERE    (Row BETWEEN @StartRowIndex AND @StartRowIndex + @NumRows)

END

[/code]

La seconda SoredProcedure ci serve a capire il numero totale di record della nostra interrogazione

[code language="SQL"]

USE [AdventureWorks]
GO
/****** Object:  StoredProcedure [dbo].[GetProdottiPaginatiCount]    Script Date: 12/05/2007 17:54:00 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:        <Author,,Name>
-- Create date: <Create Date,,>
-- Description:    <Description,,>
-- =============================================
ALTER PROCEDURE [dbo].[GetProdottiPaginatiCount] 
    -- Add the parameters for the stored procedure here

AS
BEGIN
DECLARE    @Count INT
    SET NOCOUNT ON;
    SELECT    @Count = COUNT(p.ProductID)
    FROM    Production.Product AS p INNER JOIN
            Production.ProductModel AS pm ON p.ProductModelID = pm.ProductModelID INNER JOIN
            Production.ProductModelProductDescriptionCulture AS pmx ON pm.ProductModelID = pmx.ProductModelID INNER JOIN
            Production.ProductDescription AS pd ON pmx.ProductDescriptionID = pd.ProductDescriptionID
    WHERE    pmx.CultureID = 'en'
return @Count
END

[/code]

Per quello che riguarda le Stored Procedures tengo a precisare che non essendo un DBA potrebbero essere ulteriormente ottimizzate.

Ora le nostre logiche sono definite, basterà implementarle in una pagina. Per fare ciò creiamo una  Master Page all'interno della quale definiremo tre Content Place Holder:

[code language="aspx"]

<%@ Master Language="C#" AutoEventWireup="true" CodeFile="MasterPage.master.cs" Inherits="MasterPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" >

<head runat="server">
<title>Untitled Page</title> </head>
<body>
<form id="form1" runat="server">
    <div>
        <asp:contentplaceholder id="ContentPlaceHolder1" runat="server">
        </asp:contentplaceholder>
       
        <asp:ContentPlaceHolder ID="ContentPlaceHolder2" runat="server">
        </asp:ContentPlaceHolder>

        <asp:ContentPlaceHolder ID="ContentPlaceHolder3" runat="server">
        </asp:ContentPlaceHolder>
    </div>
</form>
</body>
</html>

[/code]

Dopodiché creeremo una pagina aspx alla quale legheremo la nostra Master appena creata e all'interno del contentplaceholder1 andremo ad inserire una GridView e un ObjectDataSource

Per l'ObjectDataSource dovremo andare ad impostare a True la proprietà EnablePaging, dopodicchè dirgli quale metodo usare per richiedere i dati e quale per richiedere il numero totale dei record oltre che dirgli qual è il parametro nel quale conservare le informazioni riguardanti l'ordinamento.
Io preferisco lavorare programmaticamente direttamente dal sorgente della pagina aspx anche se quete configurazioni si possono fare dai tool per la configurazione

[code language="aspx"]

    <asp:ObjectDataSource ID="odsProdotti" runat="server" OldValuesParameterFormatString="original_{0}"

        SelectCountMethod="GetProdottiPaginatiCount" SelectMethod="GetProdottiPaginati"

        SortParameterName="sortColumn" TypeName="GPExamples.Oggetti.ProdottiFacade" EnablePaging="True" MaximumRowsParameterName="numRows">

        <SelectParameters>

            <asp:Parameter DefaultValue="0" Name="startRowIndex" Type="Int32" />

            <asp:Parameter DefaultValue="10" Name="numRows" Type="Int32" />

            <asp:Parameter Name="sortColumn" Type="String" />

            <asp:Parameter Name="sortDirection" Type="String" />

        </SelectParameters>

    </asp:ObjectDataSource>

[/code]

Steso discorso vale per la GridView:

[code language="aspx"]

    <asp:GridView ID="gvProdotti" runat="server" AllowPaging="True" AllowSorting="True" DataSourceID="odsProdotti" AutoGenerateColumns="False" OnSorting="gvProdotti_Sorting">

        <Columns>

            <asp:BoundField DataField="idProdotto" HeaderText="idProdotto" SortExpression="idProdotto" />

            <asp:BoundField DataField="nomeProdotto" HeaderText="nomeProdotto" SortExpression="nomeProdotto" />

            <asp:BoundField DataField="descProdotto" HeaderText="descProdotto" SortExpression="descProdotto" />

            <asp:BoundField DataField="modelloProdotto" HeaderText="modelloProdotto" SortExpression="modelloProdotto" />

        </Columns>

    </asp:GridView>

[/code]

Fatto questo ci rimane solo da inventare un meccanisco grazie al quale potremmo persistere I dati riguardanti l'ordinamento che altrimenti andrebbero persi tra un postback e la'ltro.

Sicuramente questo meccanismo verrà riutilizzato in altre pagine ragione per cui all'interno della cartella App_Code creo una classe che chiamerò PageBade che erediterà da System.Web.UI.Page e dalla quale farò ereditare tutte le mie pagine in modo che tutti i metodi e le proprietà esposti da PageBase saranno fruibili da tutte le pagine:

[code language="C#"]

using System;
using System.Collections.Generic;
using System.Text;

/// <summary>
/// Classe base per tutte le pagine del sito. Condivide le funzionalità che
/// saranno usate in utte le pagine. Le pagine aspx al posto di ereditare da System.Web.UI.Page
/// erediteranno da questa classe e questa erediterà da System.Web.UI.Page in modo che tutte le
/// pagine avranno le funzionalità esposte in questa pagina
/// </summary>
public class PageBase : System.Web.UI.Page
{
    public PageBase()
    {
        //
        // TODO: Add constructor logic here
        //
    }

    #region Supporto per l'ordinamento
    /// <summary>
    /// Restituisce o imposta (Gets or sets) la colonna corrente sulla quale si sta facendo l'ordinamento
    /// </summary>
    protected string SortColumn
    {
        get { return ViewState["SortColumn"].ToString(); }
        set { ViewState["SortColumn"] = value; }
    }

    /// <summary>
    /// Restituisce o imposta (Gets or sets) la direzzione di ordinamento corrente (crescente o decrescente)
    /// </summary>
    protected string SortDirection
    {
        get { return ViewState["SortDirection"].ToString(); }
        set { ViewState["SortDirection"] = value; }
    }

    /// <summary>
    /// Restituisce (Gets) la stringa SQL di ordinamento per le impostazioni attuali
    /// </summary>
    //protected string SortExpression
    //{
    //    get { return SortColumn + " " + SortDirection; }
    //}
    #endregion
}

[/code]

Adesso nel code behind della pagina dove abbiamo inserto l'ObjectDataSource e la GridView non ci rimane che scrivere poche linee di codice per far in modo che le informazioni riguardanti l'ordinamento vengano persistite nel ViewState

[code language="C#"]

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

public partial class Paginazione : PageBase
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            // imposta l'ordinamento di default
            SortColumn = "idProdotto";
            SortDirection = "DESC";
            odsProdotti.SelectParameters["sortDirection"].DefaultValue = SortDirection;
            odsProdotti.SelectParameters["sortColumn"].DefaultValue = SortColumn;
        }
    }

    #region Ordinamento
    /// <summary>
    /// Imposta (Set) l'ordinamento e ribinda la pagina
    /// </summary>
    protected void gvProdotti_Sorting(object sender, GridViewSortEventArgs e)
    {
        SortDirection = (SortDirection == "ASC") ? "DESC" : "ASC";
        SortColumn = e.SortExpression;
        odsProdotti.SelectParameters["sortDirection"].DefaultValue = SortDirection;
        odsProdotti.SelectParameters["sortColumn"].DefaultValue = SortColumn;
    }
    #endregion
}

[/code]

 

La soluzione esposta in questo articolo per l'ordinamento lato server sicuramente non è qualcosa di nuovo o rivoluzionario, in rete si trovano decine di esempi del genere, però questa soluzione rappresenta la base dalla quale partire per I prossimi articoli e che in rete e un po più difficile trovare.

Infatti  in un futuro molto prossimo vi dimostrerò come estendere questo esempio inserendo dei criteri di ricerca, come usare gli stessi oggetti per poter fare paginazione su oggetti che non la supportano nativamente, come ad esempio il Datalist, ed infine come esporre e fare ricerche in maniera paginata su  liste tipizzate.

Paginazione Lato Server Parte 2 - Ricerca negli oggetti che supportano la paginazione

Paginazione Lato Server Parte 3 - Oggetti che non supportano la paginazione con ricerca

Paginazione Lato Server Parte 4 - Using Singleton Collections

Only published comments... Dec 05 2007, 01:28 PM by Gian Paolo Santopaolo
dotNet Umbria 2007-2008
Powered by Community Server (Commercial Edition), by Telligent Systems