Podivné chování session, přegenerovávání SessionID

V aplikaci je přihlašovací dialog. Zde by měla aplikace vytvořit session a poslat její identifikátor klientovi, což se stane.

Po přihlášení se zobrazí stránka obsahující frames, každý frame (a iframe) však znovu dostává jiný identifikátor session. To je špatné, protože každému frame je potom poslána jiný identifikátor session a tudíž má jeden uživatel více session.

Problém nenastane, pokud do přihlašovacího dialogu umístím kód:

Session["some_key"] = "some_value";
Session.Clear();

Trochu mě překvapuje, že session Session.Clear() je možné provést a je možné mít tedy prázdnou session. ASP.NET tedy pravděpodobně ruší session, pokud s ní nebylo pracováno (nebylo k ní přistoupeno).

Celý problém jsem vyřešil potomkem HttpApplication (Global.asax):

private void Application_PreRequestHandlerExecute(object sender, EventArgs e)
{
    // tento kod opravuje podivne chovani Session.
    if (Context.Session != null)
    {
        Context.Session["__Application_PreRequestHandlerExecute"] = "some_value";
        Context.Session.Remove("__Application_PreRequestHandlerExecute");
    }
}

IIS6: Volba výchozích regionálních nastavení pro ASP stránky

Můžeme se dočkat nepříjemností, pokud v ASP stránkách spoléháme na konkrétní regional-settings. Přesuneme-li aplikaci na server jiné lokalizace, nepomůže nám totiž ani nastavení Control Panel ~ Regional Settings (Ovládací panely ~ Regionální nastavení).

Je v podstatě několik možností, jak správný region vnutit, nicméně na IIS5.1+ mi jako nejlepší přijdezměna hodnoty AspLCID v IIS metabázi:

\\LM\W3SVC\AspLCID

Pro české regionální nastavení je správná hodnota 1029, hodnoty ostatních uvádí Microsoftí List of Locale ID (LCID).

Související zdroje informací:

Propojení databázového uživatele na login (sp_change_users_login)

Při přesunech databází mezi servery, obnovování ze záloh a podobných úkonech se nám může stát, že se ztratí propojení mezi databázovým uživatelem (User) a jeho loginem (SQL Server login). Pomocí běžných management-nástrojů pak nelze toto propojení obnovit.

Propojení obnovíme pomocí stored procedury sp_change_users_login:

USE mydb
  
-- Auto_Fix, pokud mají user i login stejné jméno, pokud login není, bude vytvořen
-- můžeme přidat i parametr @Password, který se použije, pokud bude login zakládán nově
EXEC sp_change_users_login @Action='Auto_Fix', @UserNamePattern='user'
  
-- Update_One použijeme, pokud se nám jména neshodují
EXEC sp_change_users_login @Action='Update_One', @UserNamePattern='user', @LoginName='username'

…tuto metodu nelze použít pro Windows-loginy, pouze pro SQL Server loginy.

msdtc -resetlog řeší 99% problémů s Distributed Transaction Coordinatorem (MS DTC)

Pokud nejde spustit služba Distributed Transaction Coordinator (MS DTC), bez které ostatně nefunguje skoro nic pořádně, pak mi osobně v 99% případech pomůže jednoduchý fígl z příkazové řádky

msdtc -resetlog

…jak je již zřejmé, vyresetuje se tím log MS DTC (což mj. zlikviduje všechny pending transakce). Problém je obvykle vyřešen.

Ostatně jedna z nepříjemných hlášek IIS „Server Application Error“, kdy je v event logu cosi ve smyslu „The server failed to load application ‚/LM/W3SVC/1/ROOT‘. The error was ‚Class not registered‘.“ se řeší právě takto.

adprep na Windows 2003 Server R2

Ve všech postupech Microsoftu je krásně popsáno, jak se má použít utilita adprep pro přípravu Active Directory na Windows 2000 Serveru pro použití s Windows 2003 Serverem (ať už před upgrade nebo před prostým přidáním dalšího DC).

Všechny návody však jaksi pomíjí, že na disku 2 Windows 2003 Serveru R2 je nová verze této utility a pokud použijeme tu z disku 1, o které všechny návody píšou, tak DC nepřidáme…

Intel Matrix Storage Manager instalace: File copying was not successful

Při snaze instalovat Intel Matrix Storage Manager (6.0 i 5.7) anglickou verzi na Windows 2003 Server R2 anglický s českým regionálním nastavením a českým nastavením „Language for non-Unicode programs“ instalátor vždy skončil na hlášce

File copying was not successful.

FileMonem jsem zjistil, že se instalátor pokouší číst soubory, které v instalační složce nejsou, přičemž problém bude zřejmě s jazykovou mutací. Zkusil jsem tedy stáhnout a instalovat z Multi-language instalátoru a vše šlo – až na to, že mi nutil češtinu.

Nakonec jsem český MSM odinstaloval, přepnul volbu „Language for non-Unicode programs“ na English a pak už fungoval i anglický instalátor.

Několik ValidationGroup a jedno post-back tlačítko

Občas se nám může objevit potřeba, kdy potřebujeme formulář rozdělit na několik zón (ValidationGroup) a validaci v nich řídit samostatně, například podle toho, jako zónu si uživatel vybral radio-buttony.

ASP.NET bohužel ve verzi 1.1 nějaké podrobnější řízení validace nepodporuje, ve verzi 2.0 už dává alespoň validation groups (kterých využijeme), nicméně stále jejich použití váže na samostatnou inicializaci post-backu (pro každou validation group se předpokládá samostatné odesílací tlačítko).

Pokud potřebujeme formulář s jediným tlačítkem, který bude validovat na základě nějakého přepínače, který obsahuje (typicky na základě radio-buttonů nebo i drop-down-listu), pak nastávají potíže.

Validace na straně serveru

Pokud nám postačuje validace na straně serveru, je situace poměrně jednoduchá, protože využijeme metody Page.Validate(vaidationGroup) a následně ověřujeme už jenom Page.IsValid.

Validátory si rozdělíme na několik skupin

  1. ty, které chceme vyhodnocovat vždy, těm vlastnost ValidationGroup nenastavíme,
  2. ty, které chceme vyhodnocovat na základě přepínače, ty umístíme do příslušné skupiny ValidationGroup

Post-backovému tlačítku pak ValidationGroup nenastavíme, čímž dosáhneme toho, že i na klientu se nám budou vyhodnocovat alespoň ty společné validátory, které nemají nastaveno ValidationGroup.

V obsluze události na straně serveru pak postupujeme zhruba takto:

private void Tlacitko_Click(object sender, EventArgs e)
{
   // validátory bez ValidationGroup jsou vyhodnoceny
   // stačí se tedy ptát Page.IsValid
   if (IsValid)
   {
      if (PrepinacJednaRB.Checked)
      {
         // radio-buttonový přepínač je nastaven na zónu 1
         // vyvoláme si tedy vyhodnocení validátorů zóny 1
         Validate("ValidationGroupJedna");
         if (IsValid)
         {
            ...
            // obsluha zóny 1
         }
         else
         {
            return;
         }
      }
      else if (PrepinacDvaRB.Checked)
      {
         // přepínač je nastaven na zónu 2
         Validate("ValidationGroupDva");
         if (IsValid)
         {
            ...
            // obsluha zóny 2
         }
         else
         {
            return;
         }
      }
  
      // tady můžeme obsloužit controly společné pro všechny zóny
      // je splněna obecná validace i prošla validace zvolené zóny
      // takže třeba
      result.Save();
   }
}

Ještě je třeba doplnit, že i control ValidationSummary zobrazuje pouze hlášky příslúšné ValidationGroup, tedy bychom měly mít tolik ValidationSummary, kolik máme validation groups.

Validace na straně klienta

Požadujeme-li stejné chování validátorů už na straně klienta, je situace složitější. Musíme si totiž vytvořit klientský skript, který nejprve rozhodne na základě hodnot formuláře, která zóna je vybrána (který radio-button je zakliknut) a zavolá validace pro příslušné ValidationGroup.

Volání post-backu s příslušnou validací si vygenerujeme pomocí ClientScriptManager.GetPostBackEventReference(), kam přes PostBackOptions předámeValidationGroup. Horší to bude s vícenásobnou validací (např. společná + group), kde už si budeme muset pomoct nekonvenčními metodami a volat JScript ASP.NETu natvrdo.

Vzhledem k tomu, že v praxi pro takovéto klientské validace existují hotové a promyšlené validační controly třetích stran, nemá smysl se do této oblasti hlouběji potápět…

Připojuji také odkazy na několik článků, které popisují řízení validace na straně klienta:

SqlTransaction jednoduše s využitím anonymních metod

Základní .NET pattern pro volání transakcí je poměrně jednoduchý a známý, ostatně je uveden i jako example v MSDN/SDK dokumentaci:

 
private static void ExecuteSqlTransaction(string connectionString)
{
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        SqlCommand command = connection.CreateCommand();
        SqlTransaction transaction;
  
        // Start a local transaction.
        transaction = connection.BeginTransaction("SampleTransaction");
  
        // Must assign both transaction object and connection
        // to Command object for a pending local transaction
        command.Connection = connection;
        command.Transaction = transaction;
  
        try
        {
            command.CommandText =
                "Insert into Region (RegionID, RegionDescription) VALUES (100, 'Description')";
            command.ExecuteNonQuery();
            command.CommandText =
                "Insert into Region (RegionID, RegionDescription) VALUES (101, 'Description')";
            command.ExecuteNonQuery();
  
            // Attempt to commit the transaction.
            transaction.Commit();
            Console.WriteLine("Both records are written to database.");
        }
        catch (Exception ex)
        {
            Console.WriteLine("Commit Exception Type: {0}", ex.GetType());
            Console.WriteLine("  Message: {0}", ex.Message);
  
            // Attempt to roll back the transaction.
            try
            {
                transaction.Rollback();
            }
            catch (Exception ex2)
            {
                // This catch block will handle any errors that may have occurred
                // on the server that would cause the rollback to fail, such as
                // a closed connection.
                Console.WriteLine("Rollback Exception Type: {0}", ex2.GetType());
                Console.WriteLine("  Message: {0}", ex2.Message);
            }
        }
    }
}

Po chvilce práce s transakcemi nás však začne trápit, že se poměrně značná část zdrojového kódu neustále opakuje a vlastní výkonné jádro ve změti řádek zaniká.

Jak by se Vám líbil následující způsob volání transakcí?

int myID = 5;
object result;
  
SqlDataAccess.ExecuteTransaction(
   delegate(SqlTransaction transaction)
   {
      // uvnitř lze používat i lokální proměnné (samozřejmě i parametry, statické fieldy atp.)
  
      SqlCommand cmd1 = new SqlCommand("command string");
      cmd1.Transaction = transaction;
      cmd1.Connection = transaction.Connection;
      cmd1.Parameters.AddWithValue("@MyID", myID);
      cmd1.ExecuteNonQuery();
  
      SqlCommand cmd2 = new SqlCommand("another command");
      cmd2.Transaction = transaction;
      cmd2.Connection = transaction.Connection;
      result = cmd2.ExecuteScalar();
   });

Líbí? A přitom to není nic složitého, stačí využít delegátů a anonymních metod…

/// <summary>
/// Reprezentuje metodu, která vykonává jednotlivé kroky transakce.
/// </summary>
/// <param name="transaction">transakce, v rámci které mají být jednotlivé kroky vykonány</param>
public delegate void SqlTransactionDelegate(SqlTransaction transaction);

/// <summary>
/// Třída SqlDataAccess nám pomocí statických metod usnadňuje práci s SQL serverem.
/// </summary>
public static class SqlDataAccess
{
  /// <summary>
  /// Vykoná požadované kroky v rámci transakce.
  /// Je spuštěna a commitována nová samostatná transakce.
  /// </summary>
  public static void ExecuteTransaction(SqlTransactionDelegate transactionWork)
  {
   ExecuteTransaction(transactionWork, null);
  }
  
  /// <summary>
  /// Vykoná požadované kroky v rámci transakce.
  /// Pokud je zadaná transakce <c>null</c>, je vytvořena, spuštěna a commitována nová.
  /// Pokud zadaná transakce není <c>null</c>, jsou zadané kroky pouze v rámci transakce vykonány.
  /// </summary>
  /// <param name="transaction">transakce (vnější)</param>
  public static void ExecuteTransaction(SqlTransactionDelegate transactionWork, SqlTransaction transaction)
  {
   SqlTransaction currentTransaction = transaction;
   SqlConnection connection;
   if (transaction == null)
   {
    // otevření spojení, pokud jsme iniciátory transakce
    connection = SqlDataAccess.GetConnection(); // ponechávám na Vaší implementaci
    connection.Open();
    currentTransaction = connection.BeginTransaction();
   }
   else
   {
    connection = currentTransaction.Connection;
   }
  
   try
   {
    transactionWork(currentTransaction);
 
    if (transaction == null)
    {
     // commit chceme jen v případě, že nejsme uvnitř vnější transakce
     currentTransaction.Commit();
    }
   }
   catch
   {
    try
    {
     currentTransaction.Rollback();
    }
    catch
    {
     // chceme vyhodit vnější výjimku, ne problém s rollbackem
    }
    throw;
   }
   finally
   {
    // uzavření spojení, pokud jsme iniciátory transakce
    if (transaction == null)
    {
     connection.Close();
    }
   }
  }
}

Jenom dodávám, že druhý overload umožňuje mimo vytvoření transakce nové (pokud je parametr transaction = null) i spuštění celé operace v rámci rozlehlejší transakce vnější, což může v reálu vypada nějak takto:

public class Order
{
   ...
  
   public void Save(SqlTransaction transaction)
   {
      SqlDataAccess.ExecuteTransaction(
         delegate(SqlTransaction currentTransaction)
         {
  
             this.DoSave(currentTransaction);
             OrderItems.SaveAll(currentTransaction);
             Customer.Save(currentTransaction);
                     
  
         }, transaction);
   }
}
Implementace v HAVIT .NET Framework Extensions:

Havit.Data.SqlClient.SqlTransactionDelegate(…)
Havit.Data.SqlClient.SqlDataAccess.ExecuteTransaction(…)

Co by měl každy vědět o ViewState

1. ViewState není odpovědný za zachování vlastních hodnot controlů mezi postbacky

ViewState není potřeba pro zachování hodnot (Value) TextBoxů, CheckBoxů, DropDownListů a jiných Web controlů mezi postbacky. Tyto hodnoty jsou uloženy standardně ve formulářových postback datech (POST/GET) a ASP.NET je nastaví v metodě LoadPostData() pro všechny prvky, které implementují rozhraní IPostBackDataHandler. Na to, aby nám mezi roundtripy zůstal text v TextBoxu tedy nepotřebujeme ViewState!!!

2. Na co tedy ViewState?

ViewState je vlastnost každého controlu zděděná od System.Web.UI.Control (má ji tedy i Page) a jeho základní funkčnost je založena ná následující implementaci vlastností:

public string NavigateUrl
{
   get
   {
      string text = (string) ViewState["NavigateUrl"];
      if (text != null)
         return text;
      else
         return string.Empty;
   }
   set
   {
      ViewState["NavigateUrl"] = value;
   }
}

Hodnoty vlastností (property) se tedy ukládají do a čtou z ViewState, veškeré jejich změny se promítají do ViewState.

3. ViewState je typu StateBag

ViewState je typu System.Web.UI.StateBag, která implementuje mj. IDictionary (slouží k ukládání párů klíč-hodnota) a interně používá HybridDictionary.
Důležitou metodou StateBagu je SaveViewState(), která odpovídá za uložení ViewState.
Celý fígl je v tom, že metoda SaveViewState() uloží jenom ty vlastnosti, které se změnily po zavolání metody TrackViewState().

4. Co se tedy ukládá při uložení ViewState?

Funkčnost ViewState je úzce spojena s life-cyclem stránky a okamžikem volání metody TrackViewState(). Ta je volána na konci události Init každého controlu, a tedy i stránky (pro zjištění, zda-li již byla volána, lze použít property IsTrackingViewState).
Změny properties provedené před koncem události Init každého controlu se tedy s ViewState neukládají, veškeré další změny až do volání metodySaveViewState() (v události SaveViewState) ano.

5. Jak tedy stránka/control s ViewState funguje?

Vezměme si krátký příklad:

   private void btnSubmit_Click(object sender, EventArgs e)
   {
      lblMessage.Text = "Goodbye, Everyone!";
   }

Co se stane při první návštěvě stránky:

  1. „Instantiation stage“: Nastaví se lblMessage.Text=“Hello, World!“
  2. „Load ViewState stage“: nic se nestane, není postback
  3. „Save ViewState stage“: nic se nestane, nejsou změny ViewState
  4. „Render Stage“: Label je renderován s „Hello, World!“

Co se stane při kliku na Change Message tlačítko:

  1. „Instantiation stage“: Nastaví se lblMessage.Text=“Hello, World!“
  2. „Load ViewState stage“: nic se nestane, ViewState stránky je prázdný
  3. „Raise Postback Event“: btnSubmit_Click nastaví
  4. lbl.Message=“Goodbye, Everyone!“
  5. „Save ViewState stage“: property Text od Labelu je uložena do ViewState, protože se změnila (po volání TrackViewState())
  6. „Render Stage“: Label je renderován s „Goodbye, everyone!“

Co se stane při kliku na Empty postback tlačítko:

  1. „Instantiation stage“: Nastaví se lblMessage.Text=“Hello, World!“
  2. „Load ViewState stage“: nastaví se lblMessage.Text=“Goodbyw, Everyone!“ z ViewState
  3. „Save ViewState stage“: property Text od Labelu je uložena do ViewState, protože se změnila (po volání TrackViewState())
    „Render Stage“: Label je renderován s „Goodbye, everyone!“

Shrnutí & spol.

  1. Do ViewState se ukládají všechny změny v properties controlů provedené po ukončení události Init.
  2. Protože ViewState ukládá pouze vlastnosti controlů a ne controly samotné, musíme dynamicky přidávané controly přidávat při každém postbacku stránky znovu a znovu – nejlépe během události Init (uděláme-li to však i později, metoda .Add() zajistí nahrání ViewState do přidávaných controlů).
  3. U editovatelných DataGridů je vypnutí ViewState docela dřina.
  4. ViewState se ukládá rekurzivně včetně ViewState child-controlů a to v serializované podobě pomocí LOSFormateru.
  5. ViewState lze ukládat i na serveru na disk nebo do databáze pomocí překrytí metod SavePageStateToPersistenceMedium() aLoadViewStateFromPersistenceMedium(), které standardně právě používají hidden-field ___VIEWSTATE. V některém z dalších článků si ukážeme ukládání do Session.
  6. Další možností na zmenšení ViewState je jeho komprese a dekomprese.
  7. ViewState je chráněn před změnami pomocí machine authentication check (MAC), který však pouze kontroluje, je-li ViewState od stejné verze stránky.
  8. ViewState lze i šifrovat, rozhodně se však nedoporučuje pro ukládání citlivých informací.

BUG: MasterPage zobrazuje default hodnotu ContentPlaceHolderu a ignoruje Content

Narazil jsem na zajímavý bug v ASP.NET 2.0 (2.0.50727)…

Pokud v ContentPlaceHolderu definujeme default obsah a použijeme v něm ebedded code block (starý vnořený ASP-style blok kódu <% %>, ale i <%= %>), pak ASP.NET ignoruje Content definovaný v konkrétních stránkách a stále zobrazuje pouze default obsah z MasterPage.

...
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
   <% %>   <-- už tohle vadí
   <%= "Tohle taky vadí" %>
   <%= ResolveUrl("~/takhle-na-to-asi-narazime/") %>
</asp:ContentPlaceHolder>
...

Zajímalo mě, kde je pravděpodobná příčina problému, takže jsem se díval, co z toho ASP.NET stvoří a jak se to liší od funkční podoby. Zásadní rozdíl je už v metodě __BuildControl, kde narozdíl od korektní podoby (ContentPlaceHolder1 s jedním Labelem):

private ContentPlaceHolder __BuildControlContentPlaceHolder1()
{
      ContentPlaceHolder holder1 = new ContentPlaceHolder();
      this.ContentPlaceHolder1 = holder1;
      holder1.ID = "ContentPlaceHolder1";
      if (base.ContentTemplates != null)
      {
            this.__Template_ContentPlaceHolder1 = (ITemplate) base.ContentTemplates["ContentPlaceHolder1"];
      }
      if (this.__Template_ContentPlaceHolder1 != null)
      {
            this.__Template_ContentPlaceHolder1.InstantiateIn(holder1);
            return holder1;
      }
      IParserAccessor accessor1 = holder1;
      accessor1.AddParsedSubObject(new LiteralControl("\r\n\t\t\t"));
      Label label1 = this.__BuildControlTest();
      accessor1.AddParsedSubObject(label1);
      accessor1.AddParsedSubObject(new LiteralControl("\r\n        "));
      return holder1;
}

…chybí řádek return holder1;

private ContentPlaceHolder __BuildControlContentPlaceHolder1()
{
      ContentPlaceHolder holder1 = new ContentPlaceHolder();
      this.ContentPlaceHolder1 = holder1;
      holder1.ID = "ContentPlaceHolder1";
      if (base.ContentTemplates != null)
      {
            this.__Template_ContentPlaceHolder1 = (ITemplate) base.ContentTemplates["ContentPlaceHolder1"];
      }
      if (this.__Template_ContentPlaceHolder1 != null)
      {
            this.__Template_ContentPlaceHolder1.InstantiateIn(holder1);
            // return holder1;  <-- pravděpodobně chybí
      }
      holder1.SetRenderMethodDelegate(new RenderMethod(this.__RenderContentPlaceHolder1));
      return holder1;
}
 
private void __RenderContentPlaceHolder1(HtmlTextWriter __w, Control parameterContainer)
{
      __w.Write("\r\n\t\t\t");
}

Jinak bug je již reportován v Microsoft Connect (Feedback center), můžete se připojit k hlasování…