Author Archives: Robert Haken

avatar Neznámé

About Robert Haken

Software Architect, Founder at HAVIT, Microsoft MVP - ASP.NET/IIS

Control pro zadávání předem neznámého počtu položek

Ukažme si základní schema jednoduchého controlu, který umožní uživateli zadat data předem neurčeného počtu položek – umožní tedy uživateli dynamické přidávání dalších položek – např. řádky objednávky, několik telefonních čísel, několik e-mail e-mailů – v našem případě několik zásilek k přepravě.

 

Netvrdím, že níže uvedený postup je jediný možný a uvítám jakoukoliv diskuzi pod článkem, berte to spíše jako nasměrování pro další snažení…

Neřeším také, jak na control navázat již existující data (možná v příštím článku), a i vytahování dat z controlu není úplně user-friendly. Nicméně cílem je demonstrovat základní schéma dynamického počtu položek a vylepšení kolem už si každý jistě umí udělat.

Základní schema by se dalo shrnout:

  1. Vytvoříme si control představující jednu položku – není to sice nezbytně nutné, ale usnadní to další manipulaci – control samozřejmě může už existovat, pokud se ptáme jen na jména milenek, může nám stačit už TextBox – nicméně většinou budeme potřebovat control, který bude obsahovat nekolik prvků, validátory, DropDownListy naplněné daty, atp. V našem konkrétním případě jsem si teda připravil UserControl Zasilka.ascx, který má za předka třídu Zasilka (code-behind).
  2. Vytvoříme CompositeControl, který bude v CreateChildControls dynamicky přidávat tolik položek, kolik je právě potřeba. Aby nám fungoval postback a viewstate, musí být control-tree vystavěn při každém roundtripu znovu, a to právě ve fázi Init. V CompositeControl je připraveno schéma pomocí metody CreateChildControls(), která je volána právě už ve fázi Init. Zásadním problémem zde je, jak ve fázi Init zjistit, kolik položek máme zrovna vygenerovat, když ještě nemáme načtena post-back data, ani ViewState.
  3. Aktuální počet položek si musíme ukládat tak, abychom ho znali už ve fázi Init, například do hidden-inputu. Lze využít i Session, či jiná uložiště, vlastní hidden-input se však zde nabízí jako ideální.
  4. Přidání další položky realizujeme v event-handleru pomocí Controls.AddAt(), odebírání položky pomocí Controls.RemoveAt(). Klikne-li uživatel na tlačítko „přidat další položku“, dozvíme se o tom v event-handleru až v poměrně pokročilé fázi zpracování postbacku, když už je control-tree vytvořen (jinak by ani event nemohl být korektně obsloužen). V této fázi jsou existující položky i naplněny daty a nebylo by tedy vhodné znovu rebuildovat control-tree, protože bychom tato data ztratili, popř. museli znovu dotahovat. Protože však control-tree díky jeho pevné stavbě známe, nic nám nebrání přidat další položku do existujícího control-tree, popř. položku odebrat.
  5. Data z controlu vytahujeme například přímým přístupem do Controls, jelikož strukturu položek v Controls známe.

Kód takového controlu s dynamickým počtem položky by tedy mohl vypadat nějak následovně:

public class Zasilky : CompositeControl
 {
  // Počet zásilek si mezi postbacky posíláme v input-hidden,
  // z něj ho načítáme přímo pomocí Form, ukládáme až těsně
  // před renderem OnPreRender(), abychom měli poslední stav
  // po případném přidání/odebrání.
  public int PocetZasilek
  {
   get
   {
    if (_pocetZasilek == null)
    {
     _pocetZasilek = 1;
     object tmp = Page.Request.Form[this.ClientID + "_PocetZasilek"];
     if (tmp != null)
     {
      _pocetZasilek = Convert.ToInt32(tmp);
     }
    }
    return (int)_pocetZasilek;
   }
   set
   {
    _pocetZasilek = value;
   }
  }
  private int? _pocetZasilek;
  
  // abychom mohli buttony referencovat (chceme je skrývat),
  // uložíme si odkaz na ně do private fieldu
  private LinkButton pridejZasilkuLB;
  private LinkButton odeberZasilkuLB;
  
  // uloží nám počet položek do input-hidden
  protected override void OnPreRender(EventArgs e)
  {
   Page.ClientScript.RegisterHiddenField(this.ClientID + "_PocetZasilek", _pocetZasilek.ToString());
   
   base.OnPreRender(e);
  }
  
  // klasické schéma CompositeControlu
  protected override void CreateChildControls()
  {
   Controls.Clear();
   CreateControlHiearchy();
   ClearChildViewState();
  }
  
  // budujeme control-tree
  private void CreateControlHiearchy()
  {
   // položky - jsou naschvál na začátku, což nám usnadňuje jejich snadné referencování pomocí Controls[i],
   // jinak bychom museli přístup k nim mít složitější
   for (int i = 0; i < PocetZasilek; i++)
   {
    CreateZasilkaControl(Controls.Count);
   }
  
   Literal lit1 = new Literal();
   lit1.Text = "<tr><td colspan=\"3\" class=\"zindent\">";
   Controls.Add(lit1);
  
   // tlačítko pro přidání položky
   pridejZasilkuLB = new LinkButton();
   pridejZasilkuLB.ID = "PridejZasilkuLB"; // <-- nutno nastavit ID, jinak nám můžou blbnout postbacky
   pridejZasilkuLB.Text = (string)HttpContext.GetGlobalResourceObject("Zasilky", "PridejZasilku");
   pridejZasilkuLB.CssClass = "arrow";
   pridejZasilkuLB.Click += new EventHandler(pridejZasilkuLB_Click);
   pridejZasilkuLB.CausesValidation = false;
   Controls.Add(pridejZasilkuLB);
  
   Literal lit3 = new Literal();
   lit3.Text = "<br/>\n";
   Controls.Add(lit3);
  
   // tlačítko pro odebrání položky
   odeberZasilkuLB = new LinkButton();
   odeberZasilkuLB.ID = "OdeberZasilkuLB"; // <-- nutné nastavit ID, jinak nám můžou blbnout postbacky
   odeberZasilkuLB.Text = (string)HttpContext.GetGlobalResourceObject("Zasilky", "OdeberPosledniZasilku");
   odeberZasilkuLB.CssClass = "arrow";
   odeberZasilkuLB.Click += new EventHandler(odeberZasilkuLB_Click);
   odeberZasilkuLB.CausesValidation = false;
   odeberZasilkuLB.Visible = (PocetZasilek > 1);
   Controls.Add(odeberZasilkuLB);
  
   Literal lit2 = new Literal();
   lit2.Text = "</td></tr>\n";
   Controls.Add(lit2);
  }
  
  // vytvoření jedné položky v control-tree
  private void CreateZasilkaControl(int index)
  {
   Zasilka zasilkaControl = (Zasilka)Page.LoadControl("~/Controls/Zasilka.ascx");
   zasilkaControl.ID = "ZasilkaUC_" + index.ToString(); // <-- nutné nastavit ID kvůli korektním postback
   Controls.AddAt(index, zasilkaControl);
  }
  
  // obsluha události - přidání položky
  private void pridejZasilkuLB_Click(object sender, EventArgs e)
  {
   PocetZasilek++;
   CreateZasilkaControl(PocetZasilek - 1); // přidáme za poslední položku
   odeberZasilkuLB.Visible = true;
  }
  
  private void odeberZasilkuLB_Click(object sender, EventArgs e)
  {
   if (PocetZasilek > 1)
   {
    PocetZasilek--;
    Controls.RemoveAt(PocetZasilek); // odebereme poslední položku
    if (PocetZasilek == 1)
    {
     odeberZasilkuLB.Visible = false;
    }
   }
  }
 }

Vytahování dat z controlu by pak v nejjednoduším případě mohlo vypadat nějak takto:

for (int i=0; i < Zasilky.PocetZasilek; i++)
{
   Zasilka zasilka = (Zasilka)Zasilky.Controls[i];
   uloziste[i] = zasilka.Hmotnost;
}
  
// kdyby byl položkou jen TextBox
for (int i=0; i < Zasilky.PocetZasilek; i++)
{
   TextBox polozka = (TextBox)MyControl.Controls[i];
   uloziste[i] = polozka.Text;
}

Attachment: screenshot.gif

CSS: Vertikální centrování

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
   <title>Vertikální centrování</title>
   <style>
      .greenBorder {border: 1px solid green;} /* nepodstatný styl */
   </style>
</head>
<body>
   <div class="greenBorder" style="display: table; height: 400px; _position: relative; overflow: hidden;">
      <div style=" _position: absolute; _top: 50%;display: table-cell; vertical-align: middle;">
         <div class="greenBorder" style=" _position: relative; _top: -50%">
            libovolný prvek<br>
            libovolné výšky<br>
            a libovolného obsahu<br>
            zůstává vertikálně vystředěný
         </div>
      </div>
   </div>
</body>
</html>

Zdroj: JakPsatWeb.cz
Funguje i s jinými DOCTYPE, vyzkoušeno např. s HTML 4.01 Transitional s loose.dtd.

App_offline.htm – explicitní odstavení webové aplikace

ASP.NET 2.0 má v sobě jednu novou málo známou pomůcku – možnost dočasného odstavení webové aplikace umístěním souboru App_offline.htm do rootu webové aplikace.

Pokud do rootu aplikace umístíme soubor App_offline.htm, webová aplikace se zastaví a dokonce se zruší její aplikační doména (AppDomain), takže se odpojí všechny otevřená spojení, databázové soubory, zruší user-instance SQL, atp.

Případný request dostane v odpovědi obsah App_offline.htm souboru. Můžeme si tak připravit nějaký rozumný soubor a pouhým přejmenováním na App_offline.htm web shazovat.

Funkčnost je zamýšlena pro krátkodobé odstavení webové aplikace za účelem jejího update a využívá jí například i Visual Studio při „Publish Web Site“.

Šifrujeme snadno a rychle – Encrypt(), Decrypt()

Na CodeProject.com jsem pod názvem Simple encrypting and decrypting data in C# našel pěkný ukázkový kód na šifrování. Kód je připraven ve formě utils-třídy se statickými metodami Encrypt() a Decrypt() s několika overloady, je hezky komentován a demonstruje základní schéma pouužití šifrování.

// 
//    This sample code is provided "AS IS" with no warranties,
//    and confers no rights. 
// 
//    ATTENTION: This sample is designed to be more of a
//    tutorial rather than something you can copy and paste in
//    the production code! 
// 




using System; 
using System.IO; 
using System.Security.Cryptography; 

// 
// Sample encrypt/decrypt functions 
// Parameter checks and error handling
// are ommited for better readability 
// 

public class EncDec 
{
    // Encrypt a byte array into a byte array using a key and an IV 
    public static byte[] Encrypt(byte[] clearData, byte[] Key, byte[] IV) 
    { 
        // Create a MemoryStream to accept the encrypted bytes 
        MemoryStream ms = new MemoryStream(); 

        // Create a symmetric algorithm. 
        // We are going to use Rijndael because it is strong and
        // available on all platforms. 
        // You can use other algorithms, to do so substitute the
        // next line with something like 
        //      TripleDES alg = TripleDES.Create(); 
        Rijndael alg = Rijndael.Create(); 

        // Now set the key and the IV. 
        // We need the IV (Initialization Vector) because
        // the algorithm is operating in its default 
        // mode called CBC (Cipher Block Chaining).
        // The IV is XORed with the first block (8 byte) 
        // of the data before it is encrypted, and then each
        // encrypted block is XORed with the 
        // following block of plaintext.
        // This is done to make encryption more secure. 

        // There is also a mode called ECB which does not need an IV,
        // but it is much less secure. 
        alg.Key = Key; 
        alg.IV = IV; 

        // Create a CryptoStream through which we are going to be
        // pumping our data. 
        // CryptoStreamMode.Write means that we are going to be
        // writing data to the stream and the output will be written
        // in the MemoryStream we have provided. 
        CryptoStream cs = new CryptoStream(ms, 
           alg.CreateEncryptor(), CryptoStreamMode.Write); 

        // Write the data and make it do the encryption 
        cs.Write(clearData, 0, clearData.Length); 

        // Close the crypto stream (or do FlushFinalBlock). 
        // This will tell it that we have done our encryption and
        // there is no more data coming in, 
        // and it is now a good time to apply the padding and
        // finalize the encryption process. 
        cs.Close(); 

        // Now get the encrypted data from the MemoryStream.
        // Some people make a mistake of using GetBuffer() here,
        // which is not the right way. 
        byte[] encryptedData = ms.ToArray();

        return encryptedData; 
    } 

    // Encrypt a string into a string using a password 
    //    Uses Encrypt(byte[], byte[], byte[]) 

    public static string Encrypt(string clearText, string Password) 
    { 
        // First we need to turn the input string into a byte array. 
        byte[] clearBytes = 
          System.Text.Encoding.Unicode.GetBytes(clearText); 

        // Then, we need to turn the password into Key and IV 
        // We are using salt to make it harder to guess our key
        // using a dictionary attack - 
        // trying to guess a password by enumerating all possible words. 
        PasswordDeriveBytes pdb = new PasswordDeriveBytes(Password, 
            new byte[] {0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 
            0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76}); 

        // Now get the key/IV and do the encryption using the
        // function that accepts byte arrays. 
        // Using PasswordDeriveBytes object we are first getting
        // 32 bytes for the Key 
        // (the default Rijndael key length is 256bit = 32bytes)
        // and then 16 bytes for the IV. 
        // IV should always be the block size, which is by default
        // 16 bytes (128 bit) for Rijndael. 
        // If you are using DES/TripleDES/RC2 the block size is
        // 8 bytes and so should be the IV size. 
        // You can also read KeySize/BlockSize properties off
        // the algorithm to find out the sizes. 
        byte[] encryptedData = Encrypt(clearBytes, 
                 pdb.GetBytes(32), pdb.GetBytes(16)); 

        // Now we need to turn the resulting byte array into a string. 
        // A common mistake would be to use an Encoding class for that.
        //It does not work because not all byte values can be
        // represented by characters. 
        // We are going to be using Base64 encoding that is designed
        //exactly for what we are trying to do. 
        return Convert.ToBase64String(encryptedData); 

    }
    
    // Encrypt bytes into bytes using a password 
    //    Uses Encrypt(byte[], byte[], byte[]) 

    public static byte[] Encrypt(byte[] clearData, string Password) 
    { 
        // We need to turn the password into Key and IV. 
        // We are using salt to make it harder to guess our key
        // using a dictionary attack - 
        // trying to guess a password by enumerating all possible words. 
        PasswordDeriveBytes pdb = new PasswordDeriveBytes(Password, 
            new byte[] {0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 
            0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76}); 

        // Now get the key/IV and do the encryption using the function
        // that accepts byte arrays. 
        // Using PasswordDeriveBytes object we are first getting
        // 32 bytes for the Key 
        // (the default Rijndael key length is 256bit = 32bytes)
        // and then 16 bytes for the IV. 
        // IV should always be the block size, which is by default
        // 16 bytes (128 bit) for Rijndael. 
        // If you are using DES/TripleDES/RC2 the block size is 8
        // bytes and so should be the IV size. 
        // You can also read KeySize/BlockSize properties off the
        // algorithm to find out the sizes. 
        return Encrypt(clearData, pdb.GetBytes(32), pdb.GetBytes(16)); 

    }

    // Encrypt a file into another file using a password 
    public static void Encrypt(string fileIn, 
                string fileOut, string Password) 
    { 

        // First we are going to open the file streams 
        FileStream fsIn = new FileStream(fileIn, 
            FileMode.Open, FileAccess.Read); 
        FileStream fsOut = new FileStream(fileOut, 
            FileMode.OpenOrCreate, FileAccess.Write); 

        // Then we are going to derive a Key and an IV from the
        // Password and create an algorithm 
        PasswordDeriveBytes pdb = new PasswordDeriveBytes(Password, 
            new byte[] {0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 
            0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76}); 

        Rijndael alg = Rijndael.Create(); 
        alg.Key = pdb.GetBytes(32); 
        alg.IV = pdb.GetBytes(16); 

        // Now create a crypto stream through which we are going
        // to be pumping data. 
        // Our fileOut is going to be receiving the encrypted bytes. 
        CryptoStream cs = new CryptoStream(fsOut, 
            alg.CreateEncryptor(), CryptoStreamMode.Write); 

        // Now will will initialize a buffer and will be processing
        // the input file in chunks. 
        // This is done to avoid reading the whole file (which can
        // be huge) into memory. 
        int bufferLen = 4096; 
        byte[] buffer = new byte[bufferLen]; 
        int bytesRead; 

        do { 
            // read a chunk of data from the input file 
            bytesRead = fsIn.Read(buffer, 0, bufferLen); 

            // encrypt it 
            cs.Write(buffer, 0, bytesRead); 
        } while(bytesRead != 0); 

        // close everything 

        // this will also close the unrelying fsOut stream
        cs.Close(); 
        fsIn.Close();     
    } 

    // Decrypt a byte array into a byte array using a key and an IV 
    public static byte[] Decrypt(byte[] cipherData, 
                                byte[] Key, byte[] IV) 
    { 
        // Create a MemoryStream that is going to accept the
        // decrypted bytes 
        MemoryStream ms = new MemoryStream(); 

        // Create a symmetric algorithm. 
        // We are going to use Rijndael because it is strong and
        // available on all platforms. 
        // You can use other algorithms, to do so substitute the next
        // line with something like 
        //     TripleDES alg = TripleDES.Create(); 
        Rijndael alg = Rijndael.Create(); 

        // Now set the key and the IV. 
        // We need the IV (Initialization Vector) because the algorithm
        // is operating in its default 
        // mode called CBC (Cipher Block Chaining). The IV is XORed with
        // the first block (8 byte) 
        // of the data after it is decrypted, and then each decrypted
        // block is XORed with the previous 
        // cipher block. This is done to make encryption more secure. 
        // There is also a mode called ECB which does not need an IV,
        // but it is much less secure. 
        alg.Key = Key; 
        alg.IV = IV; 

        // Create a CryptoStream through which we are going to be
        // pumping our data. 
        // CryptoStreamMode.Write means that we are going to be
        // writing data to the stream 
        // and the output will be written in the MemoryStream
        // we have provided. 
        CryptoStream cs = new CryptoStream(ms, 
            alg.CreateDecryptor(), CryptoStreamMode.Write); 

        // Write the data and make it do the decryption 
        cs.Write(cipherData, 0, cipherData.Length); 

        // Close the crypto stream (or do FlushFinalBlock). 
        // This will tell it that we have done our decryption
        // and there is no more data coming in, 
        // and it is now a good time to remove the padding
        // and finalize the decryption process. 
        cs.Close(); 

        // Now get the decrypted data from the MemoryStream. 
        // Some people make a mistake of using GetBuffer() here,
        // which is not the right way. 
        byte[] decryptedData = ms.ToArray(); 

        return decryptedData; 
    }

    // Decrypt a string into a string using a password 
    //    Uses Decrypt(byte[], byte[], byte[]) 

    public static string Decrypt(string cipherText, string Password) 
    { 
        // First we need to turn the input string into a byte array. 
        // We presume that Base64 encoding was used 
        byte[] cipherBytes = Convert.FromBase64String(cipherText); 

        // Then, we need to turn the password into Key and IV 
        // We are using salt to make it harder to guess our key
        // using a dictionary attack - 
        // trying to guess a password by enumerating all possible words. 
        PasswordDeriveBytes pdb = new PasswordDeriveBytes(Password, 
            new byte[] {0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 
            0x64, 0x76, 0x65, 0x64, 0x65, 0x76}); 

        // Now get the key/IV and do the decryption using
        // the function that accepts byte arrays. 
        // Using PasswordDeriveBytes object we are first
        // getting 32 bytes for the Key 
        // (the default Rijndael key length is 256bit = 32bytes)
        // and then 16 bytes for the IV. 
        // IV should always be the block size, which is by
        // default 16 bytes (128 bit) for Rijndael. 
        // If you are using DES/TripleDES/RC2 the block size is
        // 8 bytes and so should be the IV size. 
        // You can also read KeySize/BlockSize properties off
        // the algorithm to find out the sizes. 
        byte[] decryptedData = Decrypt(cipherBytes, 
            pdb.GetBytes(32), pdb.GetBytes(16)); 

        // Now we need to turn the resulting byte array into a string. 
        // A common mistake would be to use an Encoding class for that.
        // It does not work 
        // because not all byte values can be represented by characters. 
        // We are going to be using Base64 encoding that is 
        // designed exactly for what we are trying to do. 
        return System.Text.Encoding.Unicode.GetString(decryptedData); 
    }

    // Decrypt bytes into bytes using a password 
    //    Uses Decrypt(byte[], byte[], byte[]) 

    public static byte[] Decrypt(byte[] cipherData, string Password) 
    { 
        // We need to turn the password into Key and IV. 
        // We are using salt to make it harder to guess our key
        // using a dictionary attack - 
        // trying to guess a password by enumerating all possible words. 
        PasswordDeriveBytes pdb = new PasswordDeriveBytes(Password, 
            new byte[] {0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 
            0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76}); 

        // Now get the key/IV and do the Decryption using the 
        //function that accepts byte arrays. 
        // Using PasswordDeriveBytes object we are first getting
        // 32 bytes for the Key 
        // (the default Rijndael key length is 256bit = 32bytes)
        // and then 16 bytes for the IV. 
        // IV should always be the block size, which is by default
        // 16 bytes (128 bit) for Rijndael. 
        // If you are using DES/TripleDES/RC2 the block size is
        // 8 bytes and so should be the IV size. 

        // You can also read KeySize/BlockSize properties off the
        // algorithm to find out the sizes. 
        return Decrypt(cipherData, pdb.GetBytes(32), pdb.GetBytes(16)); 
    }

    // Decrypt a file into another file using a password 
    public static void Decrypt(string fileIn, 
                string fileOut, string Password) 
    { 
    
        // First we are going to open the file streams 
        FileStream fsIn = new FileStream(fileIn,
                    FileMode.Open, FileAccess.Read); 
        FileStream fsOut = new FileStream(fileOut,
                    FileMode.OpenOrCreate, FileAccess.Write); 
  
        // Then we are going to derive a Key and an IV from
        // the Password and create an algorithm 
        PasswordDeriveBytes pdb = new PasswordDeriveBytes(Password, 
            new byte[] {0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 
            0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76}); 
        Rijndael alg = Rijndael.Create(); 

        alg.Key = pdb.GetBytes(32); 
        alg.IV = pdb.GetBytes(16); 

        // Now create a crypto stream through which we are going
        // to be pumping data. 
        // Our fileOut is going to be receiving the Decrypted bytes. 
        CryptoStream cs = new CryptoStream(fsOut, 
            alg.CreateDecryptor(), CryptoStreamMode.Write); 
  
        // Now will will initialize a buffer and will be 
        // processing the input file in chunks. 
        // This is done to avoid reading the whole file (which can be
        // huge) into memory. 
        int bufferLen = 4096; 
        byte[] buffer = new byte[bufferLen]; 
        int bytesRead; 

        do { 
            // read a chunk of data from the input file 
            bytesRead = fsIn.Read(buffer, 0, bufferLen); 

            // Decrypt it 
            cs.Write(buffer, 0, bytesRead); 

        } while(bytesRead != 0); 

        // close everything 
        cs.Close(); // this will also close the unrelying fsOut stream 
        fsIn.Close();     
    }
 }

Jak umoudřit eventy po ENTER ve formuláři

Pokud na straně klienta odešleme formulář klávesou ENTER, pak máme-li na stránce jediný submit-prvek (tlačítko) a k tomu jediný input-prvek (TextBox), pak díky chování mnohých prohlížečů (včetně Internet Exploreru), nedojde k vyvolání serverové události Click submit-prvku, nýbrž proběhne jen hluchý postback.

Ošálit to lze přidáním dalšího skrytého TextBoxu:

<asp:TextBox ID="EmailTB" Runat="server"/>
<asp:TextBox ID="dummy" Style="display:none;" Runat="server"/>
<asp:LinkButton ID="SubscribeLB" Text="Přihlásit" Runat="server"/>

Konverze SqlDataReaderu na DataSet

Ač to není příliš šťastná situace, může se nám někdy přihodit, že potřebujeme konvertovat SqlDataReader na DataSet.
V .NET Frameworku 1.1 jsou v podstatě dvě základní cesty – buď to udělat ručně, prvek po prvku, nebo si uvědomit, že DataAdapter dělá v podstatě totéž, a už by to tedy mohlo být někde řešeno (v .NET Frameworku 2.0 již je tato funkčnost exponováno prostřednictvím metody DataTable.Load(), viz níže).
Ručně to vypadá např. takto (kód není můj, tak to berte s rezervou):

public static DataSet DataReaderToDataSet( SqlDataReader rd )
{
    DataSet ds = new DataSet();
    do
    {
       DataTable st = rd.GetSchemaTable();
       DataTable Dt = new DataTable();
 
       if (st != null)
       {
          for (int i = 0 ; i < st.Rows.Count ; i++)
          {
             DataRow dr = st.Rows[i];
             string columnName = (string)dr["ColumnName"];
             DataColumn column = new DataColumn(columnName, (Type)dr["DataType"]);
             dt.Columns.Add(column);
          }
 
          ds.Tables.Add(dt);
 
          while (rd.Read())
          {
             DataRow dr = dt.NewRow();
 
             for (int i = 0; i < rd.FieldCount; i++)
                dr[i] = rd.GetValue(i);
 
             dt.Rows.Add(dr);
          }
       }
       else
       {
          DataColumn column = new DataColumn("RowsAffected");
          dt.Columns.Add(column);
          ds.Tables.Add(dt);
          DataRow dr = dt.NewRow();
          dr[0] = rd.RecordsAffected;
          dt.Rows.Add(dr);
       }
    }
    while (rd.NextResult());
 
    return ds;
}

Mnohem lepší fígl přes DataAdapter spočívá ve zjištění, že třída DbDataAdapter, z které jsou odvozeny všechny specifické DataAdaptery obsahuje metodu protected Fill(DataTable, IDataReader).

Můžeme tedy vytvořit vlastní DataAdapter odvozený od DbDataAdapteru, který tuto metodu použije ke zbudování příslušné tabulky:

public class DataReaderAdapter : DbDataAdapter 
{ 
   public int FillFromReader(DataTable dataTable, IDataReader dataReader) 
   { 
      return this.Fill(dataTable, dataReader); 
   } 
   protected override RowUpdatedEventArgs CreateRowUpdatedEvent( 
      DataRow dataRow, 
      IDbCommand command, 
      StatementType statementType, 
      DataTableMapping tableMapping)
   {
      return null;
   } 
   protected override RowUpdatingEventArgs CreateRowUpdatingEvent( 
      DataRow dataRow, 
      IDbCommand command, 
      StatementType statementType, 
      DataTableMapping tableMapping)
   {
      return null;
   } 
   protected override void OnRowUpdated( 
      RowUpdatedEventArgs value)
   {
   } 
   protected override void OnRowUpdating( 
      RowUpdatingEventArgs value)
   {
   } 
}

…a dát DataTable do DataSetu už není problém, případně escalovat SqlDataReader na další rowset a vytáhnout další tabulku. Ostatně DbDataAdapter obsahuje i mnoho další overloadů metody Fill() a můžeme si tak udělat mnohem chytřejší DataReaderAdapter.

Update pro .NET Framework 2.0

.NET Framework 2.0 již tuto problematiku řeší metodou DataTable.Load();

T-SQL: Výběr náhodného záznamu z tabulky

Celkem jednoduchý fígl pro výběr náhodného záznamu z tabulky

SELECT TOP 1 Sloupec
   FROM Tabulka
   ORDER BY NEWID()

Generátor NEWID() nám dává přiměřenou náhodnost. Funkci RAND() nelze použít, protože její opakované volání dává stejné hodnoty, např.

SELECT RAND(100), RAND(), RAND()
SELECT RAND(100), RAND(), RAND()

…oba řádky dají stejné hodnoty.

Nevalně bychom dopadli i s použitím GETDATE().

CREATE .. EXTERNAL NAME – Incorrect syntax

Pekelně dlouho jsem se kdysi namordoval s hláškou „Incorrect syntax near ‚[Jmeno.Assembly]‘.“, kterou mi SQL2005 server hlásil na příkaz

CREATE TYPE dbo.JmenoTypu
EXTERNAL NAME [Jmeno.Assembly].[Namespace.Namespace.Type]

Protože jsem to tehdy dělal poprvé, zkoušel jsem snad všechny možné i nemožné podoby, abych opravil syntaxi.

Problém byl úplně jiný! Databáze, na které jsem to zkoušel, byla přenesena z SQL2000 serveru a v byla v módu SQL2000.

Stačí pomocí Management Studia přepnout databázi do SQL2005 módu a vše funguje (Database – Properties ~ Options).

Když jsem to zjistil, zuřil jsem ještě víc, protože nejenomže je ta hláška dost zavádějící, ale navíc mi předtím server klidně povolil CREATE ASSEMBLY a dokonce bylo v Management Studiu assembly i vidět!

Souborový přístup ke složkám nadřazeným/sousedním webové aplikaci

Dejme tomu, že jsme v hostovaném prostředí a víme, že je na disku zhruba následující adresářová struktura:

<nevíme>/Zakaznik/
<nevíme>/Zakaznik/wwwroot/
<nevíme>/Zakaznik/data/

…a potřebujeme se z webové aplikace dostat na soubory ve složce ../data/.

Na problém narazíme, pokud bychom použili

 Server.MapPath("../data/cosi.xyz");
Server.MapPath("/../data/cosi.xyz");
Server.MapPath("~/../data/cosi.xyz");
 

…dostaneme výjimku HttpException: Cannot use a leading .. to exit above the top directory.

Pomoc je snadná, pokud máme na cílové místo opravdu přístupová práva, pak stačí použít:

 Path.Combine(Server.MapPath("/"), "../data/cosi.xyz")

…a jsme tam.

Lokalizace snadno a rychle – explicitní lokalizace

Předesílám, že se v tomto článku budu zabývat výhradně explicitní lokalizací webových projektů, tedy přímým odkazováním na resources pomocí <%$ Resources: … %>, popř. metod GetLocalResourceObject() nebo GetGlobalResourceObject(). Osobně mám tuto metodu radši, protože mám přesně pod explicitní kontrolou každý bajt, který chci lokalizovat.

Co všechno je tedy pro úspěšnou implementaci lokalizace udělat?

  1. Vytvořit resources – .resx soubory s lokalizovanými texty, popř. i jinými objekty.
  2. Přidat do stránek/kódu odkazy na resources tam, kde chceme lokalizaci.
  3. Vytvoření lokalizovaných verzí .resx souborů.
  4. Zajistit nastavení a případně i přepínání CurrentUICulture, popř. i CurrentCulture.

Vytvoření primárních resources – .resx souborů

Předpokládám běžnou práci ve Visual Studiu nebo Web Developer Express, nebudu se tedy zabývat takovými věcmi jako je resgen. VS/WDE přímo podporují přehledné vytváření a editaci .resx souborů.

Důležité je, že rozlišujeme dva typy resources:

  1. globální resources
    • data v nich jsou přístupná ze všech míst webové aplikace,
    • jsou umístěny ve složce ~/App_GlobalResources
    • pojmenovávají se MojeJmeno.resx, např. ~/App_GlobalResources/Glossary.resx
    • může jich být libovolné množství, nicméně je rozumné používat jen několik logických celků, např. Navigation.resx, Glossary.resx, atp.
  2. lokální resources
    1. data v nich jsou přímo přístupná jen ze stránky/controlu/…, které se týkají,
    2. umísťují se do složky App_LocalResources, která je podsložkou sloužky, kde je stránka/control,
    3. pojmenovávají se MojeStranka.aspx.resx, popř. MujControl.ascx.resx, atp.; například pro stránku ~/admin/stranka.aspx bude lokální resource file ~/admin/App_LocalResources/stranka.aspx.resx

Resource-file vytvoříme snadno, ve VS/WDE prostě vytvoříme příslušnou složku a do ní přes Add přidáme položku typu Resource. Resource soubor .resx je ve skutečnosti XML souborem, který je přibuildován do assembly, pozadí však nechme stranou.

Po otevření .resx souboru se nám standardně zobrazí grid pro editaci/přidávání objektů typu string. Resource nemusí být jen typu string, ale například i obrázky, ikony, zvuky, soubory nebo obecné objekty, nicméně pro běžnou lokalizaci si naprosto vystačíme s typem String, protože ve webových aplikací i u obrázků a ostatních objektů obvykle „lokalizujeme“ jen odkaz na příslušný soubor na disku (ImageUrl = „vlajka_cz.gif“) spíše než samotné soubory.

V praxi .resx záznamy v .resx souborech vznikají současně s prvním odkazem na ně, přistupme tedy k použití ve stránce/kódu.

Použití resources ve stránce/kódu

Explicitní použití resource ve stránce je primitivní, v tagu controlu, všude, kde lze přiřadit string, stačí zapsat odkaz

  • <%$ Resources: ResourceName %> pro lokální resources
  • <%$ Resources: FileNameBezPripony, ResourceName %> pro globální resources
  • Například tedy
    &lt;asp:Localize Text="&lt;%$ Resources: UvodniText %&gt;" ... runat="server" /&gt;
    &lt;asp:Label Text="&lt;%$ Resources: Glossary, Telefon %&gt;" ... runat="server" /&gt;
    &lt;asp:Image ImageUrl="&lt;%$ Resources: Images, Vlajka %&gt;" ... runat="server" /&gt;
    

    Pozor, že nejde použít <%$ … %> mimo serverové tagy, pro takový účel slouží právě nový control Localize, který není ničím jiným než Literalem s lepší design-time podporou.

    Pro použití resources v kódu je několik možností

    1. Globální resources se zrcadlí do strong-typed properties ve třídách namespace Resources. Například Resources.Glossary.Telefon nebo Resources.Images.Vlajka. Tento wrapper generuje ASP.NET v rámci web-site a je tak přístupný pouze v rámci web-site a nelze na něj odkazovat například z jiných assembly.
    2. Pro obecný přístup ke globálním a lokálním resources slouží metody třídy HttpContext
    public static object GetGlobalResourceObject(string classKey, string resourceKey);
    public static object GetGlobalResourceObject(string classKey, string resourceKey, CultureInfo culture);
    public static object GetLocalResourceObject(string virtualPath, string resourceKey);
    public static object GetLocalResourceObject(string virtualPath, string resourceKey, CultureInfo culture);
    // typicky třeba maily
    mail.Subject = (string)HttpContext.GetGlobalResourceObject("MailTemplates", "MyMailSubject");
    // nebo ve stránce
    MessageLb.Text = (string)GetLocalResourceObject("Neuspech");
    

    Metody jsou mimo třídy HttpContext přístupné i v třídě TemplateControl a jejích potomcích, tedy např. i Page.
    Pro local resources bohužel ASP.NET žádný wrapper negeneruje a z kódu na ně přistupujeme  rovnou přes GetLocalResourceObject().

    Pro přístup k resources můžeme použít samozřejmě i standardní techniky přes ResourceManager atp., ale ve webových aplikacích to není běžné.

    Co jsme dosud udělali nám samo o sobě už bude fungovat a zobrazovat, nicméně stále jsme jen u jednoho primárního jazyka. Pro přidání dalších jazyků a případně i možnost jejich přepínání musíme provést ještě dva kroky.

    Vytvoření resources (.resx souborů) pro další jazyky

    Vytvoření resources pro další jazyky je velmi primitivní záležitostí. Stačí vzít .resx soubor primárního jazyka a vytvořit jeho kopii se jménem Soubor.jazyk.resx, například tedy ze souboru ~/App_GlobalResources/Glossary.resx vytvoříme kopii ~/App_GlobalResources/Glossary.en.resx (pro angličtinu), nebo ze souboru ~/App_LocalResources/Login.aspx.resx vytvoříme kopii ~/App_LocalResources/Login.aspx.de.resx (pro němčinu).

    …a nyní nezbývá než hodnoty v novém souboru přeložit. Pokud navíc nainstalujeme překladateli WDE a naučíme ho editovat přímo .resx soubory, krása. Existují dokonce mnohé šikovné utilitky pro práci s .resx soubory, aby nebylo nutné instalovat něco tak velkého jako WDE:

    Volba jazyka a přepínání jazyků

    Jakou jazykovou verzi odešle ASP.NET na klienta se řídí hodnotou Thread.CurrentThread.CurrentUICulture, zároveň se doporučuje volit i Thread.CurrentThread.CurrentCulture, protože tím se řídí formát čísel, času, dat, měny, atp.

    Automatická volba dle požadavku prohlížeče

    První, co můžeme chtít, je, aby ASP.NET použilo jazykový požadavek prohlížeče, který ho posílá v HTTP requestu. Česká prostředí tedy rovnou uvidí české stránky, anglická anglické, atp. a nic nemusíme ani přepínat. Prohlížeče mají jazyk přednastaven od instalace a například v Internet Exploreru je možné ho měnit přes Nástroje ~ Možnosti Internetu ~ Obecné ~ Jazyky.

    Jediné, co pro implementaci tohoto postupu potřebujeme udělat, je ve web.config nastavit hodnoty elementu <globalization>, atributy uiCulture, popř. culture:

    &lt;globalization
       requestEncoding="utf-8"
       responseEncoding="utf-8"
       culture="auto:cs-CZ"
       uiCulture="auto:cs-CZ"
    /&gt;
    

    Volba auto říká, že se má použít požadavek prohlížeče, za dvojtečku lze volitelně umístit primární nastavení, které se má použít, pokud není požadavek prohlížeče realizovatelný, resp. pokud prohlížeč požadavek neudal. Pro úplnost uvádím i volby encoding, ty však nejsou předmětem tohoto článku.

    Explicitní volba jazyka uživatelem

    Dále můžeme chtít, aby si uživatel sám mohl přepnou jazyk, nezávisle na požadavku prohlížeče. Je jasné, že budeme muset přepnout Thread.CurrentThread.CurrentUICulture popř. i CurrentCulture. Zvolený jazyk si budeme ukládat třeba do cookie (použitelná je i session) a je potřeba ho přepnout pro každý request, a to co nejdříve. Buď můžeme udělat override prázdné virtuální metody Page.InitializeCulture(), pokud máme připravenou bázovou třídu pro všechny stránky, anebo změnu provádět rovnou v události Application_BeginRequest, v Global.asax.cs:

    private void Application_BeginRequest()
    {
       HttpCookie cookie = Request.Cookies["Culture"];
       if (cookie != null)
       {
          CultureInfo culture = new CultureInfo(cookie.Value);
          Thread.CurrentThread.CurrentCulture = culture;
          Thread.CurrentThread.CurrentUICulture = culture;
       }
    }
    

    a pro vlastní přepínání si uděláme třeba stránku Language.aspx:

    protected override void OnInit(EventArgs e)
    {
       string culture = Request.QueryString["culture"];
       if ((culture != null) &amp;&amp; (culture.Length &gt; 0))
       {
          HttpCookie cookie = new HttpCookie("Culture");
          cookie.Value = culture;
          Response.Cookies.Add(cookie);
       }
       string returnUrl = Request.QueryString["returnUrl"];
       if ((returnUrl != null) &amp;&amp; (returnUrl.Length &gt; 0))
       {
          Response.Redirect(returnUrl);
       }
       else
       {
          Response.Redirect("~/");
       }
       base.OnInit(e);
    }
    

    odkaz pro přepnutí stránky, pak může být třeba

    &lt;a href="language.aspx?culture=en-US"&gt;en&lt;/a&gt;
    

    nebo s ReturnUrl stejně jako login-page.

    Záznam z cvičné přednášky na toto téma

    V příloze najdete PowerPoint slides: Lokalizace webových aplikací.ppt