in

dotNet Umbria

Il primo User Group in Umbria sul mondo .Net

Articoli

Articoli vari degli iscritti a DotNetUmbria

December 2007 - Posts

  • Un metodo per generare le chiavi di licenze delle nostre applicazioni.

    Premessina

    Come programmatori dotnet, ci troviamo a volte ad affrontare il problema di proteggere le nostre applicazioni da occhi indiscreti che potrebbero curiosare troppo nel nostro codice.
    Il problema che mi sono trovato ad affrontare più volte è quello di proteggere il codice che gestisce la licenza che concedo per le mie applicazioni. Una soluzione potrebbe essere quella di offuscare il codice, oppure potrei appoggiarmi a codice unmanaged, tuttavia sono soluzioni che non mi hanno mai attirato più di tanto e che di fatto non garantiscono un elevato grado di protezione a sguardi esperti.

    La soluzione che mi è venuta in mente è ben diversa dalle soluzioni che "nascondono" il codice per fare sicurezza, al contrario la mia soluzione prevede assoluta trasparenza. Ed è anche il motivo per cui scrivo questo post. Spero che possiate aiutarmi a trovare le falle e rendere ancora più sicuro il tutto.


    Come fare (la teoria)

    Visto che nascondere il codice o criptare licenza e altro è una strada che ho scartato, la cosa più naturare è scrivere il codice di licenza, con tutte le informazioni che sono ad esso legate (come ad esempio:data di attivazione, data di scadenza, numero di utenti, licenziatario, productkey e altro), all interno di un file xml nel path di installazione. (Evitiamo allo smanettone di turno la fatica di doversi cercare il file di licenza)
    Lo scriviamo anche il chiaro in modo da poterlo consultare tranquillamente senza dover ricorrere a tools particolari.
    Inseriamo all interno del file stesso un tag che identifica in modo univoco la macchina per la quale la licenza è stata rilasciata (vedremo in seguito come ottenere queste informazioni)

    Nella nostra applicazione quindi quello che dobbiamo fare è leggere il file di licenza, fare le verifiche del caso per accertarci che il productkey sia valido e che le informazioni all interno della licenza siano corrette, verificare il codice che lega la licenza alla macchina ed eseguire la nostra applicazione.

    Rimane un problema: evitare che qualcuno modifichi a piacimento il file di licenza.

    La soluzione viene quasi da sola. E sufficiente firmare il file XML con una chiave RSA. Chiunque modificherà il file di licenza, corromperà la firma e il file risulterà non più valido.
    Abbiamo sempre a disposizione una chiave RSA che viene gia distribuita all interno dei nostri assembly: la stessa chiave che utilizzamo per firmare i nostri assembly.
    Viene generata da VisualStudio, e ne viene distribuita solo la parte pubblica insieme alla nostra applicazione, la parte privata rimane di nostra esclusiva proprietà.

    Quindi riassumendo quello che dobbiamo fare è:

    1. Creare un file di licenza in fomato XML
    2. Generare un file snk (che contiene sia chiave pubblica che privata)
    3. Firmare i nostri assembly con il file snk
    4. Firmare il file di licenza con il file snk
    5. Distribuire applicazione e file di licenza
    6. Prima di eseguire l applicazione verificare che la firma del file di licenza sia ancora valida.

     

    Come fare (la pratica)

     

    Per prima cosa ci serve una struttura per scrivere il file di licenza, quindi creiamo una classe serializzabile per contenere tutte quest informazioni.

    [code language="c#"]

    [Serializable]
      internal class BaseLicense
      {
        private string _name;
        private object _value;

        public object Value
        {
          get { return _value; }
          set { _value = value; }
        }

        public virtual string ObjectName
        {
          get { return _name; }
          set { _name = value; }
        }
      }

      [Serializable]
      internal class PCInfo : BaseLicense
      {
        private string _signature;

        public string PCSignature
        {
          get { return _signature; }
          set { _signature = value; }
        }
      }

      [Serializable]
      internal class LicenseArchive
      {
        private Dictionary<string, BaseLicense> _licenses = new Dictionary<string, BaseLicense>();
        private byte[] _signature = new byte[0];

        public byte[] Signature
        {
          get { return _signature; }
          set { _signature = value; }
        }

        public Dictionary<string, BaseLicense> Licenses
        {
          get { return _licenses; }
          set { _licenses = value; }
        }
      }

    [/code]

     

    La classe LicenseArchive è il nostro file di licenza, contiene un dictionary e una proprietà pubblica "Signature" che conterrà la firma del file xml stesso. Il dictionary esposto con la proprietà Licenses conterrà tutte le informazioni della licenza.
    La classe LicenseBase è un contenitore che contiene una coppia di informazioni "Chiave-Valore" della licenza. La classe PCInfo arricchisce queste informazioni con una stringa che contiene l identificativo del PC per il quale è stata rilasciata la licenza.
    Vediamo come leggere le informazioni per identificare in modo univoco il PC.

    Possiamo utilizzare WMI (Windows Management Instrumentation) per ottenere le informazioni che ci servono.

    [code language="c#"]

    internal static class MachineInfo
    {

        /// <summary>
        /// return Volume Serial Number from hard drive
        /// </summary>
        /// <param name="strDriveLetter">[optional] Drive letter</param>
        /// <returns>[string] VolumeSerialNumber</returns>
        public static string GetVolumeSerial(string strDriveLetter)
        {
           if (strDriveLetter == "" || strDriveLetter == null) strDriveLetter = "C";
           ManagementObject disk =
               new ManagementObject("win32_logicaldisk.deviceid=\"" + strDriveLetter + ":\"");
           disk.Get();
           return disk["VolumeSerialNumber"].ToString();
        }

        /// <summary>
        /// Returns MAC Address from first Network Card in Computer
        /// </summary>
        /// <returns>[string] MAC Address</returns>
        public static string GetMACAddress()
        {
           ManagementClass mc = new ManagementClass("Win32_NetworkAdapterConfiguration");
           ManagementObjectCollection moc = mc.GetInstances();
           string MACAddress = String.Empty;
           foreach (ManagementObject mo in moc)
           {
              if (MACAddress == String.Empty)  // only return MAC Address from first card
              {
                 if ((bool)mo["IPEnabled"] == true) MACAddress = mo["MacAddress"].ToString();
              }
              mo.Dispose();
           }
           MACAddress = MACAddress.Replace(":", "");
           return MACAddress;
        }


        /// <summary>
        /// Return processorId from first CPU in machine
        /// </summary>
        /// <returns>[string] ProcessorId</returns>
        public static string GetCPUId()
        {
           string cpuInfo = String.Empty;
           string temp = String.Empty;
           ManagementClass mc = new ManagementClass("Win32_Processor");
           ManagementObjectCollection moc = mc.GetInstances();
           foreach (ManagementObject mo in moc)
           {
              if (cpuInfo == String.Empty)
              {// only return cpuInfo from first CPU
                 cpuInfo = mo.Properties["ProcessorId"].Value.ToString();
              }
           }
           return cpuInfo;
        }
    }

    [/code]

    Ci siamo, abbiamo tutto il necessario per generare il nostro file di licenza.

    Vediamo cosa ci serve per firmarlo.
    Ho trovato questa classettina che ci permette di ottenere le chiavi RSA da un file snk e dalla parte dell snk che si trova all interno dell assembly.

     

    [code language="C#"]

    ///
    /// One static method to get an RSACryptoServiceProvider from a *.snk file.
    /// NOTE: These methods assume 1024 bit keys, the same as exported from sn.exe.
    /// See also: CryptExportKey() Win32 API.
    ///
    public sealed class SnkUtil
    {
      private const int magic_priv_idx = 0x08;
      private const int magic_pub_idx = 0x14;
      private const int magic_size = 4;

      private SnkUtil()
      {
      }

      ///
      /// Returns RSA object from *.snk key file.
      ///
      /// Path to snk file.
      /// RSACryptoServiceProvider
      public static RSACryptoServiceProvider GetRSAFromSnkFile(string path)
      {
        if (path == null)
          throw new ArgumentNullException("path");

        byte[] snkBytes = GetFileBytes(path);
        if (snkBytes == null)
          throw new Exception("Invalid SNK file.");

        RSACryptoServiceProvider rsa = GetRSAFromSnkBytes(snkBytes);
        return rsa;
      }

      private static byte[] GetFileBytes(string path)
      {
        using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
        using (BinaryReader br = new BinaryReader(fs))
        {
          byte[] bytes = br.ReadBytes((int)fs.Length);
          return bytes;
        }
      }

      public static RSACryptoServiceProvider GetRSAFromSnkBytes(byte[] snkBytes)
      {
        if (snkBytes == null)
          throw new ArgumentNullException("snkBytes");

        RSAParameters param = FigureParams(snkBytes);

        // Must set KeyNumber to AT_SIGNATURE for strong
        // name keypair to be correctly imported.
        CspParameters cp = new CspParameters();
        cp.KeyNumber = 2; // AT_SIGNATURE

        RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(1024, cp);
        rsa.ImportParameters(param);
        return rsa;
      }

      private static byte[] BlockCopy(byte[] source, int idx, int size)
      {
        if ((source == null) || (source.Length < (idx + size)))
          return null;

        byte[] ret = new byte[size];
        Buffer.BlockCopy(source, idx, ret, 0, size);
        return ret;
      }

      ///
      /// Returns true if buffer length is public key size.
      ///
      ///
      ///
      private static bool SnkBufIsPubLength(byte[] keypair)
      {
        if (keypair == null)
          return false;
        return (keypair.Length == 160);
      }

      ///
      /// Check that RSA1 is in header (public key only).
      ///
      ///
      ///
      private static bool CheckRSA1(byte[] pubkey)
      {
        // Check that RSA1 is in header.
        // R S A 1
        byte[] check = new byte[] { 0x52, 0x53, 0x41, 0x31 };
        return CheckMagic(pubkey, check, magic_pub_idx);
      }

      ///
      /// Check that RSA2 is in header (public and private key).
      ///
      ///
      ///
      private static bool CheckRSA2(byte[] pubkey)
      {
        // Check that RSA2 is in header.
        // R S A 2
        byte[] check = new byte[] { 0x52, 0x53, 0x41, 0x32 };
        return CheckMagic(pubkey, check, magic_priv_idx);
      }

      private static bool CheckMagic(byte[] keypair, byte[] check, int idx)
      {
        byte[] magic = BlockCopy(keypair, idx, magic_size);
        if (magic == null)
          return false;

        for (int i = 0; i < magic_size; i++)
        {
          if (checkIdea != magicIdea)
            return false;
        }

        return true;
      }

      ///
      /// Returns RSAParameters from byte[].
      ///
      ///
      ///
      private static RSAParameters FigureParams(byte[] keypair)
      {
        RSAParameters ret = new RSAParameters();

        if ((keypair == null) || (keypair.Length < 1))
          return ret;

        bool pubonly = SnkBufIsPubLength(keypair);

        if ((pubonly) && (!CheckRSA1(keypair)))
          return ret;

        if ((!pubonly) && (!CheckRSA2(keypair)))
          return ret;

        int magic_idx = pubonly ? magic_pub_idx : magic_priv_idx;

        // Bitlen is stored here, but note this
        // class is only set up for 1024 bit length keys
        int bitlen_idx = magic_idx + magic_size;
        int bitlen_size = 4; // DWORD

        // Exponent
        // In read file, will usually be { 1, 0, 1, 0 } or 65537
        int exp_idx = bitlen_idx + bitlen_size;
        int exp_size = 4;

        //BYTE modulus[rsapubkey.bitlen/8]; == MOD; Size 128
        int mod_idx = exp_idx + exp_size;
        int mod_size = 128;

        //BYTE prime1[rsapubkey.bitlen/16]; == P; Size 64
        int p_idx = mod_idx + mod_size;
        int p_size = 64;

        //BYTE prime2[rsapubkey.bitlen/16]; == Q; Size 64
        int q_idx = p_idx + p_size;
        int q_size = 64;

        //BYTE exponent1[rsapubkey.bitlen/16]; == DP; Size 64
        int dp_idx = q_idx + q_size;
        int dp_size = 64;

        //BYTE exponent2[rsapubkey.bitlen/16]; == DQ; Size 64
        int dq_idx = dp_idx + dp_size;
        int dq_size = 64;

        //BYTE coefficient[rsapubkey.bitlen/16]; == InverseQ; Size 64
        int invq_idx = dq_idx + dq_size;
        int invq_size = 64;

        //BYTE privateExponent[rsapubkey.bitlen/8]; == D; Size 128
        int d_idx = invq_idx + invq_size;
        int d_size = 128;

        // Figure public params
        // Must reverse order (little vs. big endian issue)
        ret.Exponent = BlockCopy(keypair, exp_idx, exp_size);
        Array.Reverse(ret.Exponent);
        ret.Modulus = BlockCopy(keypair, mod_idx, mod_size);
        Array.Reverse(ret.Modulus);

        if (pubonly) return ret;

        // Figure private params
        // Must reverse order (little vs. big endian issue)
        ret.P = BlockCopy(keypair, p_idx, p_size);
        Array.Reverse(ret.P);

        ret.Q = BlockCopy(keypair, q_idx, q_size);
        Array.Reverse(ret.Q);

        ret.DP = BlockCopy(keypair, dp_idx, dp_size);
        Array.Reverse(ret.DP);

        ret.DQ = BlockCopy(keypair, dq_idx, dq_size);
        Array.Reverse(ret.DQ);

        ret.InverseQ = BlockCopy(keypair, invq_idx, invq_size);
        Array.Reverse(ret.InverseQ);

        ret.D = BlockCopy(keypair, d_idx, d_size);
        Array.Reverse(ret.D);

        return ret;
      }
    }

    [/code]

     

    Quando avremo ottenuto le chiavi RSA dovremo semplicemente passarle ad un Crypto service provider per fare la firma.
    Daremo in pasto al metodo SignData di RSACryptoServiceProvider i dati xml da firmare.
    In questo caso utilizzeremo il file snk che abbiamo generato per firmare il nostro assembly.

    [code language="C#"]

       RSACryptoServiceProvider rsa = SnkUtil.GetRSAFromSnkFile(snkfilepath);
       byte[] signature;
       signature = rsa.SignData(datatoSign, new SHA1CryptoServiceProvider());

    [/code]

    Per verificare la firma abbiamo solo bisogno della chiave pubblica e non dell intero snk. possiamo ottenere la chiave pubblica direttamente dall assembly.

    [code language="C#"]

    byte[] publickey = Assembly.GetExecutingAssembly().GetName().GetPublicKey();
          if (publickey == null || publickey.Length == 0)
            throw new LicenseException(null, null, "This assembly cannot be licensed because is not strongly named");

          RSACryptoServiceProvider rsa = SnkUtil.GetRSAFromSnkBytes(publickey);
          return rsa.VerifyData(datatoverify, new SHA1CryptoServiceProvider(), signature);

    [/code]

    Questo è tutto. Vi riporto per intero anche le classi che ho scritto per serializzare  e firmare la licenza ( da utilizzare nel nostro license manager), e la classe LicenseReader che inseriremo all interno del nostro client.

    [code language="c#"]

    internal class LicenseReader
    {
       private LicenseArchive _licenses = new LicenseArchive();

       protected LicenseArchive LicensesArchive
       {
         get { return _licenses; }
       }

       public LicenseReader()
       {
         this.LoadLicenses();
       }

       private bool VerifyObjectSign(byte[] datatoverify, byte[] signature)
       {
         byte[] publickey = Assembly.GetExecutingAssembly().GetName().GetPublicKey();
         if (publickey == null || publickey.Length == 0)
           throw new LicenseException(null, null, "This assembly cannot be licensed because is not strongly named");

         RSACryptoServiceProvider rsa = SnkUtil.GetRSAFromSnkBytes(publickey);
         return rsa.VerifyData(datatoverify, new SHA1CryptoServiceProvider(), signature);
       }

       protected void LoadLicenses()
       {
         BinaryFormatter bf = new BinaryFormatter();

         // Get license file path
         string licensepath = Application.StartupPath + @"\" + "TeamDev.Licenses.lic";

         //Search for License File
         if (!File.Exists(licensepath)) return;
         FileStream fs = File.OpenRead(licensepath);

         // Deserialize the license file
         _licenses = (LicenseArchive)bf.Deserialize(fs);
         fs.Close();

         // serialize in a bytestream the Licenses for validation purposes
         MemoryStream ms = new MemoryStream();
         bf.Serialize(ms, _licenses.Licenses);
         byte[] buffer = new byte[ms.Length];
         ms.Position = 0;
         ms.Read(buffer, 0, (int)ms.Length);

         // Check the PC Signature & LicenseFile Signature.
         try
         {
           string pcinfo = string.Empty;

           try { pcinfo += MachineInfo.GetCPUId(); }
           catch { }
           try { pcinfo += MachineInfo.GetVolumeSerial("C"); }
           catch { }

           // Il PCInfo non può essere vuoto.
           if (pcinfo == string.Empty)
             throw new Exception("Invalid License File");

           if ((((PCInfo)_licenses.Licenses["PCINFO"]).PCSignature != pcinfo) ||
              (!VerifyObjectSign(buffer, _licenses.Signature)))
           {
             _licenses.Licenses.Clear();
             _licenses.Signature = new byte[0];

             throw new Exception("Invalid License File");
           }
         }
         catch (Exception ex)
         {
           throw new Exception("Invalid License File", ex);
         }
       }
    }



     

    internal class LicenseProvider : LicenseReader
      {
        private string _snkFilePath = string.Empty;

        public LicenseArchive Archive
        {
          get { return base.LicensesArchive; }
        }

        public string SnkFilePath
        {
          get { return _snkFilePath; }
          set { _snkFilePath = value; }
        }

        // Override Default Constructor because i don't like to load license
        // at startup.
        public LicenseProvider()
        {
          this.ClearLicenses();
        }

        public void ClearLicenses()
        {
          base.LicensesArchive.Licenses.Clear();
          base.LicensesArchive.Signature = new byte[0];
        }

        public void SaveLicenseFile(string filename)
        {
          BinaryFormatter bf = new BinaryFormatter();

          MemoryStream ms = new MemoryStream();
          bf.Serialize(ms, base.LicensesArchive.Licenses);
          byte[] buffer = new byte[ms.Length];
          ms.Position = 0;
          ms.Read(buffer, 0, (int)ms.Length);

          byte[] signature = CreateSignature(buffer,this.SnkFilePath);
          base.LicensesArchive.Signature = signature;

          FileStream fs = File.Create(filename);
          bf.Serialize(fs, base.LicensesArchive);
          fs.Flush();
          fs.Close();
        }

        private byte[] CreateSignature(byte[] datatoSign, string snkfilepath)
        {
          if(!File.Exists(snkfilepath))
            throw new FileNotFoundException("Key Archive not found");

          RSACryptoServiceProvider rsa = SnkUtil.GetRSAFromSnkFile(snkfilepath);
          byte[] signature;
          signature = rsa.SignData(datatoSign, new SHA1CryptoServiceProvider());
          return signature;
        }

        public RSACryptoServiceProvider CreateNewRSAKey()
        {
          RSACryptoServiceProvider rsaprov = new RSACryptoServiceProvider(1024);
          return rsaprov;
        }

        public RSAParameters GetPublicKey(RSACryptoServiceProvider rsaprovider)
        {
          return rsaprovider.ExportParameters(false);
        }

        public RSAParameters GetPrivateKey(RSACryptoServiceProvider rsaprovider)
        {
          return rsaprovider.ExportParameters(true);
        }
      }


    [/code]

    Conclusioni

    Abbiamo ottenuto un gestore di file di licenza per le nostre applicazioni che non necessita di "nascondere" il codice che gestisce il meccanismo di verifica del file di licenza stesso. E quindi non dobbiamo preoccuparci di offuscare nulla.

    Aspetto numerosi vostri commenti.

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

    Il seguente articolo si pone come obbiettivo la riutilizzazione delle logiche implementate nei due articoli precedenti (Parte 1 - Oggetti che supportano la paginazioneParte 2 - Ricerca negli oggetti che supportano la paginazione) per fare in modo che esse siano fruite anche da quegli oggetti che non supportano nativamente la paginazione.

    Un esempio classico di applicazione delle suddette logiche potrebbe essere l'uso del Data List, per presentare liste con un accattivante aspetto grafico in quanto altamente personalizzabile dal punto di vista estetico. Pensiamo ad esempio alla nostra lista di prodotti: quale miglior oggetto se non il Data List per presentare i prodotti al pubblico, magari correlati da foto?
    Lo scopo di questo articolo non è, sia chiaro, il layout grafico. Come si sarà notato anche negli articoli precedenti non ho implementato alcuna forma di layout legato ai CSS o agli Skin. Ritengo sia più opportuno lasciare questa parte ai grafici.

    Come al solito potete scaricare il progetto aggiornato direttamente da qui GPExamples3.zip

    Per iniziare facciamo una copia di PaginazioneGridView.aspx denominandola PaginazioneDatalist.aspx; nel codice elimineremo solo la parte riguardante la GridView e la sostituiremo con un oggetto Datalist in modo che il risultato finale sia il seguente: 

    [code language="ASPX"]

    <%@ Page Language="C#" AutoEventWireup="true" CodeFile="PaginazioneDatalist.aspx.cs" Inherits="PaginazioneDatalist" %>

    <!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>
            <table width="100%">
                <tr>
                    <td style="height: 26px">
                        <asp:Label ID="lblidProdotto" runat="server" Text="ID Prodotto:"></asp:Label>
                    </td>
                    <td style="height: 26px">
                        <asp:TextBox ID="txtidProdotto" runat="server"></asp:TextBox>&nbsp;
                    </td>
                    <td style="height: 26px">
                        <asp:Label ID="lblnomeProdotto" runat="server" Text="Nome Prodotto:"></asp:Label>
                    </td>
                    <td style="height: 26px">
                        <asp:TextBox ID="txtnomeProdotto" runat="server"></asp:TextBox>
                    </td>
                    <td style="height: 26px">
                        <asp:Label ID="lbldescProdotto" runat="server" Text="Descrizione Prodotto:"></asp:Label>
                    </td>
                    <td style="height: 26px">
                        <asp:TextBox ID="txtdescProdotto" runat="server"></asp:TextBox>
                    </td>
                    <td style="height: 26px">
                        <asp:Label ID="lblmodelloProdotto" runat="server" Text="Modello Prodotto:"></asp:Label>
                    </td>
                    <td style="height: 26px">
                        <asp:TextBox ID="txtmodelloProdotto" runat="server"></asp:TextBox>
                    </td>
                </tr>
                <tr>
                    <td colspan="8">
                        <asp:Button ID="btnRicerca" runat="server" Text="Ricerca" OnClick="btnRicerca_Click" />
                    </td>
                </tr>
            </table>
            <asp:DataList ID="dlProdotti" runat="server" DataSourceID="odsProdotti" GridLines="Both" RepeatColumns="3" RepeatDirection="Horizontal">
                <ItemTemplate>
                    <div>
                        <%# Eval("idProdotto")%>   
                    </div>
                    <div>
                        <%# Eval("nomeProdotto")%>   
                    </div>
                    <div>
                        <%# Eval("descProdotto")%>   
                    </div>
                    <div>
                        <%# Eval("modelloProdotto")%>   
                    </div>
                </ItemTemplate>
            </asp:DataList>
            <asp:Button ID="btnAvanti" runat="server" Text="Avanti" OnClick="btnAvanti_Click" />
            <asp:Button ID="btnIndietro" runat="server" Text="Indietro" OnClick="btnIndietro_Click" />
        </div>
            <asp:ObjectDataSource ID="odsProdotti" runat="server" OldValuesParameterFormatString="original_{0}"
            SelectCountMethod="GetProdottiPaginatiCount" SelectMethod="GetProdottiPaginati"
            SortParameterName="sortColumn" TypeName="GPExamples.Oggetti.ProdottiFacade" EnablePaging="True" MaximumRowsParameterName="numRows" OnSelecting="odsProdotti_Selecting">
                <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" />
                    <asp:Parameter Name="sortExpression" Type="String" />
                </SelectParameters>
        </asp:ObjectDataSource>
        </form>
    </body>
    </html>

    [/code]

    Passiamo ora ad occuparci del code behind.
    La sostanziale differenza di questo tratto è che dovremmo occuparci noi di mandare le "pagine" avanti e indietro a seconda della richiesta dell'utente.
    Noterete che per fare ciò non abbiamo implementato i metodi nella PageBase, in quanto si ipotizza che in un contesto reale la paginazione sia un qualsiasi tipo di controllo dunque, posto nella pagina, dovrebbe essere trattato come un mero oggetto.

    Quello che dobbiamo fare è semplicemente inventare un meccanismo per persistere i valori riguardanti la pagina attuale e il numero di righe per ogni pagina.
    Per fare ciò inseriamo nel View State due variabili in modo da poterne persistere i valori durante i postback. Questi due valori verranno modificati dai bottoni "avanti" e "indietro"; inoltre nell'evento Selecting dell'Object Data Source faremo in modo che l'oggetto in questione ricevi i valori del View State in modo che possa passarli ai metodi, che successivamente gli restituiranno i nuovi valori richiesti.

    Una volta ultimate le modifiche il code behind risulterà così:

     

    [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 PaginazioneDatalist : PageBase
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                SortColumn = "idProdotto";
                SortDirection = "ASC";
                ViewState.Add("MaximumRows", 6);
                ViewState.Add("StartRowIndex", 0);
            }
        }

        protected void ricerca()
        {
            string idProdotto = this.txtidProdotto.Text.Trim();
            string nomeProdotto = this.txtnomeProdotto.Text.Trim();
            string descProdotto = this.txtdescProdotto.Text.Trim();
            string modelloProdotto = this.txtmodelloProdotto.Text.Trim();

            odsProdotti.SelectParameters["sortExpression"].DefaultValue = idProdotto + ";" + nomeProdotto + ";" +
                descProdotto + ";" + modelloProdotto;
        }

        protected void ResettaPaginazione()
        {
            SortDirection = "ASC";
            ViewState["MaximumRows"] = 6;
            ViewState["StartRowIndex"] = 0;
        }

        protected void odsProdotti_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
        {
            e.Arguments.MaximumRows = (int)ViewState["MaximumRows"];
            e.Arguments.StartRowIndex = (int)ViewState["StartRowIndex"];
        }

        protected void btnAvanti_Click(object sender, EventArgs e)
        {
            ViewState["StartRowIndex"] = (int)ViewState["StartRowIndex"] + (int)ViewState["MaximumRows"];
            dlProdotti.DataBind();
        }

        protected void btnIndietro_Click(object sender, EventArgs e)
        {
            if ((int)ViewState["StartRowIndex"] <= 0)
            { }
            else
            {
                ViewState["StartRowIndex"] = (int)ViewState["StartRowIndex"] - (int)ViewState["MaximumRows"];
                dlProdotti.DataBind();
            }
        }

        protected void btnRicerca_Click(object sender, EventArgs e)
        {
            ricerca();
            ResettaPaginazione();
        }
    }

    [/code]

    Et voilà il gioco è fatto!

    Paginazione Lato Server Parte 1 - Oggetti che supportano la paginazione

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

    Paginazione Lato Server Parte 4 - Using Singleton Collections

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

    Prendendo come punto di partenza l'esempio citato nel primo articolo, Paginazione Lato Server Parte 1 - Oggetti che supportano la paginazione implementeremo in questa sede delle logiche per la ricerca.

    Come al solito potete scaricare i sorgenti dell'esempio GPExamples2.zip

    Inizieremo con l'inserimento, in entrambre le Stored Procedures, dei seguenti parametri:

    [code language="SQL"]
    @idProdotto            INT,
    @nomeProdotto        NVarChar(25),
    @descProdotto        NVarChar(50),
    @modelloProdotto    NVarChar(25)

    [/code]

    e delle condizioni WHERE:

    [code language="SQL"]
    WHERE      pmx.CultureID = 'en'
              AND ((@idProdotto = 0) OR (p.ProductID = @idProdotto))
              AND ((@nomeProdotto= '' ) OR (p.Name LIKE '%' + @nomeProdotto + '%'))
              AND ((@descProdotto= '' ) OR (pd.Description LIKE '%' + @descProdotto + '%'))
              AND ((@modelloProdotto= '' ) OR (pm.Name LIKE '%' + @modelloProdotto + '%'))

    [/code]


    Terminata questo processo si deve fare in modo che la nostra architettura comunichi questi parametri alle Stored Procedures.

    Bisognerà dunque procedere inizialmente con la creazione di un nuovo tipo ParametriProdotti, in modo da poter passare tra i metodi un oggetto tipizzato, il quale sarà dotato della logica necessaria affichè le proprietà di questo oggetto rispettino la definizione dei paramtri che le Stored Procedures accettano. ParametriProdotti.cs avrà dunque questa definizione:

    [code language="C#"]
    using System;
    using System.Data;
    using System.Configuration;

    namespace GPExamples.Oggetti.Utilities
    {
        /// <summary>
        /// Summary description for ParametriProdotti
        /// </summary>
        [Serializable]
        public class ParametriProdotti
        {
            #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
                {
                    if (value.Length > 25)
                    {
                        _nomeProdotto = value.Substring(0, 24);
                    }
                    else
                    {
                        _nomeProdotto = value;
                    }
                }
            }

            private string _descProdotto;
            /// <summary>
            /// Restituisce o imposta (Gets or sets) la descrizione del Prodotto.
            /// </summary>
            public string descProdotto
            {
                get { return _descProdotto; }
                set
                {
                    if (value.Length > 50)
                    {
                        _descProdotto = value.Substring(0, 49);
                    }
                    else
                    {
                        _descProdotto = value;
                    }
                }
            }

            private string _modelloProdotto;
            /// <summary>
            /// Restituisce o imposta (Gets or sets) il modello del Prodotto.
            /// </summary>
            public string modelloProdotto
            {
                get { return _modelloProdotto; }
                set
                {
                    if (value.Length > 25)
                    {
                        _modelloProdotto = value.Substring(0, 24);
                    }
                    else
                    {
                        _modelloProdotto = value;
                    }
                }
            }
            #endregion

            #region Costruttori
            /// <summary>
            /// Costruttore di default per Prodotto
            /// </summary>
            public ParametriProdotti()
            {
                //
                // 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 ParametriProdotti(int idProdotto, string nomeProdotto, string descProdotto, string modelloProdotto)
            {
                this._idProdotto = idProdotto;
                this._nomeProdotto = nomeProdotto;
                this._descProdotto = descProdotto;
                this._modelloProdotto = modelloProdotto;
            }
            #endregion
        }
    }

    [/code]

    In tutti i 'setter' di questo tipo controlleremo che i valori siano coerenti con quelli che la stored procedure si aspetta. Nell'esempio che abbiamo proposto il tipo Prodotto e ParametriProdotti sono molto simili, bisogna tener presente ad ogni modo che in un contesto reale i tipi potrebbero essere sostanzialmente diversi.

    Passiamo ora all'analisi del nostro oggeto di accesso ai dati ProdottiDao. Ai metodi esposti aggiungeremo un altro parametro di tipo ParametriProdotti e faremo in modo che i valori contenuti in questo tipo vengano passati alle Stored Procedures. ProdottiDao, una volta ultimate le modifiche, risulterà 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;
    using GPExamples.Oggetti.Utilities;

    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, ParametriProdotti param)
            {
                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 SqlParameterMusic;
                        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;
                        sqlParams[4] = new SqlParameter("@idProdotto", SqlDbType.Int, 0);
                        sqlParams[4].Value = param.idProdotto;
                        sqlParams[5] = new SqlParameter("@nomeProdotto", SqlDbType.NVarChar, 25);
                        sqlParams[5].Value = param.nomeProdotto;
                        sqlParamsDevil = new SqlParameter("@descProdotto", SqlDbType.NVarChar, 50);
                        sqlParamsDevil.Value = param.descProdotto;
                        sqlParams[7] = new SqlParameter("@modelloProdotto", SqlDbType.NVarChar, 25);
                        sqlParams[7].Value = param.modelloProdotto;

                        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(ParametriProdotti param)
            {
                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";
                        SqlParameter[] sqlParams = new SqlParameter[4];
                        sqlParams[0] = new SqlParameter("@idProdotto", SqlDbType.Int, 0);
                        sqlParams[0].Value = param.idProdotto;
                        sqlParams[1] = new SqlParameter("@nomeProdotto", SqlDbType.NVarChar, 25);
                        sqlParams[1].Value = param.nomeProdotto;
                        sqlParams[2] = new SqlParameter("@descProdotto", SqlDbType.NVarChar, 50);
                        sqlParams[2].Value = param.descProdotto;
                        sqlParams[3] = new SqlParameter("@modelloProdotto", SqlDbType.NVarChar, 25);
                        sqlParams[3].Value = param.modelloProdotto;

                        command.CommandType = CommandType.StoredProcedure;

                        foreach (SqlParameter sqlParameter in sqlParams)
                        {
                            command.Parameters.Add(sqlParameter);
                        }
                        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]

    Ai metodi esposti da ProdottiFacade ritengo che non si possa aggiugere un parametro di tipo ParametriProdotti, infatti, sebbene non abbia fatto un elevato numero di ricerche in tal senso, l'Oblect Data Source riesce a passare solo tipi 'base' e quindi interi, stringhe etc. Dunque, dal momento che non possiamo passare un oggetto tipizzato, passeremo una stringa che conterrà i valori a noi necessari sotto forma di valori concatenati separati dal punto e virgola.
    Infine, per creare il nostro oggetto tipizzato da passare all'oggeto per l'accesso ai dati, useremo un metodo privato.

    Ultimate le modifiche ProdottiFacade.cs risultera come segue:

    [code language="C#"]
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.ComponentModel;
    using GPExamples.DB;
    using GPExamples.Oggetti.Utilities;

    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, string sortExpression)
            {
                int sortParam = sortDirection == "ASC" ? 1 : -1;

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

                return prodottiDao.GetProdottiPaginati(startRowIndex, numRows, sortColumn, sortParam, GeneraParametriProdotto(sortBLOCKED EXPRESSION;
            }

            /// <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, string sortExpression)
            {
                return prodottiDao.GetProdottiPaginatiCount(GeneraParametriProdotto(sortBLOCKED EXPRESSION;
            }

            private static ParametriProdotti GeneraParametriProdotto(string sortExpression)
            {
                ParametriProdotti param = new ParametriProdotti();
                //ricavo i singoli valori della sortExpression
                if (string.IsNullOrEmpty(sortBLOCKED EXPRESSION
                {
                    param = new ParametriProdotti(0, "", "", "");
                }
                else
                {
                    string[] parametri = sortExpression.Split(';');
                    //idProdotto
                    //nomeProdotto
                    //descProdotto
                    //modelloProdotto
                    try
                    {
                        if (string.IsNullOrEmpty(parametri[0]))
                        {
                            param.idProdotto = 0;
                        }
                        else
                        {
                            param.idProdotto = Convert.ToInt32(parametri[0]);
                        }
                    }
                    catch
                    {
                        param.idProdotto = 0;
                    }

                    try
                    {
                        param.nomeProdotto = parametri[1];
                    }
                    catch
                    {
                        param.nomeProdotto = "";
                    }

                    try
                    {
                        param.descProdotto = parametri[2];
                    }
                    catch
                    {
                        param.descProdotto = "";
                    }

                    try
                    {
                        param.modelloProdotto = parametri[3];
                    }
                    catch
                    {
                        param.modelloProdotto = "";
                    }
                }
                return param;

            }

        }
    }

    [/code]

    Bene, ci siamo quasi.
    Bisogna solo modificare la UI ed il gioco è fatto: all'Ollobject Data Source aggiungeremo un parametro di tipo stringa sortExpression,  una tabella che conterrà i nostri campi in cui l'utente potrà inserire i propri criteri di ricerca e infine un button.

    Nel code behind della pagina bisogna solo inserire il codice per gestire l'evento click del bottone ricerca, che non farà altro che andare a valorizzare il parametro sortExpression dell'Object Datata Souce con i valori che avrà inserito l'utente:

    [code language="C#"]
    protected void btnRicerca_Click(object sender, EventArgs e)
    {
        ricerca();
    }

    protected void ricerca()
    {
        gvProdotti.PageIndex = 0;
        string idProdotto = this.txtidProdotto.Text.Trim();
        string nomeProdotto = this.txtnomeProdotto.Text.Trim();
        string descProdotto = this.txtdescProdotto.Text.Trim();
        string modelloProdotto = this.txtmodelloProdotto.Text.Trim();

        odsProdotti.SelectParameters["sortExpression"].DefaultValue = idProdotto + ";" + nomeProdotto + ";" +
            descProdotto + ";" + modelloProdotto;
    }

    [/code]


    L'argomento del prossimo articolo riguarderà la riutilizzazione del codice già scritto per paginare e ricercare usando un oggetto, il DataList, che non supporta nativamente la paginazione.

    Paginazione Lato Server Parte 1 - 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

  • 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];
                  &n