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