Tag Archives: AJAX

WebForms: Když nefunguje asynchronní AutoPostBack na RadioButtonech

Pokud bojujete s nefunkčním AutoPostBack na asp:RadioButton v ASP.NET WebForms při asynchronním postbacku, pak je to způsobeno tím, že ASP.NET nerenderuje k RadioButtonu označenému jako Checked klientskou obsluhu události onclick. Zřejmě je to by-design ochrana proti vícenásobné obsluze změn, tedy aby se událost vykonala pouze na jednom z radiobuttonů ve skupině, ne na tom označovaném i odznačovaném.

Možnosti řešení jsou tedy dvě:

  1. Zahrnout i RadioButtons do UpdatePanelu, aby se přerenderovaly a obsluhy událostí správně přenastavily.
  2. Obskurní work-around, který vyrenderuje obsluhu na všech RB taky může být
CisloSmlouvyNoveRB.InputAttributes["checked"] = "true";

Health Monitoring a sledování chyb asynchronních postbacků (AJAX)

Health Monitoring je šikovný vestavěný mechanizmus ASP.NET pro sledování a hlášení problémových situací. Typicky je používán pro zápis výjimek aplikace do event-logu nebo jejich posílání mailem.

Slabinou Health Monitoringu je, že se neumí vypořádat s chybami vzniklými během asynchronních postbacků (AJAX) ani s chybami vzniklými v rámci webových služeb ASP.NET. Přesněji řečeno ASP.NET na tyto chyby nějak nepamatuje a neoznamuje je do web-events mechanizmu, na němž je Health Monitoring závislý.

Podívejme se nyní na cestu, jak doplnit do Health Monitoringu sledování chyb AJAXu, sledování chyb webových služeb ponechávám do samostatného článku. Jak již bylo naznačeno, jde o to, že potřebujeme doplnit chybějící oznamování chyb do web.events mechanizmu. Chyba vzniklá během asynchronního postbacku způsobí vyvolání události ScriptManager.AsyncPostBackError. Potřebujeme se tedy napojit na tuto událost s vlastní obsluhou a v ní chybu předat jako WebRequestErrorEvent.

AjaxHealthMonitoring control

Jedním z možných opakovaně použitelných elegantních řešení je vytvoření vlastního controlu, který se bude umisťovat do stránek stejně jako samotný ScriptManager, např. tedy v MasterPage. Uvádím zde kód kolegy Jirky Kandy, který se s tím vypořádal takto:

public class AjaxHealthMonitoring: Control
{
    #region OnInit
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);

        ScriptManager scriptManager = ScriptManager.GetCurrent(Page);
        if (scriptManager == null)
        {
            throw new InvalidOperationException("Ve stránce nebyl nalezen ScriptManager, který je controlem AjaxHealthMonitoring vyžadován.");
        }
        scriptManager.AsyncPostBackError += new EventHandler<AsyncPostBackErrorEventArgs>(ScriptManager_AsyncPostBackError);
    }
    #endregion

    #region ScriptManager_AsyncPostBackError
    /// <summary>
    /// Obsluha události AsyncPostBackError ScriptManageru. Zajistí vyvolání události health monitoringu.
    /// </summary>
    private void ScriptManager_AsyncPostBackError(object sender, AsyncPostBackErrorEventArgs e)
    {
        if (e.Exception != null)
        {
            new WebRequestErrorEventExt(e.Exception.Message, this, e.Exception).Raise();
        }
    }
    #endregion
}

Pro úplnost uvádím odvozenou podobou třídy WebRequestErrorEventExt:

public class WebRequestErrorEventExt : WebRequestErrorEvent
{
    public WebRequestErrorEventExt(string message, object eventSource, Exception exception)
        : base(message, eventSource, WebEventCodes.WebExtendedBase + 999, exception)
    {
    }
}

Control se pak používá ve stránce stejně jako ScriptManager (do výstupního HTML kódu nic nerenderuje):

<asp:ScriptManager ScriptMode="Release" AllowCustomErrorsRedirect="true" runat="server" />
<havit:AjaxHealthMonitoring runat="server" />

Pozor na UpdatePanel a unikátní názvy (ID) controlů

AJAXový UpdatePanel se chová zvláštně vůči logice naming-containerů, pokud se tedy potkáte s názvy (ID) controlů, nepůjde Vaše stránka zkompilovat. Stačí zkusit následujících jednoduchý snippet:

<asp:TextBox ID="SomethingTB" runat="server" />
<asp:Repeater ID="MyRepeater" runat="server">
  <ItemTemplate>
    <asp:UpdatePanel runat="server">
      <ContentTemplate>
        <asp:TextBox ID="SomethingTB" runat="server" />
      </ContentTemplate>
    </asp:UpdatePanel>
  </ItemTemplate>
</asp:Repeater>

…při kompilaci budete obšťastněni chybovými hláškami

D:\Development\UpdatePanelCompiler\Default.aspx(18,57): error CS0102: The type '_Default' already contains a definition for 'SomethingTB'
c:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\updatepanelcompiler\1c44388f\ad38e94\App_Web_r7xfjqxf.0.cs(231,59): error CS0111: Type 'ASP.default_aspx' already defines a member called '__BuildControlSomethingTB' with the same parameter types

Pozor na FileUpload v UpdatePanelu

Po prázdninové odmlce jsem tu zpět s další čerstvou zkušeností, kterou jsme udělali na jednom z našich projektů.

Story

Společné uživatelské rozhraní pro vytváření nových a editaci stávajících záznamů mělo části, které byly použitelné jen pro existující objekty, mj. i možnost připojení souborů pres control FileUpload. Části použitelné jen u existujících objektů byly skryty pomocí Visible=“fase“ a zobrazovány pomocí UpdatePanelu, který je zobrazil po uložení nového objektu.

Problém byl v tom, že u nově vytvořených objektů nebylo možné připojit soubory, zatímco u existujících objektů to bez problémů šlo. Ukázalo se, ze control FileUpload v postbacku žádný soubor nedostane (HasFile bylo false), přestože uživatel soubor do formuláře zadal. Stejně se to chovalo ve všech prohlížečích a Fiddler potvrdil, že se z klienta žádný soubor nepřenesl.

Primitivní testovací kód by mohl vypadat třeba takto:

<form id="form1" runat="server">
     <asp:ScriptManager EnablePartialRendering="false" runat="server" />
        <asp:UpdatePanel runat="server">
            <ContentTemplate>
                <asp:FileUpload ID="FileFU" Visible="false" runat="server" />
                <asp:Label ID="HasFileLb" runat="server" />
                <asp:Button ID="SaveBt" Text="Save" runat="server" />
            </ContentTemplate>
        </asp:UpdatePanel>
</form>
void SaveBt_Click(object sender, EventArgs e)
{
    FileFU.Visible = true;
    HasFileLb.Text = FileFU.HasFile.ToString();
}

Po chvilce ladění se ukázala příčina problému. V režimu editace existujícího objektu formulář již od prvního requestu renderoval control FileUpload, který si sám do elementu form doplňuje potřebný atribut enctype=“multipart/form-data“, zatímco v režimu založení nového objektu se control FileUpload renderoval až AJAXem z UpdatePanelu po uložení nového objektu. UpdatePanel však v DOM stránky vymění jen svoji část a element form zůstává nedotčen, bez atributu enctype.

Summary

Pozor na to, ze AJAXovy partial rendering pomoci UpdatePaneliu vymění pouze určitou část DOM stránky a nesmi se zapomínat na vztahy teto části se zbytkem stránky. Většinou jsou tyto vztahy zřejmé, záludnost s FileUpload a atributem enctype vsak může pozlobit.

Možným řešením je například nastavování hodnoty atributu z kódu už při prvním requestu, přestože control FileUpload ještě na stránce není:

this.Page.Form.Enctype = "multipart/form-data"

AjaxControlToolkit HoverMenuExtender – dočítání vyskakovacího obsahu AJAXem

Objevil jsem jednu nepříliš dokumentovanou vlastnost HoverMenuExtenderu z AjaxControlToolkitu – že umí „vyskakovací“ obsah dočítat pomocí AJAXového callbacku na server. Nastavení je snadné, použijí se property DynamicXyz a jen je potřeba vědět (což se ukázalo jako největší kámen úrazu), jak je to vlastně celé zamýšleno a jak má vypadat serverová metoda (WebService), která má dynamický content vracet:

&lt;asp:Label ID=&quot;TargetLb&quot; Text=&quot;Ukažte sem, já se dočtu a vyskočím!&quot; runat=&quot;server&quot; /&gt;
&lt;asp:Panel ID=&quot;PopupPanel&quot; Style=&quot;display: none;&quot; runat=&quot;server&quot;&gt; &lt;%-- display:none - aby se při načítání nepřesýpala obrazovka --%&gt;
    Statický obsah pop-upu.
    &lt;asp:Panel ID=&quot;DynamicPopupContent&quot; runat=&quot;server&quot; /&gt;
&lt;/asp:Panel&gt;
&lt;ajaxToolkit:HoverMenuExtender
    TargetControlID=&quot;TargetLb&quot;
    PopupControlID=&quot;PopupPanel&quot;
    DynamicServicePath=&quot;~/AjaxServices/MyService.asmx&quot;
    DynamicServiceMethod=&quot;GetPopupContent&quot;
    DynamicContextKey=&quot;Kontext, např. ID záznamu&quot;
    DynamicControlID=&quot;DynamicPopupContent&quot;
    runat=&quot;server&quot;
/&gt;

a služba musím mít signaturu „string DoSomething(string contextKey)“:

[WebService]
[ScriptService]
public class Sluzby : System.Web.Services.WebService
{
    [WebMethod]
    [ScriptMethod]
    public string GetPopupContext(string contextKey)
    {
        return &quot;Hello World &quot; + contextKey;
    }
}

Tip 1: Pokud má být celý pop-up tvořen jen dynamickým obsahem, můžete DynamicControlID nastavit na stejný control jako PopupControlID a nemusíte pak vnořovat žádný další Panel (nebo jiný control).

Tip 2: DynamicControlID nemusí být uvnitř PopupControlID, dynamický obsah můžete dočítat i do jiného místa stránky, i když to asi není moc běžné.

Tip 3: Metodu vracející dynamický obsah můžete umístit i přímo do stránky jako PageMethod, musí být pak statická a HoverMenuExtenderu se pak nenastavuje vlastnost DynamicServicePath.

Registrace klientských skriptů: ClientScriptManager vs. ScriptManager

ClientScriptManager

Třída ClientScriptManager je součástí .NET Frameworku od jeho vzniku. Její instance je běžně přístupná přes Page.ClientScript. Třída slouží k registraci klientských skriptů, které mají být ve stránce renderovány, a k další práci s klienskými skripty.
Tato třída neví nic o AJAXu a asynchronním postbacku, pokud zaregistrujeme do stránky nějaký klientský skript, bude vyrenderován pouze v případě prvního načtení stránky (GET) nebo v klasickém postbacku (POST). Pokud je skript registrován v asynchronním postbacku, do browseru se nedostane.

ScriptManager

Třída ScriptManager je součástí ASP.NET Ajax 1.0 rozšířující .NET Framework 2.0 nebo .NET Frameworku 3.5. Tato třída rovněž slouží k registaci klientských skriptů do stránky.
Metody pro registraci klienských skriptů jsou statické a disponují rozhaním pro pohodlnější použití. Skripty registrované při prvním načítání stránky (GET) a v klasickém postbacku (POST) jsou stejně jako v předchozím případě renderovány do stránky, skripty registrované v asynchronním postbacku MOHOU být předány do browseru uživatele.
Každá z registračních metod existuje ve dvou přetíženích, které se liší typem prvního parametru – Control vs. Page.
Například:

  • RegisterClientScriptBlock(Page, Type, String, String, Boolean)
    Skripty registrované touto metodou jsou do browseru předány v každém asynchronním postbacku.
  • RegisterClientScriptBlock(Control, Type, String, String, Boolean)
    Skripty registrované touto metodou v asynchronním postbacku jsou renderovány do stránky jen tehdy, pokud je renderován předaný control. Pokud tedy při asnychronním postbacku není control v update panelu nebo je v update panelu, který není renderován, potom není renderován ani registrovaný skript.

Další metody pro registraci klientských skriptů jsou:

  • RegisterClientScriptBlock – registruje blok kódu
  • RegisterClientScriptInclude – registruje externí soubor s klienských skriptem
  • RegisterClientScriptResource – registruje soubor s klienských skriptem z resources
  • RegisterOnSubmitStatement – registruje kód vykonaný před postbackem
  • RegisterStartupScript – registruje kód vykonaný během načtení stránky

Proč nejde udělat rozumný AjaxValidator?

Po půl dni snažení a bádání jsem dospěl k subjektivnímu závěru, že nelze udělat rozumný ASP.NET validátor založený na AJAXu. Přesněji řečeno bylo mým cílem vytvořit obdobu CustomValidatoru, který by měl běžný ServerValidate a navíc klientskou jscript část, která by pomocí AJAXového HTTP-requestu validovala hodnotu vůči tomuto ServerValidate.

Narazil jsem na následující zásadní překážky, které podle mě nelze snadno překonat:

  1. Současné ASP.NET validátory, resp. všechny jejich Microsoftí obslužné klientské skripty a API jsou nekompromisně synchronní. V okamžiku požadavku validace (obvykle před submitem stránky) jsou volány prosté validační funkce všech povolených validátorů a od těchto se očekává jen true/false dle výsledku validace. V současném konceptu validátorů nikde není rozumný prostor pro asynchronní operace či nějakého zásahu do průběhu validace. 
  2. Je prakticky nemožné rozumným způsobem provést synchronní AJAXové volání vůči serveru a stejnětak je nemožné rozumně toto synchronní volání simulovat nějakým blokováním threadu uvnitř validační funkce. Javascript nezná žádné sleep/wait/pause a veškeré snahy o jeho implementaci vždy končí v tupé smyčce zatěžující CPU ze 100% do okamžiku splnění nějaké podmínky (přijetí AJAXového callbacku, dosažení určitého času, naplnění čítače, atp.)

Pro současný koncept ASP.NET validátorů jsem dospěl k těmto závěrům:

  1. Protože je koncept založen především na jednorázové kontrole hodnot před submitem formuláře a má za účel tento submit povolit/zakázat, je oprávněně založen na synchronních operacích a pro jakékoliv dlouhotrvající operace či asynchronní volání zde není prostor a poměrně logicky se s nimi nepočítá.
  2. Asynchronní AJAXová validace vůči serveru by byla použitelná u konceptu průběžné validace, kdy by byly hodnoty kontrolovány ihned po jejich zadání (onchange) a takováto validace by díky svému zpoždění nemohla blokovat odeslání formuláře, spíše by sloužila jako proaktivní kontrola uživatelského zadání.
  3. Současný koncept validátorů připouští pouze zběsilá řešení se synchronizačním blokováním threadu v různých sleep smyčkách (byť s timeoutem), popř. „chováním nekompatibilní“ on-change implementace dle bodu 2.

Veškeré pokusy o implementace AjaxValidatoru, které jsem na netu viděl, trpěly jedním nebo několika nedostatky z výše uvedených a jejich praktická použitelnost se tak blíží nule (spíše se tak stávají přetěžovači serverů, než účinnými validátory).

Avšak! Pokud by se našel někdo, koho by napadlo, jak výše uvedená omezení účinně obejít a funkční AjaxValidator vytvořit, sem s myšlenkou a pochvala ho nemine… ;-)))

PS: Sám jsem dospěl k implementaci AjaxValidatoru, který měl nastavitelný Timeout a ve validační funkci po zavolání asynchronního AJAX requestu na ServerValidate čekal po dobu tohoto timeoutu na odezvu serveru (čekal = různé hnusné implementace zatěžujících smyček čekajících na nějaký příznak nebo timeout). Pokud do Timeoutu nebyl AJAXem výsledek od serveru získán, propustila klientská část validaci jako IsValid=true, a tak se to v případě submitu zvalidovalo na serveru, nebo v případě onchange validace dovalidovalo později asynchronně (až dorazil callback, tak se aktualizoval validátor). Výsledek mi přišel pro praxi nepoužitelný, už timeout 1s je pro ovládání webového formuláře nepříjemný, stránka nereaguje na odeslání formuláře okamžitě, proto jsem to celé zahodil… Můj poznatek je, že pokud validátorem chápeme to, co ASP.NET, tedy blokaci odeslání formuláře, tak přes AJAX cesta nevede, i kdyby to šlo bez toho cyklického pollingu s timeoutem. Leda by byla odezva v řádu milisekund, což při serverových validacích moc nehrozí (obvykle nějaký dotaz do DB na exists, atp.).

AJAX: Nefunkční validátory uvnitř UpdatePanelu

Microsoftu se podařil pozoruhodný kousek, standardní validátory ASP.NET 2.0 nefungují korektně uvnitř ajaxového UpdatePanelu – při změně obsahu panelu se původní validátory neodregistrují. Např. tak nefungují validátory v GridView editaci, ve wizzardech, atp. Obvykle vyskakuje krásná JScript chyba „null is null“.

Předchozí beta verze AJAXu to řešily upravenými verzemi validátorů, které korektně používaly ScriptManager ke své přeregistraci. V RTM verzi AJAXu však již tyto validátory nejsou a Microsoft se rozhodl, že je bude aktualizovat všem přes WindowsUpdate, nezávisle na AJAXu.

Ta horší zpráva je, že tak dosud neudělal, přestože AJAX už dávno releasoval.

…takže kdo nemá nervy čekat na opravené validátory, musí sám ručně (přes <tagMapping>) použít jejich aktualizované verze (nebo odtud), tak jak tomu bylo v betách.

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