ASP.NET problēma ar AutoPostBack

Diemžēl sastapta kārtējā problēma ASP.NET iekšienē, kam būtu jābūt ļoti plaši izplatītai, tik nez kāpēc Microsoft nav uzskatījis par vajadzīgu to labot. Arī Connect sistēmā neatrodu, ka kas līdzīgs ir reģistrēts (būs jāpacenšas, taču nedomāju, ka tas ko mainīs).

Lai problēmu atkārtotu, var izveidot šādu lapu:

<asp:ScriptManager runat="server" EnablePartialRendering="true" />
<asp:UpdatePanel runat="server" UpdateMode="Always">
    <ContentTemplate>
        <asp:TextBox runat="server" ID="txt" AutoPostBack="true" />
        <asp:Button runat="server" ID="btn" Text="Submit" OnClick="Button_Click" />
        <asp:Label runat="server" ID="lbl" />
    </ContentTemplate>
</asp:UpdatePanel>

protected void Page_Load(object sender, EventArgs e)
{
    this.lbl.Text = this.Request["__EVENTTARGET"] == "txt" ? "textbox" : "other";
}

protected void Button_Click(object sender, EventArgs e)
{
    this.lbl.Text = "submit";
}

Problēma attiecas ne tikai uz ASP.NET Ajax, bet arī uz parastu ASP.NET lapu bez ScriptManager un UpdatePanel, taču tad tā parasti nav pamanāma, ja vien lapas apstrādē ir kaut neliela aizture.

Ja izmaina teksta lauka vērtību, tā tiek automātiski nosūtīta uz serveri un tas atgriež vērtību “textbox”. Savukārt, ja tiek nospiests uz pogas, tad serveris atgriež vērtību “submit”. Problēma slēpjas apstāklī, ka lietotājs, izmainot teksta lauka vērtību un uzreiz spiežot uz pogas, sagaida, ka tiks apstrādāta pogas nospiešana (piemēram, saglabāti dati). Šī brīža ASP.NET implementācijā tā nenotiek – uz serveri tiek nosūtīts AutoPostBack pieprasījums, bet lietotāja nospiestā poga paliek bez efekta. Ir pat tā, ka uz serveri reizēm var tikt nosūtīti abi divi, taču lietotājs redz, ka izpildās tikai viens.

Piedāvāju savu risinājumu (apkārtceļu) šai problēmai. Tam ir vairāki mīnusi, piemēram, ja no teksta lauka iziet ar Tab taustiņa palīdzību, uzreiz nonākot uz pogas, tad AutoPostBack nenostrādās (lai gan reāli uz pogas nospiests netiek). Tāpat to var apiet, nospiežot uz pogas un tad nobīdot peli nost, lai pogas klikšķis nenostrādā.

function shouldAbortRequest(eventTarget) {
    if (document.activeElement && document.activeElement.id != eventTarget
        && (document.activeElement.id || document.getElementById(eventTarget))) {
        var tagName = document.activeElement.tagName.toLowerCase();
        var type;
        if (tagName == "input")
            type = document.activeElement.type.toLowerCase();
        if (type == "submit" || type == "button" || tagName == "button" || tagName == "a")
            return true;
    }
    return false;
}

window.original__doPostBack = __doPostBack;
window.__doPostBack = function (eventTarget, eventArgument) {
    if (shouldAbortRequest(eventTarget))
        return;
    window.original__doPostBack(eventTarget, eventArgument);
};

Dotais JavaScript kods (jāievieto lapas beigās) aizvieto ASP.NET iebūvēto __doPostBack metodi, kas tiek izsaukta AutoPostBack gadījumā. Jaunā funkcija pārbauda, vai lietotājs nav nofokusējies uz kādu pogu un, ja tā ir, atceļ AutoPostBack pieprasījumu. Diemžēl tas nav ideāls risinājums, bet šobrīd vienīgais, kas lietotāju glābj no nepatīkamas situācijas, kur viņš ir nospiedis saglabāšanas pogu, bet reāli nekas saglabāts netiek.

ASP.NET StateManagedCollection nekorekta darbība

ASP.NET satur ļoti noderīgu kolekcijas klasi System.Web.UI.StateManagedCollection (MSDN dokumentācija). Galvenā tās priekšrocība pār parastajām kolekcijām ir tās spēja saglabāt visas izmaiņas ViewState, lai, lapai apstrādājot vairākus secīgus pieprasījumus, veiktās izmaiņas tiktu automātiski saglabātas.

Piemēram, ObjectDataSource parametru kolekcijas izmanto šo kolekciju, lai gadījumos, kad parametri tiek mainīti (tiek pievienoti parametri vai mainītas to vērtības), šīs izmaiņas saglabātos ViewState tāpat kā ievades lauku vērtības.

Diemžēl šī kolekcija satur diezgan brangu kļūdu – tā nesaglabā ViewState datos izmaiņas situācijās, kad visa kolekcija tiek iztukšota. Piemēram, ja izmanto asp:Menu vadīklu un kāda pieprasījuma laikā iztīra kāda zara visus ierakstus. Pēc nākamā pieprasījuma šīe ieraksti atkal būs savās vietās it kā nekad nekas nebūtu noticis.

Šo kļūdu pieteicu Microsoft, lai gan cerība, ka tā tiks salabota, ir diezgan minimāla. Visdrīzāk tiks pateikts, ka, jā, kļūda ir, bet tā netiks labota, lai nenograutu eksistējošas sistēmas, kas uz šo kļūdu paļaujas (apzināti vai neapzināti).

Bet katrā ziņā, vismaz nosacīts apkārtceļs eksistē (izmantojams tikai savās atvasinātajās klasēs, tas nevar salabot esošās .NET klases).

#region [ Workaround for the bug when the original StateManagedCollection does not persist Clear() call ]

/*
* Workaround for the bug described here: https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=486953
*
* If Clear() was called StateManagedCollection saves all of the items in the ViewState. Unfortunately,
* if the collection remains empty when the state is saved, StateManagedCollection saves just a "null".
* When LoadViewState is called, _saveAll private field is not set as it should be.
*/

private static Func<StateManagedCollection, bool> _SaveAll;
private static object _SaveAllLock = new object();
private bool SaveAll
{
    get
    {
        if (_SaveAll == null)
        {
            lock (_SaveAllLock)
            {
                if (_SaveAll == null)
                {
                    var p = System.Linq.Expressions.Expression.Parameter(typeof(StateManagedCollection), "col");
                    var f = System.Linq.Expressions.Expression.Field(p, "_saveAll");
                    _SaveAll = System.Linq.Expressions.Expression.Lambda<Func<StateManagedCollection, bool>>(f, p).Compile();
                }
            }
        }
        return _SaveAll(this);
    }
}

private static Func<StateManagedCollection, object> _BaseSaveViewState;
private static object _BaseSaveViewStateLock = new object();

object IStateManager.SaveViewState()
{
    if (this.Count == 0 && this.SaveAll)
        return new Pair(new object[0], new int[0]);

    if (_BaseSaveViewState == null)
    {
        lock (_BaseSaveViewStateLock)
        {
            if (_BaseSaveViewState == null)
            {
                var p = System.Linq.Expressions.Expression.Parameter(typeof(StateManagedCollection), "col");
                var f = System.Linq.Expressions.Expression.Call(p, "System.Web.UI.IStateManager.SaveViewState", Type.EmptyTypes);
                _BaseSaveViewState = System.Linq.Expressions.Expression.Lambda<Func<StateManagedCollection, object>>(f, p).Compile();
            }
        }
    }
    return _BaseSaveViewState(this);
}

#endregion