Author Archives: Robert Haken

avatar Neznámé

About Robert Haken

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

AJAX: Explicitní volání webových služeb z klienta

Microsoft ASP.NET AJAX 1.0 je právě ve stádiu Release Candidate (RC), nastává tedy správný čas pro bližší seznámení… ;-)

I když většinu potřebných AJAX features najdeme hotových ve formě server controlů, kdy nejsme nuceni napsat ani jediný řádek JavaScriptu,  není od věci vědět, že i explicitní volání webových služeb je z ASP.NET AJAXu velmi jednoduché a snadno použitelné (obzvláště pokud poznáme, že je to prakticky stejné, jako přístup k webovým službám na straně serveru, resp. z .NET Frameworku).

Zpřístupnění webové služby

Základem každé AJAX-enabled stránky je control ScriptManager, který zajišťuje správu klientského AJAXového JavaScriptu. Zpřístupnění webové služby pak není nic jiného, než vytvoření ServiceReference na naší webovou službu

<asp:ScriptManager ID="ScriptManager" runat="server">
   <Services>
      <asp:ServiceReference Path="~/MyWebService.asmx" />
   </Services>
</asp:ScriptManager>

Princip je to prakticky obdobný, jako v případě webových služeb na straně serveru (resp. v .NET aplikacích). Lze to přirovnat k přidání Web Reference do projektu ve Visual Studiu, nebo k použití utility wsdl.exe k vygenerování proxy třídy pro přístup k webové službě. Jediným zásadním rozdílem je, že lze takto přistupovat pouze na vlastní webové služby – tedy webové služby, které jsou součástí naší aplikace. Pokud bychom potřebovali přístup k externím webovým službám, není však problém vytvořit bridge prostřednictvím vlastní proxy webové služby.

Vraťme se však k našemu příkladu, dejme tomu, že webová služba ~/MyWebService.asmx by definovala jednu metodu

using System;
using System.Data;
using System.Web;
using System.Collections;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.ComponentModel;
using System.Web.Script.Services;

namespace MyNamespace
{
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ScriptService]
    public class MyWebService : System.Web.Services.WebService
    {

        [WebMethod]
        public string GetServerTime(string serverName)
        {
            return serverName + ": " + DateTime.Now.ToShortTimeString();
        }
    }
}

…jedná se o úplně klasickou webovou službu, která má navíc jen atribut [ScriptService] (jinak lze však tuto webovou službu využít jako kteroukoliv jinou). Atribut ScriptService zajišťuje, že nový handler .asmx souborů, s kterým ASP.NET AJAX přichází, dokáže při požadavku GET MyWebService.asmx/js vrátit vygenrovaný JavaScript s příslušnými proxy objekty pro práci s webovou službou. Ve skutečnosti totiž control ScriptManager přidá do stránky kód

</script src="MyWebService.asmx/js" type="text/javascript">

Pomocí atributu [ScriptMethod] u jednotlivých metod jsme navíc schopni řídit další detaily, např. formát, kterým spolu klientská a serverová část komunikují. Ve výchozí podobě totiž není komunikace na bázi XML, nýbrž v podobě JSON (JavaScript Object Notation), např.:

{"prefixText":"M","count":3}
["Michal", "Mirek", "Milan"]  

Volání metod webové služby

Samotné volání metod webové služby je už hračkou, ASP.NET AJAX nám totiž vygeneruje objekt odpovídající webové službě, v namespace odpovídající webové službě a s metodami dle webové služby. Nejjednodušší volání tak může vypadat např.

<script type="text/javascript">
    MyNamespace.MyWebService.GetServerTime('MyServer');
</script>

Musíme si však uvědomit, že veškeré AJAXové requesty jsou asynchronní, tedy musíme metodě říct, jakou funkci má zavolat po dokončení operace (přijetí odezvy od serveru) – výše uvedená nejjednodušší forma se tak moc často neuplatní. Nemáme-li totiž nastaven defaultní callback, pak nemáme jak převzít výsledek a jedná se prakticky jen o jednosměrnou komunikaci (navíc bez success-checkingu).

Další možností je tedy přidat jako další parametr metody funkci, která se má zavolat v okamžiku přijetí odezvy serveru, a která převezme a zpracuje výsledek. Parametrem funkce je výsledek volání metody přijatý v odezvě serveru.

<script type="text/javascript">
    MyNamespace.MyWebService.GetServerTime('MyServer', OnComplete);

    function OnComplete(result)
    {
        alert(result);
    }
</script>

Další vhodnou možností je přidat jako další parametr volání metody funkci, která se má zavolat v případě neúspěchu, v případě chyby:

<script type="text/javascript">
    MyNamespace.MyWebService.GetServerTime('MyServer', OnComplete, OnError);
    function OnComplete(result, context)
    {
        alert(result);
    }
    function OnError(result)
    {
        alert(result.get_message());
    }
</script>

Jako poslední parametr lze přidat ještě určitý context, prostě obecná data, která se mají předat do metody OnComplete (neposílají se na server):

<script type="text/javascript">
    MyNamespace.MyWebService.GetServerTime('MyServer', OnComplete, OnError, 'context');
    function OnComplete(result, context)
    {
        alert(context);
        alert(result);
    }
    
    function OnError(result)
    {
        alert(result.get_message());
    }
</script>

Webové metody ve stránce

Abychom nemuseli vždy vytvářet úplnou webovou službu, máme též možnost jednoduché lokální metody umístit přímo do třídy samotné webové stránky (do kódu stránky), odekorovat je atributem [WebMethod] a následně k nim můžeme přistupovat prostřednictvím objektu PageMethods, aniž bychom museli jakokouliv webovou službu registrovat. Musíme jen povolit přepínač EnablePageMethods ScriptManageru:

public partial class _Default : System.Web.UI.Page
{
    [System.Web.Services.WebMethod]
    public static string GetHelloWorld()
    {
        return "Hello world!";
    }
}
<script type="text/javascript">
    PageMethods.GetHelloWorld(OnHWComplete);
    function OnHWComplete(result)
    {
        alert(result);
    }
</script>

Webová metoda uvnitř stránky musí být statická!

Shrnutí

Explicitní volání webových služeb z klientské strany je velmi silnou zbraní ASP.NET AJAXu, pomocí které lze realizovat prakticky libovolnou AJAX funkčnost pro kterou nenajdeme vhodný hotový control. Pokud si navíc uvědomíme, že webová služba může pracovat prakticky s libovolnými serializovatelnými datovými typy (včetně DataSetu!), pak tímto dostáváme velmi příjemný nástroj pro klientské skriptování.

Související odkazy

GroupingRepeater – Groupování dát v Repeateru

Představme si situaci, kdy pomocí Repeateru vypisujeme určitá data, přičemž tato data chceme podle určitého kritéria seskupit, např. tedy chceme seznam zakázek seskupovat podle měsíce vytvoření, tj. mít v místě přelomu měsíce určitou vloženou položku s předělovou informací:

 

V zásadě toho lze pro určité scénáře dosáhnout poměrně snadno, i když ne zcela čistě. Níže uvedený postup berte spíše jako jednu z nejjednodušších omezených možností a inspiraci pro tvorbu složitějšího controlu stejné funkčnosti. Omezení viz níže!

Každá položka standardního Repeateru se při vytvoření dostává do jeho kolekce Items, přičemž je vyvolána událost ItemCreated. Stáčí nám tedy, pokud si budeme v obsluze této události ukládat data příslušného řádku a porovnávat je s daty řádku předchozího. Pokud se data ve vlastnosti, podle které chceme groupovat, liší, pak do kolekce Items repeateru přidáme novou položku – nadpis příslušného seskupení.

GroupingRepeater

Celý výše uvedený postup lze poměrně elegantně ztvárnit do controlu GroupingRepeater, který bude pro groupovací řádek používat šablonu <GroupingTemplate> a pro rozlišení datových položek k seskupení IComparer (pokud jsou položky shodné, jdeme dál, pokud se liší, vkládáme seskupovací řádek).

GroupingRepeater.cs

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections;
using System.ComponentModel;

namespace MyNamespace
{
    public class GroupingRepeater : System.Web.UI.WebControls.Repeater
    {
        private static object lastValue = null;

        public IComparer Comparer
        {
            get { return _comparer; }
            set { _comparer = value; }
        }
        private IComparer _comparer = null;

        [TemplateContainer(typeof(GroupHeader))]
        public ITemplate GroupTemplate
        {
            get { return _groupTemplate; }
            set {_groupTemplate = value; }
        }
        private ITemplate _groupTemplate = null;

        protected override void CreateChildControls()
        {
            lastValue = null; // na začátku je předchozí hodnota null
            base.CreateChildControls ();
        }

        protected override void OnItemCreated(RepeaterItemEventArgs e)
        {
            if(e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
            {
                if(e.Item.DataItem != null)
                {
                    if(_comparer.Compare(lastValue, e.Item.DataItem) != 0)
                    {
                        // Comparer označil položky jako různé, přidáme groupovací položku
                        GroupHeader item = new GroupHeader();               
                        _groupTemplate.InstantiateIn(item);
                        item.DataItem = e.Item.DataItem;
                        this.Controls.Add(item);
                        item.DataBind();
                    }
                }
                lastvalue = e.Item.DataItem;
            }
            base.OnItemCrated(e);
        }

        public class GroupHeader : Control, INamingContainer
        {
            private object _dataItem; 
            
            public virtual object DataItem 
            {
                get { return _dataItem; }
                set { _dataItem = value; }
            }
        }
    }
}

Demo.aspx

<my:GroupingRepeater id="MyGroupingRepeater" EnableViewState="false" runat="server"> 
    <GroupTemplate> 
        <h2><%# DataBinder.Eval(Container.DataItem, "Datum", "{0:y}")) %></h2>
    </GroupTemplate> 
    <ItemTemplate> 
        - <%# DataBinder.Eval(Container.DataItem, "Datum") %><br/>
    </ItemTemplate>
</my:GroupingRepeater>

Demo.aspx.cs

...

private void Page_Init(object sender, System.EventArgs e)
{ 
    DataSet ds = MyDataAccess.LoadData(...);
    MyGroupingRepeater.DataSource = ds.Tables[0]; 
    MyGroupingRepeater.Comparer = new MyComparer(); 
    MyGroupingRepeater.DataBind(); 
} 

...

private class MyComparer : System.Collections.IComparer 
{ 
    public int Compare(object x, object y) 
    { 
        if (x == null || y == null) 
            return -1; 

        DataRowView row1 = x as DataRowView; 
        DataRowView row2 = y as DataRowView; 
         
        DateTime date1 = Convert.ToDateTime(row1.Row[0]);
        DateTime date2 = Convert.ToDateTime(row2.Row[0]);
         
        if ((datum1.Year == datum2.Year) && (datum1.Month == datum2.Month))
            return 0;
        else
            return -1;
    } 
} 
Shrnutí
  1. Celé je to o porovnávání příslušné hodnoty ve dvou po sobě následujících řádcích, k čemuž můžeme využít IComparer.
  2. Toto porovnávání děláme po vytvoření každého řádku, v události ItemCreated. Událost ItemCrated repeateru je volána před vložením právě vytvořené položky do kolekce Controls, proto můžeme snadno vložit náš groupovací nadpis před tuto položku pouhým Controls.Add(groupHeader);
  3. Výše uvedený postup není úplně 100% čistý a je určen pouze pro jednoduché scénáře. Zasahujeme totiž do control-tree repeateru v průběhu jeho výstavby, a spoléháme se na vlastnost item.DataItem, která je naplněna pouze po data-bindingu. Nemůžeme tak využít ViewState repeateru, ale musíme ho plnit daty znovu při každém requestu – a to pokud možno dokonce už ve fázi Init (čím později budeme plnit, tím více problémů nás může potkat – může se nám totiž lišit control-tree jednotlivých requestů).
  4. Pokud bychom chtěli vytvořit opravdový univerzální korektní repeater s groupováním a funkčním ViewState, pak bychom potřebovali lépe pracovat s control-tree vzhledem k control-lifecycle. Nové řádky bychom museli zařazovat do kolekce Items, nikoliv přímo Controls, atp. Samotný Repeater není pro odvození takového controlu úplně ideální a lepší bude vytvořit úplně nový control. Pro nasměrování viz též článek Vlastní primitivní Repeater – templated data-bound control.
  5. Obdobné principy bychom mohli použít i pro groupování jiných seznamových controlů – DataGrid, GridView, CheckBoxList, RadioButtonList, DataList, atp.

Pozor na transakce – rollback není vždy automatický!

Nezkušeného vývojáře, ale jak jsem se bohužel na vlastní oči mnohdy přesvědčil – mnohdy i velmi zkušeného vývojáře, dokáže zdrcujícím způsobem překvapit výsledek následujícího jednoduchého příkladu:

CREATE TABLE TransactionTestTable
(
    TransactionTestID int IDENTITY PRIMARY KEY,
    TransactionText nvarchar(100)
)
GO

CREATE PROCEDURE TransactionTest
AS
    BEGIN TRANSACTION
        
        -- Korektní SQL statement uvnitř transakce
        INSERT INTO TransactionTestTable(TransactionText) VALUES('První insert')

        -- Simulace chyby uvnitř transakce (do identity-column nelze zapisovat)
        INSERT INTO TransactionTestTable(TransactionTestID, TransactionText) VALUES(1, 'Druhý insert')

        -- Korektní SQL statement uvnitř transakce, po chybě
        INSERT INTO TransactionTestTable(TransactionText) VALUES('Třetí insert')

    COMMIT TRANSACTION
GO

EXEC TransactionTest
SELECT * FROM TransactionTestTable
GO

…spuštěná transakce (zde pouze pro formu zabalená do procedury TransactionTest) selže – druhý INSERT zfailuje, protože nemůže zapisovat přímo do identity-column. To zde není podstatné, je to pouze simulace pro zfailování statementu uvnitř transakce.

Velkým překvapením mnohých však je, že v tabulce TransactionTestTable budou po skončení transakce následující data:

TransactionTestID TransactionText
1 První insert
2 Třetí insert

Zfailováním druhého statementu nedojde k automatickému rollbacku celé transakce!
Run-time chyby statementů rollbackují automaticky pouze statement, který chybu způsobil!

Pokud chceme, aby run-time chyba automaticky zrollbackovala celou transakci, musíme nastavit SET XACT_ABORT ON:

CREATE PROCEDURE TransactionTest
AS
    SET XACT_ABORT ON
    BEGIN TRANSACTION
        
        -- Korektní SQL statement uvnitř transakce
        INSERT INTO TransactionTestTable(TransactionName) VALUES('První insert')

        -- Chybný SQL statement uvnitř transakce, do identity-column nelze zapisovat
        INSERT INTO TransactionTestTable(TransactionTestID, TransactionName) VALUES(1, 'Druhý insert')

        -- Korektní SQL statement uvnitř transakce, po chybě
        INSERT INTO TransactionTestTable(TransactionName) VALUES('Třetí insert')

    COMMIT TRANSACTION
GO

Jemněji než automatický rollback pomocí SET XACT_ABORT ON lze v praxi použít ošetření chyb pomocí TRY..CATCH (i když i to má svá úskalí):

USE AdventureWorks;
GO
BEGIN TRANSACTION;

BEGIN TRY
   -- Generate a constraint violation error.
   DELETE FROM Production.Product
      WHERE ProductID = 980;
END TRY
BEGIN CATCH
   SELECT 
      ERROR_NUMBER() AS ErrorNumber,
      ERROR_SEVERITY() AS ErrorSeverity,
      ERROR_STATE() as ErrorState,
      ERROR_PROCEDURE() as ErrorProcedure,
      ERROR_LINE() as ErrorLine,
      ERROR_MESSAGE() as ErrorMessage;

   IF @@TRANCOUNT > 0
      ROLLBACK TRANSACTION;
END CATCH;

IF @@TRANCOUNT > 0
   COMMIT TRANSACTION;
GO

…případně „postaru“ testovat @@ERROR <> 0 po každém statementu.

InstallShield: Chyba -5006 nebo -5009, Kernel\CABFile.cpp, SetupDLL\SetupDLL.cpp

V průběhu jedné instalace řízené InstallShieldem mi lehnul počítač a dostal se do inkonzistentního stavu spočívajícího v tom, že opakovaný pokus o instalaci okamžitě (už při startu InstallShieldu) spadnul s chybou -5006. V podrobnostech chyby pak bylo cosi o souborech Kernel\CABFile.cpp a SetupDLL\SetupDLL.cpp, něco jako:

Error Code: -5006 : 0x80030005 
Error Information: 
>Kernel\CABFile.cpp (758)
>SetupDLL\SetupDLL.cpp (1182) 
PAPP: ... 
PVENDOR: ...
PGUID: ...

Jak jsem následně zjistil, na tuto chybu padají veškeré další instalace InstallShieldu (nebo chybu -5009 s problémy u stejných souborů).

Nakonec pomohlo zlikvidovat složku %ProgramFiles%\Common Files\InstallShield\ (nebo raději jen přejmenovat). Následující installer si tuto složku InstallShieldu založil znovu a vše chodilo správně.

Je to taková střelba od boku, nicméně přesně takhle to pomohlo, a pokud to uděláte jen jako přejmenování, abyste to případně mohli vrátit do původního stavu, tak to určitě za pokus stojí.

dcgpofix – Rebuild Default domain policy a Default domain controllers policy

dcgpofix je „Default Group Policy Restore Utility“, utilita Windows Serveru 2003, která umožňuje obnovit Default Domain Security Policy (Zásady zabezpečení domény) a Default Domain Controllers Security Policy (Zásady zabezpečení řadičů domény) do výchozího stavu po instalaci, a to nejenom pokud si rozhážeme nastavení do neudržitelného stavu, ale i v případě, že jsou tyto GPO poškozeny, nebo smazány.

Základní syntaxe je

dcgpofix [/ignoreschema][/target: {domain | dc | both}]

Viz též podrobnější info o dcgpofix od Microsoftu.

Pro Windows 2000 existuje též postup, i když mnohem drsnější.

Vlastní textová reprezentace výčtového typu enum – alternativa ToString()

K výčtovému typu enum nelze konvenčními metodami udělat vlastní textovou reprezentaci, není jak overridovat metodu ToString(). Pokud chceme každé hodnotě přiřadit pouze jedinou „user-friendly“ textovou reprezentaci, můžeme využít atributu DescriptionAttribute:

public enum StavZakazky
{
    [Description("Nedefinován")]
    Nedefinovan,

    [Description("Vytištěno")]
    TiskHotovo
}

public static class EnumExt
{
    public static string GetDescription(Type enumType, object hodnota)
    {
        string strRet = "<na>";
        try
        {
            System.Reflection.FieldInfo objInfo = enumType.GetField(Enum.GetName(enumType, hodnota));

            System.ComponentModel.DescriptionAttribute objDescription =
                (System.ComponentModel.DescriptionAttribute)objInfo.GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), true)[0];

            strRet = objDescription.Description;
        }
        catch(Exception)
        {
            // chybí description
        }
        return strRet;
    }
}

Interní: Implementováno jako Havit.EnumExt.GetDescription().

Service Broker: Notifikace .NET aplikace z SQL serveru (events, queries, messages)

SQL Server 2005 přichází s novou funkčností tzv. Service Brokera – komplexní message-based komunikační platformy, s frontami a dalšími prvky potřebnými pro stavbu robustních SOA aplikací (Service Oriented Architecture).

Podrobněji o Service Brokeru viz SQL Books Online, pro nás je pro tuto chvíli podstatné, že prostřednictvím Service Brokera lze implementovat rozumnou podobu notifikací z SQL serveru do .NET aplikací, kdy jsou .NET aplikace SQL serverem informovány o určitých událostech (např. různých CREATE, DROP, ALTER DDL příkazech), o změnách dat (tzv. query notifications) či jakýchkoliv jiných zprávách, které si sami iniciujeme (např. z trigerů, atp.). Jak dále uvidíme, dotnetovské SqlCacheDependency či SqlDependency nejsou nic jiného, než konkrétním využitím Service Brokera.

Základní principy

Podívejme se nejprve na základní principy práce Service Brokera:

  1. Komunikace prostřednictvím Service Brokera je založena na zprávách (MESSAGE). Každá jednotlivá zpráva představuje příslušnou událost, příkaz, notifikaci, prostě atomickou komunikační jednotku.
  2. Zprávy jsou uchovávány ve frontách (QUEUE). Fronta je určitým bufferem mezi odesílatelem a příjemcem zprávy.
  3. Formální požadavky, které musí zpráva splňovat (např. XML schema), určuje typ zprávy (MESSAGE TYPE).
  4. Zprávy se posílají v konverzaci (DIALOG CONVERSATION) mezi dvěma endpointy, které představují služby (SERVICE). Cesty Mezi různými instancemi SQL serveru lze zprávy předávat pomocíroutů (ROUTE).
  5. Formální požadavky na konverzaci/dialog určuje CONTRACT, který mj. určuje typ zpráv vyměňovaných mezi službami a směr, kterým se posílají.
  6. Zpráva může mimo explicitní konverzace vzniknout např. i událostí (EVENT) na straně SQL Serveru (např. DDL události CREATE, ALTER, DROP, nebo trace události, jak je známe z Profileru), nebo prostřednictvím sledování aktualizací dat (Query Notification), kdy SQL Server sleduje výsledky určitého SQL dotazu a oznámí jejich změnu (klasicky využíváno pro expiraci datové cache ASP.NET).
  7. Podstatné je, že Service Broker sám neinicializuje komunikaci mimo SQL Server, do .NET aplikace, naopak .NET aplikace si musí zprávu sama vyzvednout z fronty (RECEIVE), resp. může využít konstrukce WAITFOR, čímž v případě prázdné fronty pasivně vyčkává, než se zpráva objeví.

Výše uvedené není ani zdaleka vyčerpávajícím popisem fungování Service Brokeru, pro naše účely však postačuje jako úvod do problematiky.

Pro dále popisovanou funkčnost je potřeba mít Service Brokera na databázi zapnutého:

ALTER DATABASE AdventureWorks SET ENABLE_BROKER

Query Notifications – SqlNotificationRequest, SqlDependency, SqlCacheDependency

Nejběžnějším způsobem využití Service Brokera v .NET aplikaci jsou Query Notification Services – novinka SQL Serveru 2005 spočívají v možnosti vyžádat (subscribe) sledování výsledku určitého SQL dotazu (query). V případě změny pak dojde k vytvoření příslušné zprávy (message), jejímu uložení do fronty (queue), odkud si ji z naší .NET aplikace vyzvedneme a o aktualizaci na straně SQL Serveru se tak dozvíme (a může tak například dojít k vyřazení určitých dat z cache atp.).

Základní principy Query Notifications

  1. Query Notifications nelze aktivovat z T-SQL, ani CLR kódu hostovaného v SQL Serveru. Query Notifications lze aktivovat pouze prostřednictvím klientské aplikace, v případě .NET Frameworku prostřednictvím třídy SqlNotificationRequest (vlastnost SqlCommand.Notification).
  2. SqlNotificationRequest je svázán s příkazem (SqlCommand), kterému je nastaven a teprve vykonáním tohoto příkazu se provede příslušná subscription QN na straně SQL serveru.
  3. SQL Server sleduje pouze první změnu výsledku dotazu, tím QN končí a případný další notification request je potřeba iniciovat novým vykonáním příkazu.
  4. V žádném případě se nejedná o komunikaci iniciovanou SQL Serverem, nýbrž si musíme z příslušné fronty sami zprávu o aktualizaci vyzvednout, resp. si obsah fronty průběžně hlídat.
  5. V případě SqlDependency a SqlCacheDependency za nás vyzvednutí zprávy z příslušné fronty Service Brokera zajišťuje samotný .NET Framework prostřednictvím opakovaného volání příkazu WAITFOR (RECEIVE …) TIMEOUT v kontinuálně otevřené samostatné SqlConnection ze samostatného threadu naší aplikace. Přesvědčit se o tom můžete v SQL Profileru.

Ilustrační příklad SqlDependency:

 class Program
    {
        static void Main(string[] args)
        {
            const string connString = "Server=localhost;Database=AdventureWorks;Integrated Security=true;";
            using (SqlConnection conn = new SqlConnection(connString))
            {
                conn.Open();

                SqlCommand cmd = new SqlCommand("SELECT Bonus FROM Sales.SalesPerson");
                cmd.Connection = conn;

                SqlDependency dependency = new SqlDependency(cmd);
                dependency.OnChange += new OnChangeEventHandler(dependency_OnChange);
                SqlDependency.Start(connString);

                cmd.ExecuteNonQuery();

                Console.ReadLine();
            }

        }

        static void dependency_OnChange(object sender, SqlNotificationEventArgs e)
        {
            Console.WriteLine("Bonus changed...");
        }
    }

SqlNotificationRequest

Pokud nám nestačí vyšší programátoská abstrakce SqlDependency či SqlCacheDependency a potřebujeme řídit Query Notifications podrobněji, můžeme využít třídu SqlNotificationRequest, resp. instanční vlastnost SqlCommand.Notification, kam instanci třídy SqlNotificationRequest přiřadíme a zajistíme tak subscribování daného příkazu do Query Notifications Service. Na rozdíl od SqlDependency pak ale musíme sami zajistit vyzvedávání zpráv z příslušné fronty a jejich zpracování (viz níže).

Požadavky na sledovaný SQL dotaz

Dotaz, ke kterému chceme QueryNotification aktivovat, musí splňovat určité nemalé restrikce:

  • jména tabulek musí být uváděna včetně schématu, tedy Sales.SalesPerson, dbo.MojeTabulka, atp.
  • nelze použít SELECT *, vždy musíme udělat výčet sloupců
  • nelze použít agregační funkce
  • nelze používat subqueries, outer-joins, self-joins

Podrobný výčet omezení viz např. http://msdn2.microsoft.com/en-US/library/ms181122.aspx.

Event Notifications

Dalším typem zpráv, které můžeme od SQL Serveru prostřednictvím Service Brokera odebírat, jsou tzv. Event Notifications. V zásadě se jedná o DDL a trace události na straně serveru, ať už na úrovni databáze nebo serveru jako celku. Např. CREATE_DATABASE, ALTER_PROCEDURE, DROP_ASSEMBLY, Audit_Login, SP_Recompile, atp. atp. Události jsou hiearchicky uspořádány a lze se přihlásit i k odběru celé skupiny najednou.

Na rozdíl od Query Notifications, kdy se nám příslušné prvky Service Brokera vytvářely převážně automaticky, zde již musíme provést základní inicializaci ručně. Na rozdíl od Query Notifications se Event Notifications celé řídí prostřednictvím T-SQL příkazů. Jednoduchý příklad by mohl vypadat nějak takto:

CREATE QUEUE MyNotifyQueue;

CREATE SERVICE MyNotifyService
    ON QUEUE MyNotifyQueue
    (
    [http://schemas.microsoft.com/SQL/Notifications/PostEventNotification]
    )


CREATE ROUTE MyNotifyRoute WITH SERVICE_NAME = 'MyNotifyService', ADDRESS = 'LOCAL';

CREATE EVENT NOTIFICATION Notify_AlterProcedure
    ON DATABASE
    FOR ALTER_PROCEDURE
    TO SERVICE 'MyNotifyService', 'current database'

MESSAGE TYPE pro události je již v systému pod názvem PostEventNotification připraven, vytvoříme tedy jen frontu (CREATE QUEUE), službu (CREATE SERVICE), routu (CREATE ROUTE) a pak už samotnou event-notifikace (CREATE EVENT NOTIFICATION). Tím je na straně SQL Serveru vše připraveno a nyní už můžeme jen vyzvedávat zprávy na straně .NET aplikace:

  class Program
    {
        static bool done = false;

        static void Main(string[] args)
        {
            Thread t = new Thread(ReceiveEvent);
            t.Start();
            
            while (!done)
            {
                Thread.Sleep(1000);
                Console.Write(".");
            }

            Console.ReadLine();
        }

        static void ReceiveEvent()
        {
            const string connString = "Server=localhost;Database=AdventureWorks;Integrated Security=true;";

            using (SqlConnection conn = new SqlConnection(connString))
            {
                conn.Open();

                SqlCommand cmd = new SqlCommand();
                cmd.Connection = conn;
                cmd.CommandText = "WAITFOR (RECEIVE TOP(1) * FROM MyNotifyQueue), TIMEOUT @Timeout";
                cmd.Parameters.AddWithValue("@Timeout", 60000); // 60s

                do
                {
                    SqlDataReader reader = cmd.ExecuteReader();
                    if (reader.Read())
                    {
                        OnEvent();
                    }
                    done = true;
                }
                while (!done);
            }
        }

        static void OnEvent()
        {
            Console.WriteLine("Event received...");
        }
    }

Obdobně jako interní implementace SqlDependency zde v samostatném threadu provádíme průběžný polling WAITFOR(RECEIVE …) s příslušným timeoutem, zatímco v hlavním vlákně běží aplikace dál (obsluha UI, atp.). Receiving-vláknu bychom také měli nastavit vlastnost IsBackground na true, aby při skončení hlavního vlákna došlo k ukončení tohoto pollingu.

Vlastní zprávy odesíláné z SQL Serveru (např. z triggerů)

Dalším zdrojem zpráv předávaných prostřednictvím Service Brokera mohou být i zprávy, které sami vytvoříme, jejichž odeslání sami inicializujeme – např. z různých triggerů (DML i DDL), nebo uložených procedur.

Pro tuto chvíli přesahuje podrobný popis zamýšlený rozsah tohoto článku, nicméně jak je už z výše uvedeného zřejmé, nejde opět o nic jiného než o inicializaci jednotlivých prvků Service Brokera (vše T-SQL) a dále posílání zpráv prostřednictvím T-SQL příkazů BEGIN DIALOG a SEND. Samotná .NET aplikace by pak byla stejný princip, jak u Event Notifikacions, jen formát zpráv se bude lišit podle toho, jak si ho zadefinujeme (zprávy mohou být i prázdné, nebo plain-text).

Mnohé příklady odesílání zpráv přes Service Brokera viz SQL Books Online. Možná tento článek rozšířím někdy později.

load needed DLLs for kernel

Server mně dnes obdařil krásnou hláškou:

Windows NT could not start because of an error in the software.
Please report this problem as :
load needed DLLs for kernel.
Please contact your support person to report this problem.

Problém je v zásadě vždy ten, že je problém se souborem hal.dll, ntoskrnl.exe nebo oběma (nachází se oba v <systemroot>\system32\).

V podstatě je potřeba tyto soubory dát do pořádku, tedy buď přes Recovery console, expandem z instalačního disku, atp.

Mě se osvědčilo Ultimate Boot CD (UBCD), které mělo na sobě dokonce ovladače na IH7R RAID, takže jsem si odpustil i šachování s disketami atp.

Nějak se mi vysypala struktura disku, složka System32 končila písmenem M, další soubory nebyly. Problém se mi podařilo vyřešit už samotným

chkdsk c: /f

… z Ultimate Boot CD.

Viz též Microsoftí Q164448.

IDisposable – ResourceWrapper design pattern

IDisposable je interface, který předepisuje implementovat jednu jedinou metodu:

public interface IDisposable
{
    void Dispose ();
}

Metoda Dispose() slouží k uvolnění unmanaged resources (file handles, window handles, database connections, atp.) a celé rozhraní IDisposable je tak určeno k implementaci tzv. Explicit Resource Managementu, tedy chceme dát programátorům používajícím naší třídu možnost explicitního úklidu unmanaged resources v okamžiku, kdy instanci naší třídy přestanou dále potřebovat:

Resource r = new Resource();
try
{
  r.DoSomething();
}
finally
{
  if (r != null)
  {
    ((IDisposable)r).Dispose();
  }
}

Tento zápis lze v C# ekvivalentně zkrátit pomocí statementu using:

 

using (Resource r = new Resource())
{
    r.DoSomething();
}

Statement using je určen pro přehlednou práci s IDisposable třídami a ve skutečnosti to není nic jiného, než výše uvedený try-finally blok.

ResourceWrapper design pattern

Microsoft v .NET Frameworku sám většinou používá a pro explicitní management resources doporučuje tzv. ResourceWrapper design pattern. Jeho funkčnost vychází z následujích požadavků:

  1. Primárně by měli programátoři používající naší třídu uvolňovat unamanaged resources právě prostřednictvím volání metody IDisposable.Dispose(), popř. pomocí další metody očekávanějšího jména stejné funkčnosti – např. file.Close().
  2. Pokud nedejbože nepozorný programátor zapomene explicitně zavolat úklid metodou Dispose(), postaráme se o úklid a uvolnění resources alespoň v destruktoru třídy (C# destruktory jsou ve skutečnosti překládány do metody Finalize()).

Důležité je však zdůraznit, že zatímco metodu Dispose() můžeme volat ihned, jakmile přestaneme příslušnou instanci potřebovat, a tedy dojde k uvolnění zdrojů hned, destruktor je volaný až po garbage collection, která může proběhnout taky až za velmi dlouhou dobu a do té doby jsou všechny unmanaged resources neuklizeny, např. tedy blokujeme přístup k nějakému souboru, s nímž zbytečně dlouho nemůže nikdo jiný pracovat.

Dále je potřeba si uvědomit, jak Garbage Collector pracuje s instancemi tříd, které mají destructor (metodu Finalize):

  1. Runtime si udržuje seznam všech existujících objektů, které budou při svém odstraňování potřebovat finalizaci (mají destruktor) – tzv. Finalization queue.
  2. Garbage Collector při úklid prochází graf dostupných objektů od objektů kořenových (statické globální proměnné, lokální proměnné v zásobníku, registry procesoru) a objekty, které jsou nedostupné, ty uklízí – uvolňuje je z paměti.
  3. Pokud však Garbage Collector má uklidit objekt, který potřebuje finalizaci (je ve Finalization Queue, má destruktor), pak je neuvolní hned, nýbrž je vyjme z Finalization Queue a vytvoří jim záznam v tzv. Freachable queue (finalization-reachable). Freachable queue je součástí kořenových objektových struktur a objekty v ní umístěné jsou tak znovu dostupné (reachable) – Garbage Collector je neuklidí.
  4. Freachable queue existuje proto, že jednotlivé destuktory mohou trvat i velmi dlouho a není tak vhodné, aby volání destruktorů zdržovalo samotný běh Garbage Collectoru. Proto GC hází objekty, které potřebují finalizaci do Freachable queue a sám běží dál/skončí. Frontu Freachable obsluhuje samostatný finalizační thread, který z ní vybírá jednotlivé objekty a provádí jejich finalizaci (volá metodu Finalize, tj. destruktory).
  5. Provedením metody Finalize (destruktoru) finalizační thread vyjme objekt z Freachable queue a ten už tak není ani dostupný přes tuto frontu, ani není v hlavním seznamu objektů pro finalizaci, a proto může být Garbage Collectorem jako nedostupný konečně uklizen.
  6. Protože se objekty s destruktory stávají po dobu umístění v Freachable queue opět dostupnými, stávají se tak dostupnými i všechny objekty, které ten samotný objekt refrencuje, tj. celý podstrom objektů, přestože ony samy třeba destruktory nemají. Řádné uvolnění paměti je tak odsouváno, oproti běžnému průběhu GC nad objekty bez finalizace (bez destruktorů).
  7. Poslední záludností Freachable queue je, že objekty v ní nejsou umístěny v pořadí, v jakém byly založeny, ale v pořadí, v jakém se k jejich zpracování dostal Garbage Collector, tedy prakticky v pořadí nijak nedefinovaném. V našem destruktoru tedy nevíme, jestli destruktory objektů, které sami referencujeme, už proběhly (objekty byly ve frontě před námi), nebo jestli teprve proběhnou (objekty jsou ve frontě za námi).

Co z toho vyplývá pro implementaci:

  1. Pokud uživatel uvolní prostředky explicitně voláním metody Dispose(), pak chceme potlačit volání pojistné finalizace (destruktoru), protože finalizace odsouvá uvolnění objektu i objektů jím referencovaných. To budeme dělat použitím metody GC.SuppressFinalize(), která vyjímá objekt z Finalization Queue a objekt tak není při garbage collection už přesouván do Freachable queue, nýbrž je uvolněn ihned.
  2. Zatímco v metodě Dispose() můžeme uklízet i další námi vlastněné IDisposable objekty, protože víme, že jejich úklid ještě neproběhl (pokud je máme private/protected jen pro sebe), tak v destruktoru to možné není. Destruktory jsou totiž volány v nedefinovaném pořadí a úklid námi vlastněných podřízených IDisposable objektů už mohl proběhnout před námi.

Jak to tedy všechno uděláme:

  1. Abychom byla naše třída imunní vůči vícenásobnému volání Dispose(), a abychom sami pro další kontroly věděli, jestli už disposování proběhlo, zavedeme si proměnnou bool disposed:
    private bool disposed = false;
    

  2. Protože chceme mít implementaci uvolňování prostředků na jednom místě, a protože chceme umožnit svým případným potomkům rozšířit Dispose(), zavedeme si metodu protected virtual void Dispose(bool disposing), kde parametr disposing identifikuje, jestli samotné volání této implementační metody proběhlo z explicitního Dispose() a můžeme tak uvolňovat i další vlastněné IDisposable členy (disposing == true), nebo jestli volání proběhlo z destruktoru a můžeme uklízet jen sami sebe (disposing == false).
protected virtual void Dispose(bool disposing)
{
  // Dispose() má být imunní vůči vícenásobnému volání
  if(!this.disposed)
  {
    // Pokud jsme volání z metody Dispose(),
    // můžeme uvolnit i vlastněné IDisposable prvky. Z destruktoru ne.
    if(disposing)
    {
      // Uvolňujeme tedy tzv. managed resources.
      component.Dispose();
    }
         
    // ...v každém případě však uvolňujeme unmanaged resources 
    CloseHandle(handle);
    handle = IntPtr.Zero;            
    disposed = true; 
  }
}

3. Do samotné metody IDisposable.Dispose() pak dáme volání této implementace (s parametrem disposing = true) a dále vyřadíme objekt z Finalization Queue, tedy potlačíme jeho další finalizaci. volání destruktoru (a umožníme tak rychlý úklid objektu Garbage Collectorem):

public void Dispose()   // popř. explicitně IDisposable.Dispose()
{
  Dispose(true);
  GC.SuppressFinalize(this);
}

4. Protože chceme zajistit uvolnění unmanaged prostředků i v případě, že programátor zapomene zavolat Dispose(), zavoláme metodu Dispose() i zdestruktoru:

~ResourceWrapper()
{
   Dispose(false);
}

5. Samotnou funkčnost třídy, která má být dostupná jen před disposováním objektu, pak podmíníme (!disposed):

public void DoSomething()
{
  if (disposed)
  {
    throw new ObjectDisposedException("ResourceWrapper");
  }

  // následuje vlastní funkčnost metody

}

6. Nakonec můžeme přidat ještě metodu s domain-specific názvem pro dispose:

public void Close()
{
  Dispose();
}
Související

IE7: Vlastní vyhledávání – Search Provider – OpenSearch

O možnosti přidání vlastního vyhledávání do Internet Exploreru 7 psal už dávno Michal Altair Valášek v článku „Jak přidat vyhledávání na stránkách do IE 7.0„, nebudu ho tu tedy opakovat a úvodní informace najde každý tam.

Doplňuji pouze, že existuje také možnost vytvoření linku, který daný search provider „nainstaluje“, resp. existuje JScript funkce AddSearchProvider, která IE řekne, že má nějaký search provider nainstalovat:

&lt;a href=&quot;#&quot; onclick=&quot;window.external.AddSearchProvider('/mySearch.xml')&quot;&gt;My Search&lt;/a&gt;

Další související články a odkazy: