Category Archives: Development

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í…

V odesílaných mailech chybí háčky a čárky, někde samy mizí

Pozoruhodná situace může nastat, pokud explicitně nenastavíme encoding mailů odesílaných z ASP.NET aplikací (resp. obecně .NET aplikací).

Může se nám totiž stát, že aplikace, která na jednom serveru korektně odesílá maily, v pohodě s češtinou, po přesunu na server jiný (jinak nastavený), najednou posílá maily „napůl české“. Ono „napůl“ je na tom to nejzáludnější, zprávy totiž nechodí ve stylu rozsypaný čaj, jak to známe ze špatně nastaveného encodingu, nýbrž jsou ve zprávě odebrány háčky a čárky od většiny českých znaků (zůstává třeba á, í, é). Člověka tak hned nenapadne, v čem je problém a bádá někde mimo aplikaci.

…v mém konkrétním případě druhý server díky svému nastavení posílal zprávy v encodingu iso-8859-1 a sám (předpokládám CDO, ale nebádal jsem nad tím) si je upravoval na nejlépe čitelné.

Zásadně tedy vždy explicitně určovat encoding zpráv!!!

// v případě System.Web.Mail i System.Net.Mail
using (MailMessage mail = new MailMessage())
{
   mail.BodyEncoding = Encoding.GetEncoding("iso-8859-2");
   // System.Net.Mail.MailMessage má i SubjectEncoding
   ...
}

Tisk dlouhých tabulek – záhlaví a zápatí na každé stránce

Poměrně neznámým fíglem lze v některých browserech zajistit, aby se při tisku dlouhých tabulek, které se nevejdou na jednu stránku, vytiskly určité jejich řádky na každé stránce (záhlaví a zápatí).

Stačí využít sekce tabulky thead a tfoot a nastavit jim ty správné styly.

 ...
<style type="text/css">
   thead {display: table-header-group;}
   tfoot {display: table-footer-group;}
</style>
...
<table>
   <thead>
      <tr>
         <td>Hlavička 1</td>
         <td>Hlavička 2</td>
      </tr>
   </thead>
   <tbody>
      <tr>
         <td>123</td>
         <td>456</td>
      </tr>
      ...
   </tbody>
   <tfoot>
      <tr>
         <td>Patička 1</td>
         <td>Patička 2</td>
      </tr>
   </tfoot>
</table>

Dynamické přepínání MasterPage

Page má property Page.MasterPageFile.
Hodnotu lze přiřadit výhradně ve fázi Page_PreInit, jinak se vyvolá výjimka.
Hodnotou je cesta k .master souboru.

void Page_PreInit(object sender, EventArgs e)
{
    MasterPageFile = "~/Layout1.master";
}

Implementace dynamických změn není úplně primitivní, protože ve fází PreInit nemáme ještě PostBackData, ani neproběhly žádné RaisedEventy.
V podstatě musíme použít nějaké „nezávislé“ uložiště pro použitý Master file (např. Profile nebo Session) a řešit kolizi, že MasterPageFile je potřeba nastavit u content stránky, kdežto přepínač layoutů může být už v samotném master file.
Jde to udělat nějak takto:

Content.aspx:

void Page_PreInit(object sender, EventArgs e)
{
    MasterPageFile = Profile.MasterPageFile; // Session["MasterPageFile"];
}

Layout.master:

void MasterDDL_Changed(object sender, EventArgs e)
{
   Profile.MasterPageFile = MasterDDL.SelectedValue;
   Response.Redirect(Request.Path); // !!!! nebo ReturnUrl
}

Response.Redirect je zde potřeba, protože ke změně došlo až po fázi Page_PreInit a my potřebujeme znovunačtením stránky projít přes Page_PreInit. MasterPage si můžeme ukládat i do cookie, každopádně mechanizmus přepínání je podobný jako u lokalizace.

Pokud bychom změnu chtěli udělat jediným roundtripem, museli bychom v PreInit sami parsovat data z Forms nebo QueryStringu. Nejjednodušší je přepínání nasměrovat na samostatný soubor s ReturnUrl, jako se to dělá u lokalizace nebo loginu.

Velikost okna browseru univerzálně

V jednotlivých prohlížečích se informace o aktuální velikosti okna zjišťuje dost různě, je tedy nutno použít určitou kaskádu pro „univerzální“ (alespoň trochu) vyhodnocení:

var winWidth, winHeight, d=document;
if (typeof window.innerWidth!='undefined')
{
   winWidth = window.innerWidth;
   winHeight = window.innerHeight;
}
   else
{
   if (d.documentElement && )typeof d.documentElement.clientWidth != 'undefined') && (d.documentElement.clientWidth != 0))
   {
       winWidth = d.documentElement.clientWidth;
       winHeight = d.documentElement.clientHeight;
   }
   else
   {
       if (d.body && (typeof d.body.clientWidth != 'undefined'))
       {
           winWidth = d.body.clientWidth;
           winHeight = d.body.clientHeight;
       }
   }
}

VS2005: Debugging – Unable to attach to process. Neplatný popisovač vazby.

Opravdu drsná chyba se mi projevila ve VS2005 a nebylo boha, abych k tomu na netu něco našel.
Když jsem chtěl ladit, tak mi to vyhazovalo chyby

Unable to attach to process. Neplatný popisovač vazby.
Unable to attach to process. The binding handle is invalid.

Když jsem zkoušel „Attach to process…“, tak to házelo

Unable to connect to the Microsoft Visual Studio Remote Debugging Monitor 
named '<hostname>'. Neplatný popisovač vazby.

Unable to connect to the Microsoft Visual Studio Remote Debugging Monitor
named '<hostname>'. The binding handle is invalid.

…netušim, čím to přesně je, ale nějak to souvisí s Terminal Services, když je tato služba DISABLED. Pokud se povolí služba Terminal Services, vše funguje !!!

Jako další možný workaround údajně stačí vypnout přepínač „Enable the Visual Studio hosting process“ – Project Properties ~ Debug ~ … (osobně nemám ověřeno).

Math.Round() – zaokrouhlování pětky nahoru/dolů – MidpointRounding

Po několika revizích se v blogu B. Stanik T. usadila zajímavá rekapitulace o zaokrouhlování Math.Round() v midpointu – tedy pětky.

Výchozí chování Math.Round() zaokrouhluje pětku na sudé číslo, tedy

double x = Math.Round(4.555, 2); // x == 4.56
// ale
double y = Math.Round(4.585, 2); // y == 4.58

…říká se tomu „rounding to nearest“, „banker’s rounding“, nebo taky statistické zaokrouhlování – to proto, že se tím eliminuje statistická chyba způsobená zaokrouhlováním pětky stále stejným směrem (takhle se to prostřídá nahoru/dolu).

V .NET 2.0 přibyly metodě Math.Round() overloady, které berou jako třetí parametr enum MidpointRounding, kterým lze vynutit zaokrouhlování pětky nahoru potřebné pro účetní operace:

double x = Math.Round(4.555, 2, MidpointRounding.AwayFromZero); // x == 4.56
double y = Math.Round(4.585, 2, MidpointRounding.AwayFromZero); // y == 4.59

…druhým členem MidpointRounding je default hodnota ToEven.

SmptMail.Send: The ‚SendUsing‘ configuration value is invalid.

Ač dokumentace tvrdí, že se standardně použije lokální SMTP server, přesto někdy problém odstraní nastavení:

SmtpMail.Server = "localhost";

…každopádně v .NET 2.0 je lepší použít mailování z namespace System.Net.