Author Archives: Robert Haken

avatar Neznámé

About Robert Haken

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

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.

Připojení Outlooku RPC over HTTP(S) na Exchange

Na netu jsou stovky návodů, jak nakonfigurovat RPC over HTTP komunikaci Outlooku s Exchange serverem, např.

Návody jsou to celkem srozumitelné, nicméně může nastat mnoho zásadních zádrhelů, které návody nedostatečně zmiňují, či málo zdůrazňují. Některé, na které jsem narazil já:

Certifikát k HTTPS spojení musí být důvěryhodný

Pokud používáme k HTTPS certifikát generovaný vlastní certifikační autoritou (CA), pak musíme na klientském počítači zajistit, aby připojení k serveru probíhalo čistě, bez jakéhokoliv promptu Internet Exploreru na certifikát. Outlook totiž tento prompt nepodporuje a spojení zamítne.

V podstatě je potřeba nainstalovat certifikát na klienta, třeba přístupem na https://server/rpc/rpcproxy.dll. Musíme dosáhnout stavu, aby se nás to ptalo jen na heslo, na nic jiného. Nainstalovaný certifikát nám však bude fungovat jen v případě, že máme instalovaný i certifikát CA – ten je v členských počítačích domény instalován automaticky (pokud CA používá AD), na nečlenských počítačích je potřeba ho instalovat samostatně.

Pozor – narazil jsem na problém, kdy IIS mělo přidělený certifikát, ale mezi tím došlo k reinstalaci CA. Certifikát na IIS tak neodpovídal kořenovému certifikát CA. Internet Explorer to navíc pojal po svém. Místo, aby ohlásil neplatný certifikát, tak po instalaci kořenového certifikátu na jakýkoliv HTTPS požadavek vůči příslušnému serveru odpovídal chybou „Server nebyl nalezen nebo došlo k chybě v systému DNS.“ (jako kdyby server neexistoval).

Global Catalog na jiném serveru

Pokud máme scénář s jedním Exchange serverem, na kterém navíc běží i doménový řadič (DC), může se zdát, že není co řešit. Zásadní komplikace však nastane, pokud máme v doméně více DC a Global Catalog je na jiném serveru, než Exchange.

Který server je Global Catalogem poznáme v konzoli Active Directory Sites and Services, pokud si nalistujeme příslušný server, položku „NTDS Settings“ a k ní Properties – tam je zaškrtávací políčko Global Catalog. Jak přesunout GC na jiný server je mimo dosah tohoto článku.

Pokud tedy máme GC na jiném serveru, nestačí nám standardní konfigurace portů (hodnota ValidPorts), protože potřebujem na port 6004 dostat právě process lsass.exe z GC.

Jedna možnost je přesunout GC na stejný server k Exchange. Druhou možností je nastavit klíč registru „NSPI interface protocol sequences“ i na GC server a na exchange serveru nastavit v klíči „ValidPorts“ port 6004 na GC server (NetBIOS jméno i FQDN). V Outlooku pak jako server exchange vyplníme NetBIOS název GC serveru.

Pozor na to, že nám ve standardním nastavení funguje dokonce i RPCPING, protože si server s exchange na port 6004 nasadí vlastní proces lsass.exe. Nicméně Outlook zahájí komunikaci a nic se nepřipojí. V logu webové služby jsou též korektní řádky RPC_IN_DATA a RPC_OUT_DATA se success kódem 200.

Troubleshooting nástroje a doporučení

  • spouštět Outlook s parametrem outlook /rpcdiag (též Ctrl+RClick na tray-ikonu Outlooku a volba Connection Status…)
  • telnet localhost 6001,  6002, 6004 (ze serveru) dává banner „ncacn_http/1.0“
  • netstat -ano (ze serveru) nám říká, jaké procesy obsluhují jaký portu (6001-store.exe, 6002-mad.exe, 6004-lsass.exe nebo mad.exe),
  • utilita rpcping z Windows Server Resource Kitu (z klienta),
  • utilita rpcdump z Windows Server Resource Kitu (z klienta i ze serveru),
  • adresa https://server/rpc/ se musí ptát na heslo a pak odpovědět 401.3,
  • adresa https://server/rpc/rpcproxy.dll se musí ptát na heslo a pak odpovědět prázdnotou,
  • v logu webové služby musí být záznamy RPC_IN_DATA a RPC_OUT_DATA, kód 200.
  • při nastavování accountu v Outlooku musí být v základním dialogu interní jméno serveru a až v nastavení RPC over HTTPS se dávají public jména serveru (ty se musí shodovat se jménem, na které je vystaven certifikát pro SSL)

VPN přípojení a zachování přístupu k internetu

Pokud používáme VPN klienta z Windows a připojujeme se k virtuální privátní síti (VPN), pak ve výchozím nastavení ztratíme po připojení k VPN přímý přístup k internetu, protože VPN klient nastaví default gateway na vzdálenou default gateway VPN serveru (i z důvodu zabezpečení VPN). Veškerá IP komunikace, která není explicitně routována jinak (což obvykle není), je tak směřována na default gateway VPN připojení – to buď požadavek vyřídí, nebo nevyřídí (často právě spíše nevyřídí a „jsme bez internetu“).

Každopádně, pokud chceme zachovat dosavadní gateway, stačí ve vlastnostech VPN připojení odškrnout položku „Use default gateway on remote network“, respektive„Používat výchozí bránu vzdálené sítě“. Tu najdeme na záložce Sítě, pod vlastnostmi protokolu TCP/IP a Upřesnit, ve starých Win9x to bylo pod Properties ~ Server Types ~ TCP/IP Settings.

Pozor však, že standardní volba má svojí logiku. Pokud se připojíme na VPN, stává se náš počítač rozhraním mezi firemní sítí a internetem a je tak potenciálně nebezpečným místem pro útoky.

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.

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);
}