$MyExpression: Argument, aneb jak na custom expressions (ExpressionBuilder)

Jistě jste si v ASP.NET 2.0 všimli nové vlastnosti, tedy používání expressions v markup-kódu. Vestavěny jsou tyto:

<%$ ConnectionStrings: Name %>
<%$ Resources: Glossary, Key %>
<%$ AppSettings: Key %>

…a my si ukážeme, jak lze jednoduše přidávat vlastní (uvádím pouze primitivní příklad, který má ukázat, že to jde, a kudy na to).

Vše je to v podstatě o tom, že vytvoříme třídu odvozenou od abstraktní báze ExpressionBuilder a implementujeme metodu GetCodeExpression(…) nebo metodu EvaluateExpression(…). Metda EvaluateExpression() vrací vyhodnocený výraz, zatímco metoda GetCodeExpression() vrací kód, který se má použít v přiřazení do property při kompilaci ASPX stránky.

Abychom vytvořený expression-builder použili v našem web, musíme ho ještě přihlásit ve web.configu.

Jak by tedy mohla taková nejjednoduší třída pro expression vypadat:

namespace MyNamespace
{
   [ExpressionPrefix( "MyExpression" )]
   public class MyExpressionBuilder : System.Web.Compilation.ExpressionBuilder
   {
      public override CodeExpression GetCodeExpression(
         BoundPropertyEntry entry,
         object parsedData,
         ExpressionBuilderContext context)
     {
         return new CodeSnippetExpression(entry.Expression);
     }
   }
}

ve web.config pak zavedeme takto:

<compilation debug="true">
      <expressionBuilders>
          <add expressionPrefix="MyExpression" type="Namespace.MyExpressionBuilder, MyAssembly"/>
      </expressionBuilders>
  </compilation>

a v kódu pak použijeme třeba takto:

<asp:Literal Text="<%$ MyExpression: MyClass.MyConst %>" runat="server" /> 
<asp:TextBox ID="PasswordTB" MaxLength="<%$ MyExpression: Uzivatel.Properties.Password.MaximumLength %>" runat="server" />
<asp:RegularExpressionValidator ValidationExpression=<%$ MyExpression: MyRegexPatterns.EmailStrict %>" ... />

Důležité je zdůrznit, že MyExpression v této podobě nedělá nic jiného, než že ASP.NET-compileru dosazuje do míst, kde máme MyExpression použit, příšlušný kód. Můžeme tedy postavit naprosto libovolný syntakticky korektní kus kódu, který je compileru předhozen za přiřazovací rovnítko obdobně jako by se jednalo o makro známé z jiných programovacích platforem, např.:

@__ctrl.MaxLength = ((int)(Uzivatel.Properties.Password.MaximumLength));

Mozilla FireFox: Nefunguje XHTML-validní image-map

Následující XHTML-validní image-map v Mozille nechodí:

<img id="reklama" src=".." width="200" height="600" alt="..." usemap="#mapa" />
<map id="mapa">
    <area shape="rect" coords="10,110,186,245" alt="..." href="..." target="_blank" >
</map>

Mozille nestačí <map id=“…“> nýbrž vyžaduje <map name=“…“>… :-(((

Demo: http://www.bernzilla.com/bugs/imagemap1.html

Rozdíl mezi COALESCE() a ISNULL()

1. COALESCE() je z ANSI/ISO standardu SQL32, kdežto ISNULL() je jen T-SQL rozšíření (Microsoft SQL Serveru).

2. ISNULL() má vlastní interpretaci, kdežto COALESCE() je pouze zkrácený zápis pro CASE strukturu a i se tak provádí:

CASE
    WHEN (expression1 IS NOT NULL) THEN expression1
    ...
    WHEN (expressionN IS NOT NULL) THEN expressionN
    ELSE NULL
END

3. Typ výsledku ISNULL() je vždy dle prvního parametru, typ výsledku COALESCE() odpovídá CASE struktuře, tj. pokouší se o maximální záběr ze všech parametrů (SQL Server Books Online: „Returns the highest precedence type from the set of types in result_expressions and the optional else_result_expression.„).

DECLARE @Test char(2)
SET @Test = NULL
SELECT ISNULL(@Test, 'abcde'), COALESCE(@Test, 'abcde')

Např. výše uvedený test vrací ‚ab‘, ‚abcde‘.

4. Vzhledem k převodu COALESCE() na CASE oproti vlastnímu provádění, je ISNULL() obvykle rychlejší.

5. ISNULL() pochopitelně bere pouze dva parametry, kdežto COALESCE() víc.

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

Vypnutí beeperu aneb jak udělat, aby počítač po změně hlasitosti nepípal

Beeper způsobuje pípání počítače z Windows, zejména na velmi nežádoucím místě – po změně hlasitosti ve standardním ovládání hlasitosti. Naštěstí lze beeper vypnout a to tímto postupem:

  • Spustit správce zařízení
  • V menu zobrazit vybrat Zobrazit skrytá zařízení
  • Najít Beeper v sekci Ovladače nepodporující technologii Plug and Play
  • Pravé tlačítko a vlastnosti beeperu
  • Kliknout tlačítko zastavit
  • Vybrat ze seznamu Typ položku Zakázáno
  • Potvrdit, že jsme si jistí a že chceme restartovat počítač

Postup ověřen ve Windows XP.

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().