Author Archives: Robert Haken

avatar Neznámé

About Robert Haken

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

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.

Outlook: Vyhledávání adresáta pouze v Global Address List

Pokud v Outlooku vyhledáváme uživatele, pak pokud existuje záznam v Kontaktech (Contacts) i v Globálním seznamu adres (Global Address List), pak nám Outlook vrátí ten z Kontaktů. Pokud však vyhledáváme pomocí syntaxe „=username“, pak dostaneme vždy záznam z Global Address List.

SQL: IntArray – pole hodnot typu int jako UDT + aggregate (CLR)

Na SQL serveru do verze 2000 mě štvalo, že nemá žádnou rozumnou práci s poli. Jakmile tedy vyšel SQL2005 s možností použití .NET Frameworku (CLR), napsal jsem si hned vlastní typ IntArray. Nyní, po půl roce jeho používání, mohu říct, že se výborně osvědčil.

Typ IntArray samozřejmě není primárně určen pro použití v tabulkách, kde by sloužil jako typ sloupce, tím bychom degradovali relační schéma. Co nám tedy takové pole prvků typu int usnadní?

  • můžeme snadno psát parametrické dotazy s podmínkou WHERE CiselnikID IN [1, 2, 3, …], typické pro vyhledávání – máme-li například tabulku osob, číselník jejich pracovních pozic a chceme-li vypsat několik pozic najednou – například všechny účetní, personalisty a uklízečky k tomu,
  • můžeme snadno „jedním vrzem“ (do jedné řádky) načítat objekty i s položkami – pokud používáme v Business Layer klasický přístup s ghost objekty inicializovanými svým ID a s lazyloadem dalších hodnot, pak můžeme krásně načítat nadřazený objekt i s jeho členskými kolekcemi. Například v objednavka.Load() načteme data objednávky a zároveň pole IDček všech řádek objednávky, kterým hned vytvoříme ghost objekty,
  • stejně jako načítat, můžeme snadno ukládat vybrané kolekce objektu, například můžeme snadno uložit jedním vrzem uživatele i se seznamem jeho rolí.

SqlInt32Array

První, co tedy potřebujeme, je samotný CLR user-defined type:

using System;
using System.Data;
using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;
using System.Text;
using System.Collections;
using System.Collections.Generic;
  
namespace Havit.Data.SqlTypes
{
 [Serializable]
 [SqlUserDefinedType(
  Format.UserDefined,
  Name = "IntArray",
  IsByteOrdered = true,
  MaxByteSize = 8000)]
 public class SqlInt32Array : INullable, IBinarySerialize
 {
  #region private value holder
  private List<SqlInt32> values = null;
  #endregion 
  



  #region Constructors
  /// <summary>
  /// Vytvoří instanci s hodnotou NULL.
  /// </summary>
  public SqlInt32Array()
  {
   this.values = null;
  }
  

  /// <summary>
  /// Vytvoří instanci a naplní ji předanými hodnotami.
  /// </summary>
  /// <param name="values">hodnoty, které mají instance reprezentovat</param>
  public SqlInt32Array(int[] values)
  {
   if ((values == null) || (values.Length == 0))
   {
    this.values = null;
    return;
   }
   if (values.Length > 1999)
   {
    throw new ArgumentException(String.Format("Maximální velikost pole je 1999 hodnot, požadováno je však {0} hodnot.",
     values.Length));
   }

   this.values = new List<SqlInt32>();
   for (int i = 0; i < values.Length; i++)
   {
    this.values.Add(new SqlInt32(values[i]));
   }
  }
  #endregion
   

  #region Add
  /// <summary>
  /// Přidá prvek do pole.
  /// </summary>
  /// <param name="value"></param>
  public void Add(SqlInt32 value)
  {
   if (!value.IsNull && (value.Value == Int32.MinValue))
   {
    throw new ArgumentException("Prvek nesmí mít vyhrazenou hodnotu Int32.MinValue.");
   }

   if (this.values == null)
   {
    this.values = new List<SqlInt32>();
   }
   values.Add(value);
  }
  #endregion

  #region Count
  /// <summary>
  /// Počet prvků v seznamu.
  /// </summary>
  public int Count
  {
   get { return values.Count; }
  }
  #endregion
   

  #region Indexer
  /// <summary>
  /// Indexer pro přístup k prvkům podle jejich pořadí.
  /// </summary>
  /// <param name="index">index (pořadí) prvku</param>
  /// <returns>hodnota <see cref="SqlInt32"/></returns>
  public SqlInt32 this[int index]
  {
   get { return values[index]; }
  }
  #endregion
   

  #region Merge
  /// <summary>
  /// Spojí dvě pole v jedno.
  /// </summary>
  /// <param name="array">přidávané pole</param>
  public void Merge(SqlInt32Array array)
  {
   if (!array.IsNull)
   {
    for (int i = 0; i < array.values.Count; i++)
    {
     this.values.Add(array.values[i]);
    }
   }
  }
  #endregion
   

  #region Accessors
  /// <summary>
  /// Vrátí pole SqlInt32[] s hodnotami.
  /// </summary>
  /// <returns>Pole SqlInt32[] s hodnotami.</returns>
  public SqlInt32[] GetSqlInt32Array()
  {
   if (this.IsNull)
   {
      return null;
   }
   return (SqlInt32[])values.ToArray();
  }

  /// <summary>
  /// Vrací tabulku Int32 hodnot.
  /// Metoda určená pro mapování do T-SQL na table-valued function (TVF).
  /// </summary>
  /// <param name="values">Proměnná, která má být rozbalena do tabulky hodnot Int32.</param>
  /// <returns>tabulka Int32 hodnot (pomocí FillInt32Row)</returns>
  [SqlFunctionAttribute(
   Name= "IntArrayToTable",
   TableDefinition = "[Value] int",
   FillRowMethodName = "FillSqlInt32Row")]
  public static IEnumerable GetSqlInt32Values(SqlInt32Array values)
  {
   return values.GetSqlInt32Array();
  }

  /// <summary>
  /// Metoda zajišťující převod řádku v table-valued function (TVF).
  /// </summary>
  /// <param name="int32ArrayElement">vstupní hodnota řádku</param>
  /// <param name="value">výstupní hodnota řádku</param>
  public static void FillSqlInt32Row(object sqlInt32ArrayElement, out SqlInt32 value)
  {
   value = (SqlInt32)sqlInt32ArrayElement;
  }
  #endregion
   

  #region Parse
  /// <summary>
  /// Vytvoří z CSV textové reprezentace hodnotu pole.
  /// </summary>
  /// <param name="text">CSV text hodnot</param>
  /// <returns>pole s hodnotami dle CSV</returns>
  [SqlMethod(DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None)]
  public static SqlInt32Array Parse(SqlString text)
  {
   if (text.IsNull)
   {
    return Null;
   }
   string[] parts = text.Value.Split(',');
   int length = parts.Length;
   SqlInt32Array result = new SqlInt32Array();
   for (int i = 0; i < length; i++)
   {
    if (String.Compare(parts[i].Trim(), "NULL", true) == 0)
    {
     result.Add(SqlInt32.Null);
    }
    else
    {
     result.Add(new SqlInt32(Convert.ToInt32(parts[i])));
    }
   }
   return result;
  }
  #endregion
   

  #region ToString
  /// <summary>
  /// Převede hodnotu na CSV textovou reprezentaci string
  /// </summary>
  /// <returns>CSV seznam hodnot</returns>
  public override string ToString()
  {
   if (this.IsNull)
   {
    return null;
   }

   StringBuilder sb = new StringBuilder();
   for (int i = 0; i < this.values.Count; i++)
   {
    if (this.values[i].IsNull)
    {
     sb.Append("NULL");
    }
    else
    {
     sb.Append(this.values[i].Value);
    }
    if (i < this.values.Count - 1)
    {
     sb.Append(",");
    }
   }
   return sb.ToString();
  }
  #endregion
   

  #region Null (static)
  /// <summary>
  /// Hodnota NULL.
  /// </summary>
  public static SqlInt32Array Null
  {
   get
   {
    return new SqlInt32Array();
   }
  }
  #endregion
   

  #region INullable Members
  /// <summary>
  /// Indikuje, zda-li je hodnota NULL.
  /// </summary>
  public bool IsNull
  {
   get
   {
    return ((this.values == null) || (this.values.Count == 0));
   }
  }
  #endregion
   

  #region IBinarySerialize Members
  /// <summary>
  /// Načte hodnotu z binární reprezentace.
  /// </summary>
  /// <remarks>
  /// Binární serializace je takováto:
  /// byte 1-4 ~ Int32 Length (velikost pole, pokud je 0, pak je hodnota NULL)
  /// byte 5-(8000) ~ values (NULL hodnoty reprezentuje Int32.MinValue)
  /// </remarks>
  /// <param name="r"><see cref="System.IO.BinaryReader"/> s binární reprezentací hodnoty</param>
  public void Read(System.IO.BinaryReader r)
  {
   // byte 1 - počet hodnot
   Int32 length = r.ReadInt32();
   if (length == 0)
   {
    // NULL
    this.values = null;
   }
   else
   {
    // hodnoty
    this.values = new List<SqlInt32>();
    for (int i = 0; i < length; i++)
    {
     Int32 temp = r.ReadInt32();
     if (temp == Int32.MinValue)
     {
      this.values.Add(SqlInt32.Null);
     }
     else
     {
      this.values.Add(new SqlInt32(temp));
     }
    }
   }
  }
  

  /// <summary>
  /// Vytvoří binární reprezentaci hodnoty.
  /// </summary>
  /// <remarks>
  /// Binární serializace je takováto:
  /// byte 1-4 ~ Int32 Length (velikost pole, pokud je 0, pak je hodnota NULL)
  /// byte 5-(8000) ~ values (NULL hodnoty implementuje Int32.MinValue)
  /// </remarks>
  /// <param name="w"><see cref="System.IO.BinaryWriter"/> do kterého má být binární reprezentace zapsána</param>
  public void Write(System.IO.BinaryWriter w)
  {
   // byte 1 - počet hodnot
   if (this.IsNull)
   {
    w.Write(0);
   }
   else
   {
    w.Write(this.values.Count); 
    // hodnoty
    for (int i = 0; i < this.values.Count; i++)
    {
     if (this.values[i].IsNull)
     {
      w.Write(Int32.MinValue);
     }
     else
     {
      w.Write(this.values[i].Value);
     }
    }
   }
  }
  #endregion
 }
}

Jak jste si jistě všimli, mimo typu IntArray zavádíme hned i UDF funkci IntArrayToTable, kterou bude v T-SQL pole převádět na tabulku.

Náš UDT můžeme do SQL serveru zavést buď přímo z Visual Studia pomocí Deploy, pokud máme jako typ projektu SQL Server Project, nebo ručně pomocí T-SQL:

CREATE ASSEMBLY [Havit.Data.SqlServer]
FROM 'C:\Havit.Data.SqlServer.dll'
  
CREATE TYPE [dbo].IntArray
EXTERNAL NAME [Havit.Data.SqlServer].[Havit.Data.SqlTypes.SqlInt32Array]
  
CREATE FUNCTION IntArrayToTable
(
    @array dbo.IntArray
)
RETURNS TABLE
(
     [Value] int
)
AS EXTERNAL NAME [Havit.Data.SqlServer].[Havit.Data.SqlTypes.SqlInt32Array].[GetInt32Values]

Příklad vyhledávání:

CREATE PROCEDURE Filter
(
     @Vlastnosti dbo.IntArray = NULL
)
AS
    SELECT col FROM tab
        WHERE ((@Vlastnosti IS NULL) OR (VlastnostID IN (SELECT Value FROM dbo.IntArrayToTable(@Vlastnosti))))

Příklad využití pole IDček pro ukládání:

 

CREATE PROCEDURE dbo.Uzivatel_Update 
(
 @UzivatelID int,
 @Username varchar(30),
 @Password nvarchar(30),
 @DisplayAs nvarchar(50),
 @Email nvarchar(80),
 @Deleted bit = 0,
 @Role IntArray = NULL
)
AS
 SET NOCOUNT ON
   
 SET XACT_ABORT ON
 BEGIN TRANSACTION
   
  -- Update tabulky uživatelů
  UPDATE dbo.Uzivatel
   SET
    Username = @Username,
    Password = @Password,
    DisplayAs = @DisplayAs,
    Email = @Email,
    Deleted = @Deleted
   WHERE
    (UzivatelID = @UzivatelID)
      
  -- Update rolí
  DELETE FROM dbo.Uzivatel_Role
   WHERE
    (UzivatelID = @UzivatelID)
    
  IF (@Role IS NOT NULL)
   INSERT INTO dbo.Uzivatel_Role(UzivatelID, RoleID)
    SELECT @UzivatelID AS UzivatelID, Value AS RoleID FROM dbo.IntArrayToTable(@Role)
   
 COMMIT TRANSACTION
   
 RETURN

Použití UDT z aplikace pak vypadá nějak takto:

SqlParameter paramRole = new SqlParameter("@Role", SqlDbType.Udt);
   paramRole.UdtTypeName = "IntArray";
   paramRole.Value = new SqlInt32Array(this.Role.GetIDs());
   cmd.Parameters.Add(paramRole);

SqlInt32ArrayAggregate

Na to, abychom z tabulky se sloupcem int hodnot dostali pole, potřebujeme agregát. Tedy obdobu SUM, AVG, MAX, MIN, prostě něco, co udělá ze všech hodnot jednu.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.IO;
  
namespace Havit.Data.SqlTypes
{
 /// <summary>
 /// Aggregate k UDT SqlInt32Array, který zajišťuje převod tabulky hodnot na pole.
 /// </summary>
 [Serializable]
 [Microsoft.SqlServer.Server.SqlUserDefinedAggregate(
  Format.UserDefined,
  IsInvariantToDuplicates = false, IsInvariantToNulls = false, IsInvariantToOrder = true, IsNullIfEmpty = true,
  MaxByteSize = 8000, Name = "IntArrayAggregate")]
 public class SqlInt32ArrayAggregate : IBinarySerialize
 {
  #region private value holder
  /// <summary>
  /// Uchovává mezivýsledek.
  /// </summary>
  private SqlInt32Array array;
  #endregion
  
  #region Init
  /// <summary>
  /// Inicializace agregátoru.
  /// </summary>
  public void Init()
  {
   array = new SqlInt32Array();
  }
  #endregion
  
  #region Accumulate
  /// <summary>
  /// Přidá další hodnotu do agregace.
  /// </summary>
  /// <param name="value">přidávaná hodnota</param>
  public void Accumulate(SqlInt32 value)
  {
   array.Add(value);
  }
  #endregion
  
  #region Merge
  /// <summary>
  /// Spojí dva agregáty v jeden
  /// </summary>
  /// <param name="group">druhá agregace</param>
  public void Merge(SqlInt32ArrayAggregate group)
  {
   group.array.Merge(group.array);
  }
  #endregion
  
  #region Terminate
  /// <summary>
  /// Vrátí výsledek agregace.
  /// </summary>
  public SqlInt32Array Terminate()
  {
   return this.array;
  }
  #endregion
  
  #region IBinarySerialize Members
  /// <summary>
  /// De-serializuje agregaci.
  /// </summary>
  /// <param name="r">BinaryReader</param>
  public void Read(BinaryReader r)
  {
   this.array = new SqlInt32Array();
   this.array.Read(r);
  }
  
  /// <summary>
  /// Serializuje agregaci.
  /// </summary>
  /// <param name="w">BinaryWriter</param>
  public void Write(BinaryWriter w)
  {
   this.array.Write(w);
  }
  #endregion
 }
}

A příklad použití takového agregátu:

CREATE PROCEDURE dbo.Uzivatel_Load 
(
 @UzivatelID int
)
AS
 SET NOCOUNT ON
   
 SELECT
   Uzivatel.*,
   (SELECT dbo.IntArrayAggregate(RoleID) FROM dbo.Uzivatel_Role WHERE UzivatelID = @UzivatelID) AS Role
  FROM dbo.Uzivatel
  WHERE
   (UzivatelID = @UzivatelID)
   
 RETURN

… a jeho využití v aplikaci:

SqlInt32Array roleArray = (SqlInt32Array)reader["Role"];

…určitě se toho dá mnohé vylepšovat, uvítám samozřejmě jakékoliv komentáře.

Update 09/2013

CLR datový typ IntArray a související IntArrayAggregate jsme s drobnými doladěními používali jako klíčovou součást datové vrstvy na 99% našich aplikací od roku 2006 až do roku 2012. Nejednalo se tedy o žádný experimentální pokus, ale produkční prvek, který nám za tu dobu udělal velmi dobrou službu. Jedinou nevýhodou bylo občasné dohadování ohledně potřeby povolit CLR na SQL serverech zákazníků/hostingu.

Na konci roku 2012 jsme se rozhodli, že pro MS SQL 2008 a novější přejdeme na Table Value Parameters pro „směr tam“ a SELECT FOR XML pro „směr zpět“ (místo SqlIntArrayAggregate). Motivací bylo získání vyššího výkonu (byť dosavadní řešení s CLR poskytovalo výkon přinejmenší dostatečný) a zjednodušení situace.

IntArray + IntArrayAggregate jsme si ponechali jen pro pár zbývajících SQL 2005 serverů.

Dynamicky přidávané controly musí mít nastaveno ID, jinak jim blbne postback

Pokud do control-tree přidáváme dynamicky nějaké controly, které mají obsluhu postbacku (data nebo event), pak pokud těmto controlům explicitně nanastavíme nějaké ID, může se nám snadno stát, že postback nebude korektně vyhodnocen, např.:

  • Button, LinkButton, ImageButton nebudou emitovat události Click, Command, …,
  • inputové controly (TextBox, DropDownList, …) zapomenou přes roundtrip data,

…v zábavnějším případě se nám může stát, že postback není korektní jen u některých dynamický controlů a u některých se vyhodnotí správně (třeba pokud přidáváme řádky objednávky, tak nám data zapomíná jen poslední řádek).

Nevím, jestli je to bug, ale dělalo to už v .NET 1.1 a dělá to i v .NET 2.0.

Každopádně nastavením ID u dynamicky přidávaných controlů se těchto potíží zbavíme!

private void CreateControlsHiearchy()
{
   LinkButton lb = new LinkButton();
   lb.ID = "MyLB"; // <--- bez toho není jisté, že se nám bude Click korektně volat !!!
   lb.Click += ...
   ...
   Controls.Add(lb);
}

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 + &quot;_PocetZasilek&quot;];
     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 + &quot;_PocetZasilek&quot;, _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 &lt; PocetZasilek; i++)
   {
    CreateZasilkaControl(Controls.Count);
   }
  
   Literal lit1 = new Literal();
   lit1.Text = &quot;&lt;tr&gt;&lt;td colspan=\&quot;3\&quot; class=\&quot;zindent\&quot;&gt;&quot;;
   Controls.Add(lit1);
  
   // tlačítko pro přidání položky
   pridejZasilkuLB = new LinkButton();
   pridejZasilkuLB.ID = &quot;PridejZasilkuLB&quot;; // &lt;-- nutno nastavit ID, jinak nám můžou blbnout postbacky
   pridejZasilkuLB.Text = (string)HttpContext.GetGlobalResourceObject(&quot;Zasilky&quot;, &quot;PridejZasilku&quot;);
   pridejZasilkuLB.CssClass = &quot;arrow&quot;;
   pridejZasilkuLB.Click += new EventHandler(pridejZasilkuLB_Click);
   pridejZasilkuLB.CausesValidation = false;
   Controls.Add(pridejZasilkuLB);
  
   Literal lit3 = new Literal();
   lit3.Text = &quot;&lt;br/&gt;\n&quot;;
   Controls.Add(lit3);
  
   // tlačítko pro odebrání položky
   odeberZasilkuLB = new LinkButton();
   odeberZasilkuLB.ID = &quot;OdeberZasilkuLB&quot;; // &lt;-- nutné nastavit ID, jinak nám můžou blbnout postbacky
   odeberZasilkuLB.Text = (string)HttpContext.GetGlobalResourceObject(&quot;Zasilky&quot;, &quot;OdeberPosledniZasilku&quot;);
   odeberZasilkuLB.CssClass = &quot;arrow&quot;;
   odeberZasilkuLB.Click += new EventHandler(odeberZasilkuLB_Click);
   odeberZasilkuLB.CausesValidation = false;
   odeberZasilkuLB.Visible = (PocetZasilek &gt; 1);
   Controls.Add(odeberZasilkuLB);
  
   Literal lit2 = new Literal();
   lit2.Text = &quot;&lt;/td&gt;&lt;/tr&gt;\n&quot;;
   Controls.Add(lit2);
  }
  
  // vytvoření jedné položky v control-tree
  private void CreateZasilkaControl(int index)
  {
   Zasilka zasilkaControl = (Zasilka)Page.LoadControl(&quot;~/Controls/Zasilka.ascx&quot;);
   zasilkaControl.ID = &quot;ZasilkaUC_&quot; + index.ToString(); // &lt;-- 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 &gt; 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 &lt; Zasilky.PocetZasilek; i++)
{
   Zasilka zasilka = (Zasilka)Zasilky.Controls[i];
   uloziste[i] = zasilka.Hmotnost;
}
  
// kdyby byl položkou jen TextBox
for (int i=0; i &lt; 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"/>