Author Archives: Robert Haken

avatar Neznámé

About Robert Haken

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

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

Pozicování obrázkových bulletů – ul, li

S pozicování obrázkových bulettů použitých jako list-style-image je děsnej opruz, každý prohlížeč to interpretuje jinak a spolehlivě to snad ani nejde.

Je mnohem jednodušší dát list-style-type:none a obrázkové bulettky udělat jako správně pozicované pozadí li:

ul { list-style-type: none; }
li
{
   background-image: url(...);
   background-repeat: no-repeat;
   background-position: 0 5px;
   padding-left: 10px;
}
 

ExternalException: A generic error occurred in GDI+. při Image.Save()

Tuto krásnou výjimku dostaneme, pokud chceme Image ukládat do stejného souboru, z kterého byl načten. GDI+ to prostě nedovoluje.

Dá se to ale obejít například zkopírováním obrázku do nové instance:

Bitmap bitmap;
using (Image image = Image.FromFile(sourceFilename))
 {
   // načteme si obrázek do bitmapy, abychom mohli zavřít soubor
   bitmap = new Bitmap(image);
}
...
bitmap.Save(...); // tady už to nevadí, přetrhli jsme vazbu na soubor

Jak přistupovat na HttpContext.Current v novém threadu

Když si v ASP.NET spustíme nový thread (vlákno), tak tento sice získá některé vlastnosti z původního threadu (např. culture), ale nemá přístup k HttpContext.Current, resp. je v něm null.

V každém případě musíme vědět, co děláme, protože druhé vlákno například nemůže moc manipulovat s Response, neboť není vůbec jisté, do jakého stavu Response uvedl thread první, např. už totiž mohl Response odeslat na klienta.

Společné uložiště threadů – statická property

První možností je připravit hodnoty, které bude nový thread potřebovat, do uložiště, které je přístupné oboum threadům, klasicky třeba statické property nějaké třídy.

public static string RootUrl
{
   get
   {
      if (rootUrl == null)
      {
         if (HttpContext.Current != null)
         {
            rootUrl = "http://" + HttpContext.Current.Request.Url.Host + HttpContext.Current.Request.ApplicationPath;
         }
         else
         {
            rootUrl = "http://nejaky/default/url";
         }
      }
      return rootUrl;
   }
}
private static string rootUrl;

Výše uvedený příklad statické property zahrnuje i určitou logiku. Rozhodující je však uložení výsledku prvního volání do statické privátní proměnné rootUrl. Pokud si tedy před spuštěním nového threadu zaručíme alespoň jedno čtení dané property, ta si inicializuje svojí hodnotu v prvním threadu a v druhém threadu už dá rovnou výsledek připravený v rootUrl (nebude HttpContext.Current potřebovat).

Teoreticky lze do statické property typu HttpContext cachovat celý HttpContext.Current, nepovažuji to však za příliš vhodné.

Předání contextu v instanci třídy zapouzdřující thread

Druhou možností, která je vhodnější, pokud už potřebujeme předat do druhého threadu opravdu celý context, je zapouzdření druhého threadu do samostatné třídy:

public class MyClass 
{ 
   private HttpContext _context; 
   public MyClass(HttpContext context)
   {
      _context = context;
   } 
   public void Start() 
   { 
      ThreadStart start = new ThreadStart(DoWork); 
      Thread.Start(start); 
   } 
   private void DoWork() 
   { 
      // tady můžem něco dělat z contextem,
      // ale pozor, že request už může být odeslán na klienta
   } 
} 

private void Page_Load(object sender, EventArgs e) 
{ 
   MyClass myClass = new MyClass(HttpContext.Current); 
   myClass.Start(); 
}
HttpContext.Current.Cache

Pokud potřebujeme jen HttpContext.Current.Cache, tak je vše výše uvedené nesmysl, protože na cache máme přístupovat přes HttpRuntime.Cache, a to funguje i v novým threadu…

SQL: Stránkování záznamů pomocí ROW_NUMBER()

Pokud potřebujeme stránkovat záznamy na straně SQL, můžeme to udělat pomocí jedné z nových funkcí SQL2005:

USE AdventureWorks
WITH Rows AS
(
   SELECT *, ROW_NUMBER() OVER(ORDER BY ProductID) AS RowNumber FROM Production.Product
)
SELECT * FROM Rows WHERE RowNumber BETWEEN 101 AND 200

Bohužel se mi to nepodařilo přímo bez CTE v prvním WHERE, tyhle nové funkce jdou zřejmě jenom jako sloupce nebo v ORDER BY.
Další funkce ze skupiny ranking jsou RANK(), DENSE_RANK(), ROW_NUMBER() a NTILE().

Komu se nelíbí Common Table Expression (CTE) s WITH, tak to samozřejmě jde i jako vnořený SELECT:

SELECT  Description, Date
   FROM (SELECT  ROW_NUMBER() OVER (ORDER BY Date DESC) AS Row,
            Description, Date FROM LOG) AS LogWithRowNumbers
   WHERE  Row >= 1 AND Row <= 10

Počet znaků ntext položky – DATALENGTH()

Na pole referenčních typů ntext, text, apod. nejde použít řetězcovou funkci LEN(expr), funguje však

 DATALENGTH(expr)

které vrátí velikost v bytech (u Unicode položek je to dvojnásobek počtu znaků).

Mimochodem DATALENGTH() se dá použít i jako takové SQL String.IsNullOrEmpty():

WHERE DATALENGTH(MyColumn) = 0