2009. július havi bejegyzések

IIS7 UrlRewrite és ASP.NET postback

Az IIS 7-hez kiadott UrlRewrite modul számos előnnyel rendelkezik: az alkalmazástól független, XML-ben konfigurálható, regex alapú, van hozzá GUI, nem csak rewrite-ot, hanem redirectet is tud és még sorolhatnám. Mivel a webkiszolgálóba épül be, tetszőleges platformon íródott alkalmazással használható, így kiválóan működik ASP.NET-tel is. Mindössze csak a postback-kel van gond.

Az UrlRewrite modul leginkább azt tudja, hogy a bejövő URL-ekre reguláris kifejezéssel megállapítja, hogy melyik minta illeszkedik, majd kiveszi az URL egy részét és átteszi query string paraméterbe. Például:

Bejövő URL: http://localhost/RewriteSample/Users/Tas
Átírt URL: http://localhost/RewriteSample/User.aspx?name=Tas

Ehhez mindössze az alábbi szabályt kell felvennünk a web.configban, amiben az IIS Managerbe beépülő varázsló sokat segít:

 <rewrite>
     <rules>
        <rule name="UserRewrite" stopProcessing="true">
            <match url="^Users/([^/]+)/?$"/>
            <conditions>
                <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true"/>
                <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true"/>
            </conditions>
            <action type="Rewrite" url="User.aspx?name={R:1}" />
        </rule>
     </rules>
  </rewrite>

A böngészőben ezek után a barátságos cím fog megjelenni, valójában azonban a User.aspx fog futni, ami mindebből semmit nem vesz észre: az oldalon lévő DataSource vezérlő QueryStringParametere tökéletesen fog működni.

A gondok akkor keletkeznek, amikor nem csak GET-es oldalunk van, hanem postbackelünk is.

Postbacknél duplázódnak a query stringek

Ha feldobunk egy Button vezérlőt az oldalra, aminek hatására történik egy postback, majd újratöltődik az oldal, azt fogjuk észrevenni, hogy a lekérdezéseink elromlanak. Jobban utánajárva kideríthetjük, hogy a problémát a hibás query stringek okozzák. A fenti User.aspx oldalnak postback után két name query string paramétere lesz, ráadásul az URL is elromlik:

    http://localhost/RewriteSample/Users/Huba?name=Huba

Újabb postback után:

    http://localhost/RewriteSample/Users/Huba?name=Huba&name=Huba

Ekkor a name query string paraméter értéke “Huba,Huba,Huba” lesz, amire biztosan nem vágytunk. A megoldás az appendQueryString=”false” attribútum felvétele a web.configba:

  <action type="Rewrite" url="User.aspx?name={R:1}" appendQueryString="false" />

Postback után megjelenik a query string

A fenti attribútum felvétele után a sokszorozódási probléma megszűnik, viszont postback után egy példányban megjelenik a query string paraméter, így:

    http://localhost/RewriteSample/Users/Kond?name=Kond

vagy rosszabb esetben így (nesze neked barátságos URL):

    http://localhost/RewriteSample/Users/Töhötöm?name=T%u00f6h%u00f6t%u00f6m

Mindennek az oka a lekért oldal form tagjében keresendő:

  <form name="aspnetForm" method="post" action="Kond?name=Kond" id="aspnetForm">

Szerencsére ASP.NET 3.5 SP1-től kezdve a HtmlForm Action tulajdonsága írható, így nem kell JavaScripttel bűvészkednünk, mint korábban. A bejövő URL-t a HTTP_X_ORIGINAL_URL szerver változóban kapjuk meg, ha volt újraírás. (Érdekes módon ez megegyezik Request.RawUrl tulajdonsággal, de azt hivatalosan nem találtam meg sehol, hogy mindig megegyezik.) Akár beállíthatjuk az egészet, vagy ha egységesek akarunk lenni a nem újraírt oldalakkal, akkor csak az utolsó “/” utáni részt, sőt felturbózhatjuk a tegnapi ReturnUrl-es trükkel is:

  private void CorrectPagesWithRewrittenUrls()
  {
    string originalUrl = this.Request.ServerVariables[ "HTTP_X_ORIGINAL_URL" ];

    if( !String.IsNullOrEmpty( originalUrl ) )
    {
        int lastSlashPosition = originalUrl.LastIndexOf( '/' );
        this.Form.Action = originalUrl.Substring( lastSlashPosition + 1 );

        HtmlGenericControl c = new HtmlGenericControl();
        c.TagName = "input";
        c.Attributes[ "name" ] = "ReturnUrl";
        c.Attributes[ "type" ] = "hidden";
        c.Attributes[ "value" ] = originalUrl;            
        this.Form.Controls.Add( c );
    }
  }

Ezt meghívhatjuk Page_Loadból, ám ha valami miatt nem változtathatunk a kódon vagy több oldalt is érint a probléma, akkor lehet saját Control Adaptert írni. Ehhez először egy .browser fájl kell tennünk az App_Browsers mappába, ami megmondja, hogy a HtmlForm példányok rendereléséért a MyFormControlAdapter lesz a felelős.

  <browsers>
    <browser refID="Default">
        <controlAdapters>
            <adapter controlType="System.Web.UI.HtmlControls.HtmlForm" adapterType="MyFormControlAdapter" />
        </controlAdapters>
    </browser>
  </browsers>

Mivel nem akarjuk a teljes form renderelésének terhét magunkra vállalni, hanem csak egyetlen attribútumot akarunk átírni, ezért így valósítottam meg a control adaptert:

  public class MyFormControlAdapter : ControlAdapter
  {
    protected override void Render( HtmlTextWriter writer )
    {
        base.Render( new MyFormHtmlTextWriter( writer ) );
    }
  }

Ebben az a jó, hogy a MyFormHtmlTextWriternek nem kell minden metódusát implementálnunk, elég azzal foglalkozni, ami az attribútumok írásáért felelős:

  public class MyFormHtmlTextWriter : HtmlTextWriter
  {
    public MyFormHtmlTextWriter( HtmlTextWriter writer ) : base( writer )
    {
        this.InnerWriter = writer.InnerWriter;
    }

    public override void WriteAttribute( string name, string value, bool fEncode )
    {
        if( name.Equals( "action", StringComparison.OrdinalIgnoreCase ) )
        {
            HttpContext context = HttpContext.Current;
            string originalUrl = context.Request.ServerVariables[ "HTTP_X_ORIGINAL_URL" ];

            if( !String.IsNullOrEmpty( originalUrl ) && context.Items[ "FormActionModified" ] == null )
            {
                int lastSlashPosition = originalUrl.LastIndexOf( '/' );
                value = originalUrl.Substring( lastSlashPosition + 1 );

                context.Items[ "FormActionModified" ] = true;
            }
        }

        base.WriteAttribute( name, value, fEncode );
    }
  }

A fenti példa teljes forráskódja letölthető innen.

Reklámok

Melyik oldal jön be bejelentkezés után?

Az ASP.NET Login vezérlőnek van egy DestinationPageUrl tulajdonsága, amellyel megadhatjuk annak az oldalnak a címét, ahova sikeres bejelentkezés után a vezérlő továbbdob. Ez szép is lenne, már ha működne.

Az esetek nagy részében a Login vezérlőt feldobjuk egy Login.aspx oldalra és a LoginStatus vezérlővel irányítjuk a felhasználót erre az oldalra. A LoginStatus azonban nem egyszerűen a Login.aspx URL-t adja meg az átirányítás céljaként, hanem utánafűz egy ?ReturnUrl= kezdetű query stringet. Ha a Login vezérlő talál ReturnUrl-t a query stringben, akkor annak az értéke elsőbbséget élvez a DestinationPageUrl tulajdonsággal szemben és az ott megadott oldalra jut a felhasználó sikeres bejelentkezés után.

A kérdés már csak az, hogy mi van akkor, ha szeretném felülbírálni a LoginStatus okoskodását – például URL rewriting miatt – és én szeretném beállítani, hogy melyik oldal jelenjen meg bejelentkezés után? Természetesen a LoginStatus vezérlőnek nincs ilyen tulajdonsága…

Némi Reflectorozás után rájöhetünk, hogy az internal FormsAuthentication.GetReturnUrl metódus felelős a visszatérési cím összeállításáért. Ez először megnézi, hogy van-e ReturnUrl a query stringben és ha nincs (miért is lenne??), akkor még megnézi a Request.Form gyűjteményben is. Nincs más dolgunk, mint biztosítani azt, hogy a Formban legyen ilyen érték, amit legegyszerűbben úgy tehetünk meg, hogy létrehozunk az oldalra egy ilyen nevű rejtett mezőt – fontos, hogy a name attribútuma ReturnUrl legyen.

Ez sem olyan egyszerű, mint gondolnánk:

  1. A HiddenField vezérlőnek nincs Name tulajdonsága, sőt Attributes sincs 😦
  2. A HtmlInputHidden vezérlőnek van Name tulajdonsága, de az ID alapján automatikusan kap értéket 😦
  3. Marad a HtmlGenericControl:
  HtmlGenericControl c = new HtmlGenericControl();
  c.TagName = "input";
  c.Attributes[ "name" ] = "ReturnUrl";
  c.Attributes[ "type" ] = "hidden";
  c.Attributes[ "value" ] = myUrl;            
  this.Form.Controls.Add( c );

Ha ezt a kódot betesszük az oldalon a Page_Loadba, akkor a LoginStatus a ReturnUrl query string megépítésénél a myUrl értékét fogja használni, a Login vezérlő pedig oda fogja irányítani a felhasználót bejelentkezés után.

Technorati-címkék: ,,

Minek annyi foreach?

Ha elfogadjuk azt az alaptételt, hogy “kevesebb kód – kevesebb bug”, akkor miért ragaszkodunk annyira a szószátyár foreach ciklusokhoz?

Számtalanszor látok olyan kódot, ami egy lista minden elemével csinál valamit, gyakran csak átadja egy metódusnak:

  List<string> vezerek = new List<string> { "Álmos", "Előd", "Ond", "Kond", "Tas", "Huba", "Töhötöm" };
  foreach( var vezer in vezerek )
  {
    Console.WriteLine( vezer );
  }

Ennél az alábbi sokkal egyszerűbb és pont ugyanezt csinálja:

  vezerek.ForEach( vezer => Console.WriteLine( vezer ) );

Persze olyan is van, amikor nem minden elemmel akarjuk elvégezni ezt a műveletet, csak azokkal, ami teljesít egy feltételt:

  foreach( var vezer in vezerek )
  {
    if( vezer.EndsWith( "d" ) )
    {
      Console.WriteLine( vezer );
    }
  }

Ennél is van egyszerűbb:

  vezerek.FindAll( vezer => vezer.EndsWith( "d" ) ).ForEach( vezer => Console.WriteLine( vezer ) );

Az első példában egyértelmű, hogy a rövidebb megoldást célszerű alkalmazni, mert pont ugyanazt csinálja a két kód, csak az egyik rövidebb és jobban olvasható. A második esetben azonban a FindAll és a ForEach egyaránt egy-egy for ciklusra fordul, tehát itt már két ciklus fog lefutni egymás után – azaz  mérlegelnünk kell az olvashatóság és a teljesítmény között. Például nem biztos, hogy ez a legjobb megoldás egy CheckBoxList kiválasztott elemeinek DataTable-be töltésére:

  DataTable dt = new DataTable();
  dt.Columns.Add( "Item", typeof( string ) );
  this.cblNames.Items
.Cast<ListItem>()
.ToList()
.FindAll( item => item.Selected )
.ForEach( item => dt.Rows.Add( item.Value ) );

Ha eltekintünk a teljesítménytől, akkor is fontos szempont, hogy sokak számára a fenti lambdás írásmód még nem számít “olvashatónak”, ugyanúgy ahogy a (feltétel) ? (ha igaz) : (ha hamis) szintakszis (van ennek valami szép neve?) sem. Mindkettő túl tömör. Azonban van egy óriási előnyük: ordít róluk, hogy mire szolgál az adott kódsor. A foreach csak egy ciklus, a ForEach pedig egy gyűjtemény minden elemével elvégez egy műveletet. Az if csak egy feltétel vizsgálat, a (feltétel) ? (ha igaz) : (ha hamis) viszont egy feltételtől függő értékadás. Kétségkívül kell hozzá kis gyakorlat, de megéri.

Ti mit gondoltok, olvashatóbb a tömörebb kód?

Technorati-címkék: ,

Seven – Ready To Go

Ahogy előzetes megígérték – még júliusban – elkészült a Windows 7 és a Windows Server 2008 R2! Nem tudom, hogy ki miért fog átállni, át fog-e egyáltalán, összeszedtem néhány kedvenc és nem annyira kedvenc feature-ömet.

A lista nem teljes és abszolút szubjektív válogatás.

Windows 7

A W7-t béta kora óta használom élesben a notebookomon és nagyon jó tapasztalatom van vele. Szerintem minden idők legstabilabb bétája volt, és ha az RC óta nem sokat rontottak rajta, akkor tudom ajánlani 🙂

  • Felhasználóknak:
    • Érezhetően gyorsabb boot, standby és hibernate (ok, most lehet mondani, hogy a Vistát nem nagy kunszt legyűrni 🙂
    • Sokkal tempósabb ablak- és fájlkezelés.
    • Működik a hálózat, de tényleg! Nincs gond a vezeték nélküli hálózatokkal és nincsenek belassulások akkor sem, ha a domaines notebookomat olyan másik hálózatra kötöm, ahonnan nem találja meg a DC-t.
    • Jobb akkumulátor gazdálkodás.
    • Az új tálca számomra használhatatlan 😦 De majdnem teljesen vissza lehet állni a régire 🙂
  • Fejlesztőknek:
    • Rengeteg új API, amit elsősorban vastag kliens alkalmazások tudnak kihasználni. Aki ezzel foglalkozik, annak feltétlenül érdemes megnézni a PhotoView (XP2Win7) referencia alkalmazást.
    • .NET 4 értelemszerűen nincs benne, csak .NET 3.5 SP1 😦
    • IIS 7.5
    • PowerShell V2 + ISE
  • Üzemeltetőknek:
    • DirectAccess – bye-bye VPN
    • Sokkal kevesebb és gyorsabb UAC prompt.

Windows Server 2008 R2

  • Hyper-V V2: Live migration, dynamic virtual machine storage
  • Service accounts: az IIS 7 AppPooloknál nagyon bevált
  • PowerShell V2 + Integrated Scripting Environment (ISE): remote, background, transactions, debugger
  • IIS 7.5: PowerShell v2, BPA, néhány (többet vártam) bővítmény integrálva ééééés végre lehet ASP.NET-et telepíteni Server Core-ra!!! Vigyázat, mert a .NET Frameworknek csak egy “subset”-je támogatott, a felhasználói felülettel (WinForms, WPF) és a shellel kapcsolatos részek nem. Természetesen itt is csak 3.5 van, 4.0 még nincs.

Elérhetőség: MSDN és TechNet előfizetőknek augusztus 6-án (újabb források szerint 14-én), boltokban október 22-én. Bővebben itt.

További újdonságok magyar nyelvű screencastokban a TechNet oldalon megtalálhatóak.

GT-nek biztos hosszabb listája van… Nektek mi tetszik?

ASP.NET és SharePoint hosting lehetőségek

Újra és újra felmerül a kérdés, hogy miután elkészítettem az ASP.NET alkalmazásomat vagy a SharePointra épülő megoldásomat, hol tudom közzétenni, ki ad nekem ingyen tárhelyet?

Ennek a blogbejegyzésnek az a célja, hogy összeálljon egy linkgyűjtemény, elsősorban ingyenes és magyar szolgáltatókkal, ezért kérlek ha tudsz hasonló lehetőségről, küldd el nekem! Köszönöm!

Tekintve, hogy nagyon kevés hoszter van, ezért a fizetőseket is szerepelnek a listában. Egyiket sem próbáltam, ez csak hivatkozás lista, nem ajánlás vagy tapasztalati sorrend!

Magyar hoszterek

Nemzetközi hoszterek

Van még, amit érdemes kipróbálni?

Mi lesz veled Windows Forms?

A Microsoft Regional Director levlistán nemrég felmerült a kérdés, hogy várható-e még frissítés a Windows Formshoz vagy már annyira a WPF van fókuszban, hogy a kutyát nem érdekli, mi lesz a WinForms fejlesztőkkel?

Meg is jött a hivatalos válasz Tim Sneath-től:

“Microsoft is committed to supporting WinForms for the foreseeable future as part of the .NET Framework. WinForms is a mature and stable technology with a broad ecosystem of tools and controls that enable rapid development of Windows solutions. WinForms can also host WPF controls and windows through an interoperability layer. While we’ll continue to maintain WinForms, the majority of new investment will be focused on our newer XAML-based technologies: Silverlight and WPF. “

Ha valaki nem tudna olvasni a sorok között: a WinForms utolsó komolyabb változásokon a .NET 2.0 idején esett át, cirka 5 éve. Azóta persze jöttek hotfixek, meg support is van még hozzá sokáig, de új verzió (ami nem csak “4.0” branding) aligha lesz belőle. Állítólag jóformán már nincs is Windows Forms csapat Redmondban… A jó hír az, hogy akárki akármit mond, van hasonlóság, aki már egyszer fejlesztett .NET platformon kliens alkalmazást Visual Studioval, annak lesznek ismerős fogások. (Nekem például nagyon hamar sikerült belejönnöm a Silverlightos fejlesztésbe, pedig a VS 2008 alig ad hozzá segítséget.) Hogy az interop mennyire használható, azt mondja meg az, aki nap mint nap ezzel küzd.

Valószínűleg nem leszünk rákényszerítve, hogy XAML alapokon fejlesszünk, de előbb vagy utóbb lesznek ilyen elvárások és ne lepődjünk meg azon, ha az újdonságok előbb fognak megjelenni WPF-hez, mint Windows Formshoz. Vagy talán meg sem jelennek hozzá. A hivatalos WindowsClient.net oldalon már szerintem túlsúlyban van a WPF tartalom…

Titeket ez érint, felkészültetek a váltásra?

Technorati-címkék: ,,

Go Live hibák

Gyakran előfordul, hogy egy webalkalmazás éles üzembe állítása nem olyan simán megy, mint ahogyan azt szeretnénk vagy ahogy azt a fejlesztő elképzeli. A minap egy kész ASP.NET-es alkalmazást kellett IIS 7 alatt közzétennem, miközben a rendszer szebbnél szebb hibaüzenetekkel örvendeztetett meg.

Arra természetesen gondoltam, hogy ellenőrizzem a connection string beállításokat és hogy beállítsam az alkalmazáshoz tartozó application poolt, első futtatáskor mégis ezzel kellett szembesülnöm:

HTTP Error 500.19 – Internal Server Error
The requested page cannot be accessed because the related configuration data for the page is invalid.

Error Code: 0x8007000d
Handler: Not yet determined

A web.config tökéletes volt, csak éppen az IIS nem ismert fel benne minden elemet, ugyanis a webalkalmazás használta az URL Rewrite Module-t. Miután ezt letöltöttem és telepítettem, a fenti hibaüzenet megszűnt. Jött helyette másik:

HTTP Error 401.3 – Unauthorized
You do not have permission to view this directory or page because of the access control list (ACL) configuration or encryption settings for this resource on the Web server.

Notification: AuthenticateRequest
Handler: StaticFile
Error Code: 0x80070005

Aki konfigurált már IIS 6-on Application Poolt, ilyenkor rögtön megnézi, hogy valóban jó-e az Application Pool identity, és annak a felhasználói fióknak valóban van-e joga az alkalmazás mappájának olvasására. Ezt tényleg nem árt ellenőrizni, de IIS 7 alatt még azt is lehet külön állítani, hogy anonymous hitelesítés esetén az IUSR vagy az application pool fiók nevében fusson az alkalmazás:

Application pool identity használatának beállítása IIS 7 alatt

Alapértelmezés szerint itt az IUSR felhasználó szerepel, miután átállítottam Application pool identity-re az alkalmazás már elindult, csak az oldalon nem jelentek meg a grafikai elemek és szemmel láthatóan a CSS sem töltődött le. Beírtam az oldalon megjeleníteni kívánt logo.png közvetlen elérési útját a böngészőbe, így sikerült előcsalnom a teljes hibaüzenetet:

HTTP Error 500.19 – Internal Server Error
The requested page cannot be accessed because the related configuration data for the page is invalid.

Module: HttpLoggingModule
Notification: SendResponse
Handler: StaticFile
Error Code: 0x80070021
Config Error: This configuration section cannot be used at this path. This happens when the section is locked at a parent level. Locking is either by default (overrideModeDefault="Deny"), or set explicitly by a location tag with overrideMode="Deny" or the legacy allowOverride="false". 

Az 500.19 már ismerős, megint valami konfigurációs hiba. A Detailed Error Information blokkból azonban kiderül, hogy itt a kérés már tovább jutott és a HttpLoggingModule futása közben halt el. Valami olyan dolog lehet beállítva a web.configban, ami erre a modulra vonatkozik, csak éppen felsőbb, szerver szinten nem engedélyezett ennek az opciónak a beállítása alkalmazás szinten. Megnézve a kérdéses mappát, valóban szerepelt benne egy web.config, amiben ki volt kapcsolva a naplózás:

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
        <system.webServer>
            <!-- Disable IIS7 logging for this folder -->
            <httpLogging dontLog="true" />
        </system.webServer>
    </configuration>

Nosza irány a Feature Delegation funkció az IIS Mangerben, ahol beállítottam, hogy a Logging legyen Read/Write az alapértelmezett Not delegated helyett:

IIS 7 Feature Delegation - Logging Read/Write

Mindezek egy fejlesztés szempontjából aránylag egyszerű alkalmazásnál jöttek elő, csak éppen a fejlesztés közben nem vették folyamatosan figyelembe az üzemeltetési feladatokat, így ezeket a beállítási igényeket egyszerűen nem dokumentálták.