2007. december havi bejegyzések

LINQ to XML: osztályhierarchia, navigációs és módosító metódusok

Megjegyzés: A Live Space szerint a 600 pixelnél szélesebb kép már túl sok, ezért jobbnak látta használhatatlanná kicsinyíteni a képet ebben a cikkben, emiatt az rosszul jelenik meg. A cikk teljes változata, jó minőségű képpel megtalálható az MSDN Kompetencia Központ oldalán.

Az egyik óriási problémám az XML osztálykönyvtárakkal, hogy nagyon sok időbe kerül, míg sikerül átlátnom, hogy melyik osztály mire való és hogyan kapcsolódik az összes többihez. Az általam ismert XML osztálykönyvtárak közös jellemzője, hogy ugyanazt az eredményt nagyon sokféleképpen lehet elérni, hiszen egy adott elem több úton is megközelíthető. Na de melyik út a legrövidebb?

A LINQ to XML, vagy gyerekkori nevén az XLinq kicsit más, az osztályok nevei magukért beszélnek, ráadásul nincs feleslegesen sok belőlük. Ezzel együtt sokat segített, hogy a Visual Studioval felrajzoltattam az osztályhierarchiát a System.Xml.Linq névtérben, így sokkal könnyebben átláttam az osztályok egymás közötti viszonyát (katt a képre a teljes méretért):

LINQ to XML class diagram

Hogy ne csak a levegőben lógjanak, az XDeclaration, XName és XNamespace osztályoknál csaltam egy kicsit, nem az öröklési lánc, hanem tulajdonságok segítségével ábrázoltam őket.

A következő lépés az volt, hogy sorra vettem az egyes osztályokban lévő metódusokat és megpróbáltam őket funkciójuk alapján csoportosítani. Nagyon remek, hogy van a Studioban IntelliSense, csak éppen nem tudja különválasztani például a lekérdező és a módosító metódusokat, az ABC sorrend pedig ilyenkor nem sokat segít. Az eredmény az alábbi táblázat lett, nagyságrendileg 50 metódust találtam:

Típus Metódus vagy tulajdonság Visszatérési érték
Navigáció a gyermek elemek irányába
XContainer FirstNode { get; } XNode
XContainer LastNode { get; } XNode
XContainer* Nodes() IEnumerable<XNode>
XContainer* DescendantNodes() IEnumerable<XNode>
XElement* DescendantNodesAndSelf() IEnumerable<XNode>
XContainer Element( XName ) XElement
XContainer* Elements() IEnumerable<XElement>
XContainer* Elements( XName ) IEnumerable<XElement>
XContainer* Descendants() IEnumerable<XElement>
XContainer* Descendants( XName ) IEnumerable<XElement>
XElement* DescendantsAndSelf() IEnumerable<XElement>
XElement* DescendantsAndSelf( XName ) IEnumerable<XElement>
XElement HasElements { get; } bool
Navigáció a szülő elemek irányába
XNode* Parent { get; } XElement
XNode* Ancestors() IEnumerable<XElement>
XNode* Ancestors( XName ) IEnumerable<XElement>
XElement* AncestorsAndSelf() IEnumerable<XElement>
XElement* AncestorsAndSelf( XName ) IEnumerable<XElement>
Navigáció azonos szinten
XNode IsBefore( XNode node ) bool
XNode IsAfter( XNode node ) bool
XNode PreviousNode { get; } XNode
XNode NextNode { get; } XNode
XNode NodesBeforeSelf() IEnumerable<XNode>
XNode NodesAfterSelf() IEnumerable<XNode>
XNode ElementsBeforeSelf() IEnumerable<XElement>
XNode ElementsBeforeSelf( XName ) IEnumerable<XElement>
XNode ElementsAfterSelf() IEnumerable<XElement>
XNode ElementsAfterSelf( XName ) IEnumerable<XElement>
Navigáció az attribútumok között
XElement HasAttributes { get; } bool
XElement Attribute( XName ) XAttribute
XElement FirstAttribute { get; } XAttribute
XElement LastAttribute { get; } XAttribute
XElement Attributes() IEnumerable<XAttribute>
XElement Attributes( XName ) IEnumerable<XAttribute>
Módosítás
XElement, XAttribute SetValue( object )  
XElement, XAttribute Value { get; set; }  
Gyermek elemek és attribútumok módosítása
XContainer Add( params object[] )  
XContainer AddFirst( params object[] )  
XContainer RemoveNodes()  
XElement RemoveAttributes()  
XElement RemoveAll()  
XContainer ReplaceNodes( params object[] )  
XElement ReplaceAttributes( params object[] )  
XElement ReplaceAll( params object[] )  
XElement SetElementValue( XName, object )  
XElement SetAttributeValue( XName, object )  
Módosítás a szülőn keresztül
XNode AddBeforeSelf( params object[] )  
XNode AddAfterSelf( params object[] )  
XNode, XAttribute Remove()  
XNode ReplaceWith( params object[] )  

* A metódus szekvenciákon is működik.

Remélem más is hasznát veszi, ha hiba van benne, vagy kimaradt valami, kérlek írjatok.

 

Technorati tags: , , ,
Reklámok

IEnumerable.Dump()

Épp az egyik új tárgyunkhoz írok jegyzetet és példakódot LINQ to XML témában és felmerült, hogy kellene egy kényelmesen használható Dump metódus gyűjtemények elemeinek konzolra írásához.

Ha már úgyis C# 3.0 és .NET 3.5, akkor miért ne legyen extension method, legalább könnyű lesz újra felhasználni és talán kevesebbet is kell gépelni, amikor meg akarom hívni. Létrehoztam tehát egy névteret és egy statikus osztályt, benne pedig egy bővítő metódust:

    namespace LinqToXmlSample
    {
        public static class Extensions
        {
            /// <summary>
            /// Kiírja a konzolra egy gyűjtemény összes elemének értékét.
            /// </summary>
            /// <typeparam name="T">A gyűjtemény elemeinek típusa.</typeparam>
            /// <param name="collection">A feldolgozandó gyűjtemény.</param>
            public static void Dump<T>( this IEnumerable<T> collection )
            {
                foreach( T item in collection )
                {
                    Console.WriteLine( item );
                }

                Console.WriteLine( "nÖsszesen: {0} elem.n", collection.Count() );
            }
        }
    }

Egy statikus osztályban lévő statikus metódus attól lesz bővítő metódus, hogy a bővítendő típust adjuk meg első paramétereként, megelőzve őt a this kulcsszóval. Így a fenti Dump metódusom elérhető lesz minden IEnumerable<T> példányon.

Ettől kezdve vidáman tudtam ilyeneket írni:

    IEnumerable<XElement> acts = doc.Root.Elements( "ACT" );
    acts.Dump();

Ez már majdnem jó, csak az vele a gond, hogy az XElement egy ToString hívásnál nem csak a tartalmát, hanem a kacsacsőröket is beírja a válasz stringbe. Én pedig néha akarok kacsacsőröket, néha meg nem.

Ezért úgy döntöttem, hogy gyártok még egy Dump metódust, ami egy gyűjtemény elemeiből képzett tetszőleges értéket ki tud írni a konzolra. Ehhez felhasználtam a korábban már bemutatott Func delegate-et:

  /// <summary>
  /// Kiírja a konzolra egy gyűjtemény elemeiből tetszőlegesen képzett értékeket.
  /// </summary>
  /// <typeparam name="T">A gyűjtemény elemeinek típusa.</typeparam>
  /// <typeparam name="TResult">A gyűjtemény elemeiből képzett, a konzolra kiírandó értékek típusa.</typeparam>
  /// <param name="collection">A feldolgozandó gyűjtemény.</param>
  /// <param name="transformer">A gyűjtemény elemeit a kiírandó értékre alakító függvény.</param>
  public static void Dump<T, TResult>( this IEnumerable<T> collection, Func<T, TResult> transformer )
  {
      foreach( T item in collection )
      {
          Console.WriteLine( transformer( item ) );
      }
       Console.WriteLine( "nÖsszesen: {0} elem.n", collection.Count() );
  }

Ebben az a jó, hogy a transformer paraméter helyére tetszőleges függvényt írhatok, ami T bemeneti típusból TResult típust állít elő. Felhasználva a korábban már megértett lambda kifejezéseket, írhatok ilyeneket:

    IEnumerable<XElement> personas2 = doc.Root.Descendants( "PERSONA" );
    personas2.Dump( e => e.Value );            

Vagy persze akár komplexebbeket is:

    IEnumerable<XElement> acts = doc.Root.Elements( "ACT" );
    acts.Dump( e => e.Element( "TITLE" ).Value );

Az egészben talán az a legszebb, hogy egyetlen névtér hivatkozással ezek után bárhol el használhatom ezt a szintakszist, ami szerintem sokkal olvashatóbb, mintha mindenhol foreach és if hegyek lennének.

 

LINQ IntelliSense dekódolása: Action, Func, TSource és lambda

Úgy látom, hogy a képek nem jól jelennek meg ebben a cikkben. A cikk teljes változata megtalálható az MSDN Kompetencia Központ oldalán.

 

Annyi sok SQL kódot láttunk már, hogy az alábbi LINQ-es kódon már meg sem akad a szemünk:

    string[] elves = new string[] { "Tudor", "Vidor", "Hapci", "Szundi", "Morgó", "Szende", "Kuka" };

    IEnumerable<string> dors = elves.Where( e => e.EndsWith( "dor" ) );

    foreach( string dor in dors )
    {
        Console.WriteLine( dor );
    }

A Where már annyira természetes, hogy fel sem tűnik, mit ír ki az IntelliSense, amikor elkezdjük használni. De nem is kell túl messzire menni ahhoz, hogy az IntelliSense-ből valami igazán nehezen érthetőt varázsoljunk elő. Íme például a "segítség" az OrderBy metódushoz:

OrderBy IntelliSense

A "Sorts the elements…" szöveg persze érthető, de mi az a TSource, meg a TKey és hogy kerül oda az a Func<>?

Ahhoz, hogy ezt megértsük, vissza kell emlékeznünk arra, hogy mit tanultunk a delegate-ekről. A Visual Studio and .NET Framework glossary szerint a delegate:

In the .NET Framework, a reference to a function. A delegate is equivalent to a function pointer.

Ezért aztán már .NET 2.0-ban írhattunk ilyeneket:

    // Egy saját delegate
    public delegate void DoThis( string input );

    // Egy metódus, ami delegate-et kap paraméterül és meghívja azt
    public void ProcessAll( string[] array, DoThis processMethod )
    {
        for( int i = 0; i < array.Length; i++ )
        {
            processMethod( array[ i ] );
        }
    }

    // A delegate erre a metódusra hivatkozik
    public void PrintUpper( string input )
    {
        Console.WriteLine( input.ToUpper() );
    }

    // Itt kezdődik minden
    public void Start()
    {
        string[] days = new string[] { "Hétfő", "Kedd", "Szerda", "Csütörtök", "Péntek", "Szombat", "Vasárnap" };

        DoThis myMethod;
        myMethod = PrintUpper;
        ProcessAll( days, myMethod );
    }

Kihasználva, hogy vannak generikus típusok, átírhatjuk a delegate-ünket általánosabb formára:

    public delegate void DoThis<T>( T input );

    public void ProcessAll( string[] array, DoThis<string> processMethod )
    {
        for( int i = 0; i < array.Length; i++ )
        {
            processMethod( array[ i ] );
        }
    }

    public void Start()
    {
        //...
        DoThis<string> myMethod;
        //...
    }

Mivel az általunk létrehozott DoThis elég általánosnak tűnik, ezért a .NET Framework készítői már a 2.0 változatban létrehoztak egy pont ugyanilyen delegate-et System.Action<T> néven. Használhatjuk azt is a saját kódunkban, ugyanaz lesz az eredmény:

    public void ProcessAll( string[] array, Action<string> processMethod )
    {
        for( int i = 0; i < array.Length; i++ )
        {
            processMethod( array[ i ] );
        }
    }

    public void Start()
    {
        //...
        Action<string> myMethod;
        //...
    }

Hasonlóan gyakori feladatnak tűnik olyan szűrő metódusok alkalmazása, amelyek egy bemenő paramétert feldolgoznak, majd visszatérnek egy igaz vagy hamis értékkel. Ezek az ún. predicate-ek (a predicate angol szó ugyanis állítást jelent), amik leírására már a .NET 2.0-ban létezett egy System.Predicate<T> delegate ezzel a szignatúrával:

public delegate bool Predicate<T>( T obj )

Ezt felhasználva készíthetünk olyan metódust, ami egy tömb kiválasztott elemeivel csinál valamit – csak éppen nem tudja, hogy melyek a kiválasztott elemek és hogy mit kell velük csinálni. Ezeket a feladatokat más metódusok fogják elvégezni, amikre természetesen delegate-ek segítségével hivatkozunk:

    public void ProcessDays( string[] array, Action<string> processMethod, Predicate<string> filterMethod )
    
        for( int i = 0; i < array.Length; i++ )
        {
            if( filterMethod( array[ i ] ) )
            {
                processMethod( array[ i ] );
            }
        }
    }
    // Szűrő metódus: true-val tér vissza, ha a paraméter munkanap
    public bool IsWorkday( string day )
    {
        if( day == "Szombat" || day == "Vasárnap" )
        {
            return false;
        }
        return true;
    }

    // Feldolgozó metódus: a paramétert kiírja nagybetűkkel
    public void PrintUpper( string input )
    {
        Console.WriteLine( input.ToUpper() );
    }

    public void Start()
    {
        string[] days = new string[] { "Hétfő", "Kedd", "Szerda", "Csütörtök", "Péntek", "Szombat", "Vasárnap" };
        ProcessDays( days, PrintUpper, IsWorkday );
    }

Az eredmény pedig a munkanapok nevei a konzolon, csupa nagybetűvel.

Mi újat hoz mindebbe a .NET 3.5 és a C# 3.0? Először is, hogy a lambda kifejezések segítségével még tömörebben leírhatjuk mindezt. Például az egész delegate átadást és metódus hívást egyetlen kifejezésbe sűríthetjük:

    ProcessAll( days, day => Console.WriteLine( day.ToUpper() ) );

A szintaxis működik több delegate-re is:

    ProcessDays( days, 
        day => Console.WriteLine( day.ToUpper() ), 
        day => !(day == "Szombat" || day == "Vasárnap" ) );

A lambda kifejezés tehát egy delegate-es metódus hívásra fordul le LINQ to Objects esetén.

A másik .NET 3.5 újdonság, hogy a System.Core.dll-ben ott figyel jó néhány előre definiált, általános célú delegate:

    namespace System
    {
        public delegate void Action();

        public delegate void Action<T1, T2>( T1 arg1, T2 arg2 );

        public delegate void Action<T1, T2, T3>( T1 arg1, T2 arg2, T3 arg3 );

        public delegate void Action<T1, T2, T3, T4>( T1 arg1, T2 arg2, T3 arg3, T4 arg4 );

        public delegate TResult Func<TResult>();

        public delegate TResult Func<T, TResult>( T arg );

        public delegate TResult Func<T1, T2, TResult>( T1 arg1, T2 arg2 );

        public delegate TResult Func<T1, T2, T3, TResult>( T1 arg1, T2 arg2, T3 arg3 );

        public delegate TResult Func<T1, T2, T3, T4, TResult>( T1 arg1, T2 arg2, T3 arg3, T4 arg4 );
    }

Ezek közül az Action variációkat már ismerjük, olyan metódusok, amelyek csak bemeneti paramétereket várnak és nem térnek vissza semmivel (void). Ezzel szemben a Func változatoknak van visszatérési értékük, melyek típusát is meg kell adnunk, ezt jelzi a TResult. Ebből is van több változat, hogy a többféle bemeneti paramétert le tudjuk írni T1..T4 típusokkal.

Ezek után már könnyen megérthetjük az egyes LINQ metódusokhoz kapcsolódó IntelliSense tippet. A Where esetén például ezt láthatjuk:

Where tooltip

Az előző lista alapján érthetjük, hogy a Func<string, bool> éppen a Func<T, TResult> delegate-nek felel meg, azaz string bemeneti értékhez bool visszatérési értéket rendelő függvényről van szó – és az ilyen függvényt hívjuk predicate-nek. Használhatnánk éppen a Predicate<T> típust is, de valamiért a LINQ bővítő metódusok megalkotói inkább a Funcokat választották. 

Felhasználva a korábbi IsWorkday metódusunkat, írhatjuk akár ezt is, hiszen annak épp string be, bool ki szignatúrája van:

    IEnumerable<string> workdays = days.Where( IsWorkday );

Amit persze lambdával "illik" leírni, akár metódushívással:

    IEnumerable<string> workdays = days.Where( day => IsWorkday( day ) );

Akár pedig tömören, metódushívás nélkül:

    IEnumerable<string> workdays = days.Where( day => !( day == "Szombat" || day == "Vasárnap" ) );

A Where persze egy egyszerű eset, de mi a helyzet az OrderBy paramétereivel? Írhatjuk például ezt és megkapjuk a napokat hosszuk szerint rendezve:

    IEnumerable<string> workdays = days.OrderBy( day => day.Length );

Az OrderBy paramétere egy Func<TSource, TKey> (lásd a tooltippet a cikk elején), amiről már tudjuk, hogy egy olyan metódus, aminek TSource a bemeneti paramétere és TKey típussal tér vissza. Mivel a TKey is csak egy paraméter, az OrderBy tetszés szerinti érték szerint tud rendezni.

Aki úgy gondolja, hogy ez gyerekjáték, annak javaslom az alábbi kód megfejtését nem "mit ír ki", hanem inkább "miért ezt kell ideírnunk" játék formájában (segítségként persze használható az Aggregate függvény leírása az MSDN-ben):

    string totalLength = days.Aggregate( 
        0, 
        ( total, day ) => total += day.Length,
        result => String.Format( "A hét {0} karakter hosszú.", result )
        );

Ez ugyan valóban csak egy sor kód, de edzettebb szem kell a megértéséhez, mintha ugyanezt egy foreach-ben írtuk volna le.

 

Technorati tags: , , , , ,

Office 2007 SP1 slipstreaming

Közel egy évvel az Office 2007 RTM után végre megjelent hozzá az első javítócsomag. Nem mondom, hogy az Office 2007 annyira használhatatlan lenne RTM formájában, mint a Vista, de azért erre is ráfért végre a service pack.

A Microsoft javítócsomagok már jó ideje lehetővé teszik, hogy beintegráljuk őket az eredeti RTM verzió telepítőjébe, így egyetlen telepítéssel rögtön a javításokkal ellátott verziót varázsolhatjuk a gépünkre. Ezt a folyamatot nevezik slipstreamingnek. Ez eddig egészen trükkösen működött, gyakorlatilag egy speciális kapcsolóval kellett elindítanunk a service pack telepítőjét, ami rátelepítette magát az eredeti install csomagra. Nem véletlen, hogy csak kevés halandó vállalkozott a feladatra.

Szerencsére ezt az Office 2007-tel teljesen újragondolták és leegyszerűsítették: az RTM telepítő tartalmaz egy Updates nevű mappát, amit mi ide beteszünk, azt a telepítő megtalálja és okosan fel is használja. Ide kell tehát tenni az SP1 fájljait is, csak hogy nem a webről letölthető .exe, hanem .msp fájlok formájában. Ehhez előbb ki kell bontanunk az .exe-t, ami az alábbi paranccsal tehető meg, a /extract kapcsolónak egy célkönyvtárat kell megadnunk, például így:

W:install>office2007sp1-kb936982-fullfile-en-us.exe /extract:"W:installOffice2007Updates"

Ehhez a művelethez admin jogra van szükségünk, mert valaki úgy gondolta, hogy a licenc elfogadásához mindenképp atyaúristeni privilégiumok szükségesek. Szerencsére később semmi más feladatunk nincs, mint elindítani a telepítőt, az SP1 automatikusan települni fog. Nekem SP1 telepítés előtt a Word 12.0.6015.5000, SP1 12.0.6212.1000 verziószámot mutatott.

Ugyanez a módszer tökéletesen működik az Office 2007 Language Pack-re is, ahhoz is létezik már SP1.

 

Csinálj magadnak saját MSDN-t

Talán nem közismert, de az MSDN és a TechNet Library tartalma elérhető egy webszolgáltatáson keresztül, ez az ún. MTPS, az MSDN/TechNet Publishing System. Aki nem hiszi, járjon utána, és olvassa el a doksit, vagy nézze meg a webszolga WSDL-jét.

Ezen a nyilvános webszolgáltatáson keresztül letöltögethetjük a minket érdeklő fejezeteket az netről és ha kedvünk támad, akkor gyúrhatunk belőle saját MSDN-t is. A CodePlexen megtalálható PackageThis alkalmazás pontosan ezt teszi lehetővé, méghozzá barátságos WinForms felületen keresztül, így nem kell beleásnunk magunkat az API-ba.

Ha megtetszik és használni szeretnénk, feltétlenül olvassuk el a Prerequisites fejezetet az oldalon, mert szükség lesz a HTML Help SDK-ra a CHM fájlok előállításához. Aki esetleg úgy jár, mint én, hogy a HTML Help Workshop telepítője félmunkát végez, az hozza létre az alábbi registry kulcsot, mert a PackageThis ezt keresi:

HKCUSoftwareMicrosoftHTML Help WorkshopInstallDir (REG_SZ) = C:Program FilesHTML Help Workshop

Újabb hálás köszönet Lutz Roeder Reflectorának, hogy erre segített fényt deríteni 🙂

Az ok, ami miatt ráakadtam erre az alkalmazásra az volt, hogy ha .NET 3.0 alatt akartam Windows Workflow Foundationnel foglalkozni, a help telepítéséhez szükség volt a legfrissebb Vista SDK-ra, ami bizony néhány gigabájt. Persze a Visual Studio 2008-cal mindez a múltté, mégis kényelmesebb lehet egy kisebb méretű help fájlt átmásolni, mint a teljes MSDN-t telepíteni. A PackageThisben bepipáltam a közel 5000 WF-fel foglalkozó MSDN topicot, majd elindítottam a letöltést, végül eredményként egy kevesebb, mint 6 MB-os, azaz hat megabájtos CHM fájlt kaptam. Persze nem annyira szép, mint a teljes MSDN, és azt sem állítom, hogy minden tökéletesen benne van, de legalább ráfér egy pendrive-ra, nem beszélve arról, hogy az index és a keresés nagyságrendekkel gyorsabb. Már csak ezért is megér egy próbát.

Az MSDN Kompetencia Központ oldalára feltöltöttem a PackageThis forrás fájlt (.xml) és a CHM fájlt ehhez a cikkhez, hátha érdekel valakit.

 

VS05-08 migráció: miért nem megy a virtualizáció?

Érdekes jelenség: van egy kódrészletem, ami tökéletesen megy, ha Visual Studio 2005 alatt létrehozott projektbe teszem bele, ám elszáll, ha Visual Studio 2008 alól próbálom használni. A kód a Program Files mappába próbál írni, így valahogy:

    string programFiles = Environment.GetFolderPath( Environment.SpecialFolder.ProgramFiles );
    string subFolder = Path.Combine( programFiles, "MSDNKK teszt" );
    Directory.CreateDirectory( subFolder );

Egyszerű .NET 2.0 System.IO, semmi extra. Kihasználom, hogy Vista alatt működik a mappa virtualizáció, tehát ha nincs is jogom írni a Program Files mappába, a Vista majd létrehozza ezen az útvonalon:

C:UsersbalassyAppDataLocalVirtualStoreProgram FilesMSDNKK teszt

Ez tökéletesen működik VS 2005 alól, ám VS 2008 alatt ugyanez a kód mintha kikerülné a mappa virtualizációt és jön az XP alatt megszokott kivétel:

System.UnauthorizedAccessException: Access to the path ‘C:Program FilesMSDNKK teszt’ is denied.

A megoldás a bin mappában lakik, még pedig alkalmazásnév.vshost.exe.manifest néven – a VS 2008 ugyanis létrehozza ezt a fájlt, a VS 2005 viszont nem. Ez van benne:

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
        <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
        <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
            <security>
                <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
                    <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
                </requestedPrivileges>
            </security>
        </trustInfo>
    </assembly>

A lényeg a requestedExecutionLevel, amit a Studio jó érzékkel asInvoker értékkel hoz létre – ez a javasolt. Ha így hagyjuk, azzal azt jelezzük az operációs rendszernek, hogy jól nevelt Windows alkalmazásról van szó, ami ugyanolyan access tokennel futhat, mint a szülő process. Már pedig egy jól nevelt alkalmazás nem irogat a Program Files mappába és egyéb közös területekre (pl. HKLM) sem, és ha ez így van, akkor a Windows szépen kikapcsolja a virtualizációt!

Nosza próbáljuk meg átállítani ezt az értéket! Választhatunk a lehetséges highestAvailable vagy requireAdministrator közül bármelyiket, virtualizációnk akkor sem lesz! Hiszen pont ez a lényeg, a helyi admin épp a Program Files mappába akar írni.

Ez a kapcsoló azért jó valamire:

asInvoker vs requireAdministrator

Ahogy a pajzs ikon is jelzi, megkaphatjuk érte a consent vagy a credential promptot, attól függően, hogy rendszergazdák vagyunk vagy mezei felhasználók. Adminként pedig már nem lesz szükségünk a virtualizációra.

Már csak egy információval vagyok adós: hogyan lehet mindezt szabályozni Visual Studio 2008-ban. Természetesen a Project Properties ablakban:

Project Properties: manifest

Alapértelmezés szerint ez az opció Embed manifest with default settings értéken áll, ezért hozza létre a Studio a bin mapában a .manifest állományt. Ha ezt ki akarjuk kapcsolni és vissza akarunk állni a Visual Studio 2005 üzemmódra, akkor válasszuk a Create application without a manifest értéket és legalább legyen lelkiismeret furdalásunk, hogy miért nem írunk "jól nevelt" alkalmazást.

Helyette inkább másoljuk le a generált fájlt, nevezzük át mondjuk app.manifestre, vegyük fel a Solution Explorerbe, majd válasszuk ki a fenti ablakban. Így jól kézben tarthatjuk ezt a beállítást és ha valóban írni akarunk közös mappákba vagy registry kulcsokba, akkor rajzoltassuk oda azt a pajzsot, hadd lássa ország-világ, hogy ennek az alkalmazásnak admin jog kell!

A requestedExecutionLevel beállítással kapcsolatban bővebb információ az MSDN UAC példájában található, a cikkhez tartozó példakód pedig letölthető az MSDN Kompetencia Központ oldaláról.

 

Technorati tags: , ,

A .NET Framework leghasználhatatlanabb osztálya

Ezennel szavazást indítok A .NET Framework leghasználhatatlanabb osztálya megtisztelő cím elnyerésére. A jelölteket az MSDN Kompetencia Központ oldalán, a cikkhez kapcsolódó hozzászólások formájában tudjátok beküldeni.

Az én favoritom a .NET Framework 2.0-ban megjelent System.Security.SecureString osztály. Ezt az osztályt annyira fontosnak szánták a Framework tervezői, hogy mindjárt az mscorlibbe került – a szándék jó, az eredmény nem egészen.

Kezdjük talán azzal, hogy mire jó ez az osztály. Ha a System.String osztályt használjuk bizalmas információk tárolására, több biztonsági problémába is ütközhetünk:

  1. A String titkosítás nélkül tárolja az adatokat a memóriában.
  2. A String példányokat a GC takarítja a memóriából, amikor úgy gondolja, hogy épp itt az ideje a memória rendbe tételének. Azaz a bizalmas adatunk ki tudja meddig marad a memóriában.
  3. A String megváltoztathatatlan (immutable), azaz a legtöbb sztring manipulációs művelet újabb String példányokat hoz létre, amiket szintén sújtanak az 1. és 2. pontok.

Ezeknek a problémáknak a megoldására vezették be a SecureString osztályt, ami a következőket tudja:

  • A sztring DPAPI-val titkosítva kerül a memóriába és az osztály a módosítások során is gondoskodik a tartalom titkosításáról.
  • A SecureString implementálja az IDisposable interfészt, azaz meg tudjuk határozni, hogy mikor semmisüljön meg az osztály egy példánya.
  • A MakeReadOnly metódus segítségével írásvédetté tehető a tartalom.

A cél tehát, hogy megszabaduljunk a String osztálytól és helyette a SecureString osztályt használjuk, ha bizalmas adatokról van szó. Vigyázat, ez magában hordozza azt, hogy még átmenetileg sem használhatunk String példányokat! Épp itt kezdődnek a gondok, ugyanis az osztály tervezői mindent megtettek azért, hogy erre kényszerítsenek minket…

Először is nem lehet az osztályt "normálisan" inicializálni, az üres konstruktoron kívül ugyanis csak egy char*-ot (nem írtam el, valóban kar csillagot) váró unsafe konstruktora van. Idegesítő, hogy nem lehet közvetlenül Stringből inicializálni, de persze érthető, hiszen ha lehetne, akkor megint ott lennénk, ahol a part szakad, string példányt használnánk. Maradnak tehát az AppendChar, InsertAt, SetAt és RemoveAt metódusok – ez lesz az a pont, ahol majd a kedvünk kezd elmenni az egésztől….

Nézzük a másik irányt: hasonló okokból a SecureString nem hagyja, hogy Stringgé alakítsuk. Persze ki lehet belőle nyerni a tartalmat, de nem lesz barátságos. Ki írna ilyen kódot önszántából:

IntPtr bstr = Marshal.SecureStringToBSTR( mySecureString );
string notSecureAnyMore = Marshal.PtrToStringUni( bstr );

Tehát se be, se ki, hiszen épp ez az, amit el kell kerülnünk. Semmi gond, használjunk mindenhol String helyett SecureString példányokat!

Na, épp itt van az eb elhantolva: Keith Brown már 2005. januárjában megjelent cikkében, tehát már jóval a .NET 2.0 megjelenése előtt rámutatott, hogy ez az osztály önmagában használhatatlan, ha más Framework osztályok nem építenek rá. Ő épp az SqlConnection osztályt hozza fel példaként, de pontosan ugyanez a helyzet a DirectoryEntry vagy hogy még durvábbat mondjak, a NetworkCredential osztályokkal. Nem csak 2005-ben, hanem most, cirka 3 évvel később is!

Vannak persze osztályok, ahol a Microsoft igenis felhasználta az új lehetőséget, a Process már ismeri, ott van az X509Certificate2 is (biztos nagyon fontos és hasznos), de találtam még néhány SQL Server 2005-tel kapcsolatos SMO osztályt is, ami már SecureString paramétert vár. Úgy néz ki, hogy megalkották az új osztályt, egy-két másikban bizonyították, hogy életképes, azután elfelejtették. Méghozzá annyira, hogy a teljesen új fejlesztésekben sem használták fel.

Íme néhány példa: az Office 2007 VSTO-ban a Document.Password és a Workbook.Password sima string. Az új Microsoft.Web.Administration névtérben a VirtualDirectory.Password szintén mezei string. Vagy akár a .NET 3.0-ban is keresgélhetünk: System.ServiceModel.Security névtérben a UserNamePasswordClientCredential.Password tulajdonságról ordít, hogy SecureStringnek kellene lennie, de persze ez sem az.

Ez azonban mind csak a felszín, az igazi probléma sokkal mélyebben található és átjárja az egész Frameworköt. Már onnan kezdve SecureStringet kellene használni, hogy a felhasználó megad egy bizalmas információt, tehát már közvetlenül a felhasználói felületnél. Erre mit találok a PresentationFramework.dll-ben, ami a majdnem vadiúj WPF része? A PasswordBox kontroll Password tulajdonsága sem SecureString! A SharePoint csapat persze megoldotta, a WSS 3.0-ban a Microsoft.SharePoint.WebControls névtérben van egy PasswordTextBox kontroll, aminek SecurePassword tulajdonsága SecureString, tehát a feladat még weben sem lehetetlen. De persze a Console osztály még hírből sem hallott róla.

Ahhoz, hogy az egész használható legyen, az összes érintett vezérlőt át kellene írni, az egyszerű TextBoxtól kezdve a CreateUserWizardig. Így az adat bekérése már biztonságosnak mondható lehetne. Azután persze meg kellene oldani a tárolást, hogy a titkosítás feloldása nélkül tudjam elmenteni az adatokat akár adatbázisba, akár egy konfig állományba és persze vissza is olvasni onnan. Szintén érintett még az összes kommunikációs protokoll illetve az azokat elfedő osztályok (pl. a DirectoryEntry).

Hogy mindezt miért nem valósította meg a Microsoft? Nem tudom, de arra tippelnék, hogy azért, mert nem elég felügyelt kódban kiegészíteni ezeket az osztályokat, bizony teljesen át kell dolgozni őket, méghozzá natív kód bevonásával, hogy még belül, átmenetileg se használjanak string példányokat. A natív kód bevonása pedig köztudottan megsokszorozhatja a szükséges fejlesztési időt.

Ezek után mondja meg nekem valaki, hogyan használjam a SecureString osztályt?!? Aki alkalmazta már, kérem adjon tanácsot, különben az én szememben valóban ez marad a .NET Framework leghasználhatatlanabb osztálya.

 

A cikkhez tartozó példakód letölthető az MSDN Kompetencia Központ oldaláról.

 

Technorati tags: , , ,