Category Archives: ASP.NET

Detekce ztráty ViewState při použití SessionPageStatePersisteru

Pokud směřujete ViewState do Session pomocí SessionPageStatePersisteru, pak se dříve nebo později setkáte s problémem ztrát ViewState a s potřebou detekovat a řešit tuto situaci.

Ke ztrátě ViewState může dojít v následujících případech:

  • v případě zániku/vyčištění Session, zejména při jejím timeoutování při nečinosti uživatele – uživatel si například nechá dlouho otevřenou stránku, vyprší mu mezi tím session, následně provede postback a problém je na světě, došlý postback nemá na serveru už ViewState,
  • v případě ztráty InProc Session díky restartování aplikace (deployment nové verze, recyklace poolu, atp.)
  • v případě vypadnutí stránky z fronty SessionPageStatePersisteru – persister omezuje totiž počet uložených ViewState a při uložení dalšího nejstarší vypadávají. Výchozí délka fronty je 9, je možné ji přenastavit na větší ve web.configu elementem sessionPageState, atributem historySize
    • pokud si například uživatel otevře současně 10 oken aplikace, pak pokud se vrátí k prvnímu oknu a provede postback, opět chybí ViewState,
    • pokud uživatel používá tlačítko Zpět browseru, např. pokud máme na stránce grid a každý řádek má na sobě tlačítko Detail, jehož obsluha události dělá redirect na stránku s detaily. Takový uživatel pokud klikne na detail prvního řádku, pak Zpět, druhého řádku, Zpět, … kliknutím na detail desátého řádku už způsobený postback na serveru nemá svůj ViewState (devět ViewState detailů vytlačilo z fronty ViewState stránky s gridem) a problém je zase na světě.

Jak takovou situaci detekovat? …overridováním metody LoadPageStateFromPersistenceMedium() v našich stránkách (obvykle pomocí společného předka všech stránek, nějakého PageBase):

protected override object LoadPageStateFromPersistenceMedium()
{
    // base implementace vrací new Pair(pageStatePersister.ControlState, pageStatePersister.ViewState);
    object pageState = base.LoadPageStateFromPersistenceMedium();

    // ztrátu ViewState můžeme tedy detekovat jako (pair.Second == null)
    // metoda LoadPageStateFromPersistenceMedium se volá jen pro IsPostBack
    Pair pair = pageState as Pair;
    if ((pair != null) && (pair.Second == null))
    {
        Trace.Warn("ViewStateLost !!!");

        FormsAuthentication.RedirectToLoginPage();
    }

    return pageState;
}

Základní (base) implementace metody LoadPageStateFromPersistenceMedium() totiž nedělá celkem nic jiného, než že z použitého persisteru získá ControlState, ViewState a udělá z nich Pair.

Situaci můžeme řešit například tím, že uživatele požádáme o nové přihlášení, či provedeme redirect kamsi do rootu nebo sami na sebe. ViewState už nikde nevydolujeme, musíme z toho nějak vybruslit, aniž bychom uživatele obšťastnili výjimkou NullReferenceException, InvalidFormatException a jinými podobnými, které v takových situacích nastávají.

Pozor na Response.Redirect(), Response.End() a obsluhu výjimek

Jak myslíte, že dopadne následující příklad po kliknutí na tlačítko? (Stránka obsahuje jen label MyLabel a button MyButton)

public partial class _Default : System.Web.UI.Page 
{
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        MyLabel.Text = (string)Session["OK"];
    }

    void MyButton_Click(object sender, EventArgs e)
    {
        try
        {
            Session["OK"] = "ok";
            Response.Redirect("~/");
        }
        catch
        {
            Session["OK"] = "exception";
        }
    }
}

Mnohé z Vás asi překvapím, když řeknu, že do Session[„OK“] se uloží „exception“ a ten se v dalším requestu i zobrazí.

Response.Redirect(), resp. metoda Response.End(), kterou Redirect sám volá, totiž funguje tak, že vyvolá ve webové aplikaci interní výjimku (Thread.CurrentThread.Abort()), která je samotnou webovou aplikací zpracovávána tak, aby bylo dosaženo kýženého efektu, tj. aby se vykonávání kódu zastavilo v daném místě a další kód se nevykonal (resp. tato ThreadAbortException se na konci catch-bloků vyvolává znovu a jsou vykonány všechny příslušné catch/finally bloky a tedy např. i Page.OnUnload()).

Potíž však nastane v okamžiku, kdy sami obalíme volání Response.Redirect() či Response.End() zachytáváním výjimek a nespecifikujeme dostatečně typ výjimek, které chceme zachytávat. Pokud necháme chytat výjimky všechny, uvedeme jako typ Exception, pak se dočkáme nežádoucího efektu, kdy nám volání Response.Redirect()/End() způsobí vykonání obsluhy výjimky, blok catch.

Východiskem je tedy obsluhovat pouze specifické typy výjimek, tak, jak to ostatně obecné guidelines doporučují pro všechny situace.

Jednoduchý exception logging pomocí Trace/TraceSource mechanizmů .NET

Pokud se sháníte po jednoduchém mechanizmu, jak ve Vašich aplikacích logovat výjimky, pak zřejmě v .NET pro consolové/WinForm aplikace marně hledáte něco, jako je healthMonitoring pro ASP.NET, kde je připraven jednoduchý, ale mocný nástroj a lze pomocí pouhých několika řádků ve web.config souboru získat přehled o výjimkách ve Vaší aplikaci, např.:

<configuration>
    <system.web>
        <healthMonitoring enabled="true">
            <rules>
                <add name="Mail Notifications on All Errors" eventName="All Errors" provider="SimpleMailWebEventProvider" profile="Default"/>
            </rules>
            <providers>
                <add name="SimpleMailWebEventProvider" type="System.Web.Management.SimpleMailWebEventProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A" from="errors@havit.cz" to="errors@example.cz" subjectPrefix="MyApplication: " buffer="true" bufferMode="Notification" maxEventLength="4096" maxMessagesPerNotification="2"/>
            </providers>
        </healthMonitoring>
    </system.web>
</configuration>

…výše uvedená konfigurace Vám zajistí zasílání mailových notifikacích o neobsloužených výjimkách a chyách Vaší aplikace, jednoduché, prosté.

Jak na to u consolových a WinForm aplikací?

Pro consolové či WinForm aplikace nic jako healthMonitoring připraveno není, nicméně s pár řádky kódu lze dosáhnout obdobného efektu a dokonce u toho využít široké možnosti pro tracing v .NET 2.0. Základní myšlenka je prostá:

  • .NET 2.0+ v sobě obsahuje široké možnosti pro tracing, kdy můžeme definovat různé zdroje zpráv (TraceSource), zprávy různé rozdělovat (Switch), filtrovat (TraceFilter) a směrovat na různé listenery (výstup do konzole, XML, textového souboru, event-logu, SQL, SMTP, atd. další si můžeme napsat) – to vše bez nutnosti jakýchkoliv doplňků – ať už Microsoftích Enterprise Library / Application Blocků, či produktů třetích stran typu Log4Net. Základní přehled o možnostech si můžeme udělat například z článku A Tracing Primer – Part I [Mike Rousos].
  • .NET sám nemá mechanizmus, jak výjimky zapisovat do trace
  • napsat pár řádků kódu, které zajistí zápis výjimek do trace je snadné!!!
  • do trace můžeme buď výjimky posílat explicitně (níže volání ExceptionTracer.TraceException(myException)),
  • nebo se můžeme přihlásit k odběru událostí, které neošetřené výjimky způsobují,
    • obecně jde o událost AppDomain.CurrentDomain.UnhandledException
    • ve WinForm aplikacích pak událost Application.ThreadException

Interface pro nasazení?  …prostý

V nejjednodušší variantě si můžeme udělat například třídu ExceptionTracer, kterou přihlásíme k odběru příslušných událostí a v obsluze událostí už budeme jen výjimku posílat do trace.

Pro konzolové aplikace:

class Program
{
    static void Main(string[] args)
    {
        ExceptionTracer.SubscribeToUnhandledExceptions();

        throw new InvalidOperationException("Chybka!");

        // ExceptionTracer.Default.TraceException(new ArgumentNullException("param", "Sakrapísek!"));
    }
}

Pro WinFormAplikace:

static class Program
{
    [STAThread]
    static void Main()
    {
        ExceptionTracer.SubscribeToWindowsFormsThreadExceptions();

        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}

App.config pak může vypadat nějak takto:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.diagnostics>
        <sources>
            <source name="Exceptions" switchValue="Error">
                <listeners>
                    <add name="LogFileListener"
                        type="System.Diagnostics.TextWriterTraceListener"
                         initializeData="Exceptions.log"
                    />
                    <add name="XmlListener"
                         initializeData="Exceptions.xml"
                         type="System.Diagnostics.XmlWriterTraceListener"
                    />
                </listeners>
            </source>
        </sources>
    </system.diagnostics>
</configuration>

ExceptionTracer [Simplified Version]

Jednoduchá, ale plně funkční verze ExceptionTracekru může vypadat třeba takto:

 

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Windows.Forms;

namespace Havit.Diagnostics
{
    public class ExceptionTracer
    {
        /// <summary>
        /// Přihlásí ExceptionTracer k odběru všech neobsloužených výjimek (event AppDomain.CurrentDomain.UnhandledException).
        /// </summary>
        public static void SubscribeToUnhandledExceptions()
        {
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
        }
        private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            if (e.ExceptionObject is Exception)
            {
                TraceException((Exception)e.ExceptionObject);
            }
        }

        /// <summary>
        /// Přihlásí ExceptionTracer k odběru všech neobsloužených výjimek WinForm (event Application.ThreadException).
        /// </summary>
        public static void SubscribeToWindowsFormsThreadExceptions()
        {
            Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
        }
        private static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
        {
            TraceException(e.Exception);

            // původní implementace obsluhy výjimky, WinForm dialog s chybou
            using (ThreadExceptionDialog excptDlg = new ThreadExceptionDialog(e.Exception))
            {
                DialogResult result = excptDlg.ShowDialog();
                if (result == DialogResult.Abort)
                {
                    Application.Exit();
                }
            }
        }

        /// <summary>
        /// Pošle do trace zadanou výjimku.
        /// </summary>
        /// <param name="exception">výjimka k zaznamenání</param>
        public static void TraceException(Exception exception)
        {
            TraceSource ts = new TraceSource("Exceptions");

            ts.TraceEvent(TraceEventType.Critical, 0, exception.ToString());

            ts.Flush();
            ts.Close();
        }
    }
}

Příklad výstupu [XML]

<E2ETraceEvent xmlns="http://schemas.microsoft.com/2004/06/E2ETraceEvent">
<System xmlns="http://schemas.microsoft.com/2004/06/windows/eventlog/system">
  <EventID>0</EventID> 
  <Type>3</Type> 
  <SubType Name="Critical">0</SubType> 
  <Level>1</Level> 
  <TimeCreated SystemTime="2008-01-31T21:08:35.0625000Z" /> 
  <Source Name="Exceptions" /> 
  <Correlation ActivityID="{00000000-0000-0000-0000-000000000000}" /> 
  <Execution ProcessName="WindowsFormsApplication1" ProcessID="1232" ThreadID="1" /> 
  <Channel /> 
  <Computer>OSKAR</Computer> 
</System>
<ApplicationData>System.ApplicationException: Test !!! at WindowsFormsApplication1.Form1.button1_Click(Object sender, EventArgs e) in D:\Development\ExceptionLogging\WindowsFormsApplication1\Form1.cs:line 20 at System.Windows.Forms.Control.OnClick(EventArgs e) at System.Windows.Forms.Button.OnClick(EventArgs e) at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent) at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks) at System.Windows.Forms.Control.WndProc(Message& m) at System.Windows.Forms.ButtonBase.WndProc(Message& m) at System.Windows.Forms.Button.WndProc(Message& m) at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m) at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m) at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)</ApplicationData> 
</E2ETraceEvent>

Jak přepsat PageStatePersister

 

Jak již bylo napsáno v jiném našem článku, vlastnost PageStatePersister slouží k získání objektu, který zajišťuje uložení ViewState. Článek detailně popisuje použití třídy SessionPageStatePersister, která zajišťuje uložení ViewState do Session.

Chybná implementace

Pokud chceme použít SessionPageStatePersister bez konfigurace pomocí adaptéru, musíme přepsat zmíněnou vlastnost, ideálně ve společném předkovi všech stránek v aplikaci. Nejjednodušší (avšak nesprávná implementace) může při každém dotazu na hodnotu vlastnosti vyrobit instanci persisteru a tu vrátit.

protected override PageStatePersister PageStatePersister
{
    get
    {
        // takto ne!
        new SessionPageStatePersister(this.Page);
    }
}
Symptomy

Dosáhneme sice požadovaného efektu – ViewState se ukládá do Session, ovšem v dříve funkčních částech aplikace začneme pozorovat nečekané efekty:

  1. Mezi postbacky se nezachovávají některé hodnoty ukládané do control state (například DataKeys u GridView).
  2. V některých velmi specifických případech se neprovede nastavení hodnoty do vlastnosti, pokud tato vlastnost již měla hodnotu nastavenou v souboru ASPX a hodnota vlastnosti se vnitřně ukládala do ViewState (po provedení příkazMujControl.Vlastnost = novaHodnota nebude hodnota vlastnosti změněna).
Správná implementace

Ačkoliv se to nedočteme v dokumentaci, ale zjistíme po bližším zkoumání .NET reflektorem, musíme v jedné instanci třídy Page vracet stále stejnou instanci, protože na hodnotu vlastnosti se opakovaně dotazují metody třídy Page (jednou z nich je metoda RegisterRequiresControlState, což by vysvětlovalo symptomy v bodu 1). Po zapracování tohoto poznatku jsme se zmíněných problémů zbavili. Správně je tedy třeba vlastnost PageStatePersister přepsat takto:

protected override PageStatePersister PageStatePersister
{
    get
    {
        if (_pageStatePersister == null)
        {
            _pageStatePersister = new SessionPageStatePersister(this.Page);
        }
        return _pageStatePersister;
    }
}
private PageStatePersister _pageStatePersister;

Zjištění volné paměti počítače na webhostingu

Pokud se chceme z naší hostované ASP.NET aplikace dozvědět něco o stroji, na kterém běží, nemůžeme tak zpravidla udělat přes WMI dotazy, či performance-countery, protože nemáme k dispozici účet s dostatečnými právy a dostáváme výjimku Access Denied.

I nám C#-istům je však k dispozici to, co mají ve VisualBasicu ukryto pod featurou My, konkrétně nás zde zajímá My.Computer.Info, které není nic jiného než instancí třídyMicrosoft.VisualBasic.Devices.ComputerInfo s propertama AvailablePhysicalMemory,AvailableVirtualMemory, TotalPhysicalMemory, TotalVirtualMemory a několika dalšími s informacemi o verzi OS atp.

Stačí si tedy do našeho C# projektu nareferencovat assembly Microsoft.VisualBasic, vytvořit si instanci třídy ComputerInfo a zeptat se jí na přísušné hodnoty:

using Microsoft.VisualBasic.Devices;

...

ComputerInfo computerInfo = new ComputerInfo();
InfoLb.Text = computerInfo.AvailablePhysicalMemory.ToString("n0");

Třída ComputerInfo používá volání podpůrných nativních metod .NET a informace o paměti stroje dostaneme i pod běžným hostingovým účtem.

Změna výchozí verze ASP.NET pro nové weby IIS

Pokud Vám IIS pro nově zakládané web-site přednastavuje jinou verzi ASP.NET, než si představujete, můžete toto výchozí nastavení ovlivnit spuštěním příkazu aspnet_regiis.exe od nově požadované verze ASP.NET s níže uvedenými parametry:

aspnet_regiis -sn W3SVC/

Utilitu aspnet_regiis najdete v C:\WINDOWS\Microsoft.NET\Framework\<číslo verze>\.

ViewState vs. fáze Init, aneb jak jsem se chytil

Na svém posledním ASP.NET kurzu jsem se báječně chytil na jednom primitivním demu na fungování ViewState. Krásně jsem předváděl, jak „ve fázi Init není ještě ViewState trackován, po fázi Init nastává LoadViewState a před tím se zapne jeho tracking – TrackViewState()“, nu což, vyrobil jsem si krásné demo a nestíhal jsem pak chvíli zírat.

(Základy fungování ViewState viz článek Co by měl každý vědět o ViewState.)

„Nefunkční“ demo

Vyrobil jsem primitivní stránku, s jedním Labelem a jedním tlačítkem.

<asp:Label ID="MyLabel" Text="Výchozí text" runat="server" />
<asp:Button ID="MyButton" Text="Postback" runat="server" />

a jak jsem se demonstrovat, jak přiřazení vlastnosti Text ve fázi Init nepřežije postback:

void Page_Init(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        MyLabel.Text = "Nová hodnota z fáze Init!";
    }
}

Uff, jaké bylo mé překvapení, když Label i po kliknutí na tlačítko Postback přežíval s hodnotou „Nová hodnota z fáze Init!“. Mnohým je hned jasné, kde jsem udělal chybu, ostatním přiblížím:

ViewState v životním cyklu stránek/controlů

Mé překvapení naštěstí netrvalo dlouho, rychle jsem si ověřil, kde přesně se volá v životním cyklu controlů TrackViewState() a rozuzlení bylo na světě:

  1. TrackViewState() není první operace po fázi Init, nýbrž poslední operace fáze Init samotné,
  2. fáze Init (jako jediná ze základních fází životního cyklu) probíhá zdola nahoru, procházením stromu control-tree do hloubky – fáze Init nadřazeného controlu je završena až po dokončení fází Init všech jeho child-controls.

Výsledek je tedy nasnadě, v obsluze události Page_Init jsme úplně na vrcholu control-tree a fáze Init jednotlivých controlů již proběhla. Jejich TrackViewState() již proběhl a ViewState controlů tak již sleduje změny jednotlivých hodnot. Nastavení MyLabel.Text ve fází Page_Init tedy již je operací, kdy změny ViewState labelu jsou sledovány, narozdíl od ViewState stránky (Page) samotné, kde ještě TrackViewState() neproběhl.

„Funkční“ demo

Demo jsem rychle opravil a vše krásně fungovalo, jak má:

void MyLabel_Init(object sender, EventArgs e)
{
    if (!Page.IsPostback)
    {
        this.Text = "Text nastavený ve fázi Init labelu, nepřežije Postback.";
    }
}

void Page_Init(object sender, EventArgs e)
{
    if (!IsPostback)
    {
        this.Title = "Title nastavený ve fázi Init page, nepřežije Postback.";
    }
}

Ve fázi Init samotného Labelu ještě jeho TrackViewState() neproběhl, tedy se Text nezachová.

Ve fázi Init stránky už proběhl TrackViewState() controlů, ale stránky samotné ještě ne, tedy třeba Title se nezachová.

Klientské (ne)cachování skriptů z WebResource.axd

Skripty emitované do stránky pomocí odkazů do assembly, pomocí WebResource.axd, se v browseru cachují pouze pokud máme debug-kompilaci vypnutou:

<compilation debug="false" />

NullReferenceException v DefaultWsdlHelpGenerator.aspx při přístupu k webovým službám

Vytváříme klasickou webovou službu publikovanou pomocí ASMX. Při pokusu o přístup k této webové službě přes internetový prohlížet obdržíme výjimku NullReferenceException – a to ještě žádnou metodu nevoláme, jen prohlížíme dostupné služby.
Debugger se nám zastavuje v souboru DefaultWsdlHelpGenerator.aspx na řádku 1335:

OperationBinding FindHttpBinding(string verb) {
        foreach (ServiceDescription description in serviceDescriptions) {
            foreach (Binding binding in description.Bindings) {
                HttpBinding httpBinding = (HttpBinding)binding.Extensions.Find(typeof(HttpBinding));

Vskutku pozoruhodné je pak řešení problému: Ve web.configu máme v sekci <pages> nastavení autoEventWireUp=“false“. Po odstranění tohoto nastavení přístup k dokumentaci webových služeb funguje.
DefaultWsdlHelpGenerator.aspx je generátor dokumentace pro zobrazení v prohlížeči. Ten se evidentně kompiluje s použitím nastavení aplikace a v kódu spoléhá na zavolání metody Page_Load pomocí automatického navázání vybraných událostí (AutoEventWireUp). K dispozici máme i zdrojový kód tohoto souboru, standardně se nachází v C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG.
(Runtime .NET Framework 2.0)

Workaround

Na vývojářské mašině se dá taky rovnou zeditovat soubor C:\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG\DefaultWsdlHelpGenerator.aspx a na první řádek přidat:

<%@ Page AutoEventWireup="true" %>

Firefox velmi pomalý při použití Windows Vista a ASP.NET Development serveru

Načítání webových stránek je pod Firefoxem v kombinaci s Windows Vista a ASP.NET Development serverem velmi pomalé. První podezření padlo na ASP.NET AJAX pod Firefoxem, nicméně pravda se ukázala být jinde.

Za všechno může automatické ladění TCP stacku ve Windows Vista. Na internetu jsem nalezl dvě řešení:

Zákaz automatického ladění TCP stacku…

… lze provést spuštěním „netsh interface tcp set global autotuninglevel=disabled“ z příkazové řádky.

Toto řešení jsem nezkoušel.

Zákaz IPv6 ve Firefoxu

Dle návodu pomůže ve Firefoxu:

  1. přejít na stránku about:config,
  2. vyhledat položku network.dns.disableIPv6,
  3. nastavit hodnotu na true.

Toto řešení bylo jednoduché a účinné.

Jaká je Vaše zkušenost s kombinací Firefoxu, Windows Vista a ASP.NET Development serveru? Nebo jste na stejný problém narazili i při jiné kombinaci produktů?