LINQ címkéhez tartozó bejegyzések

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: ,

XML adatkötés a gyakorlatban

Korábban már írtam arról, hogy az MSDN Kompetencia Központ RSS feedjét a FeedBurner szolgáltatás segítségével mérjük. A FeedBurner biztosít egy webes felületet a statisztikai adatok lekérdezéséhez, de ezen kívül egy REST-es API-t is ad, ami lehetővé teszi, hogy közvetlenül saját alkalmazásunkban jelenítsük meg ezeket az adatokat.

Az Awareness API legegyszerűbb metódusának meghívásához egy HTTP GET kérést kell küldenünk a szervernek az alábbi formában:

http://api.feedburner.com/awareness/1.0/GetFeedData?uri=myfeeduri

A válasz pedig az alábbi szerkezetű XML:

<?xml version="1.0" encoding="UTF-8"?>
<rsp stat="ok">
    <!-- This information is part of the FeedBurner Awareness API. 
         If you want to hide this information, you may do so via your FeedBurner Account. -->
    <feed id="1352372" uri="myfeeduri">
        <entry date="2008-06-01" circulation="27" hits="161" />
    </feed>
</rsp>

A lényeg természetesen a date, circulation és hits attribútumokban van, a feladat ezeknek a megjelenítése. Egyedül arra kell odafigyelnünk, hogy a válasz csak abban az esetben érvényes, ha a stat attribútum értéke ok. Ellenkező esetben kissé eltérő a válasz felépítése és a hibaüzenetet egy err elem msg attribútumában kapjuk meg.

LINQ to XML

Miután van már LINQ XML-hez is, csábító, hogy ezzel oldjuk meg a feladatot. Ehhez mindössze ennyi kódot kell írnunk:

    XDocument doc = XDocument.Load( url );
    string status = doc.Root.Attribute( "stat" ).Value;

    if( status.Equals( "ok", StringComparison.OrdinalIgnoreCase ) )
    {
        XElement entry = doc.Descendants( "entry" ).First();
        string date = entry.Attribute( "date" ).Value;
        string circulation = entry.Attribute( "circulation" ).Value;
        string hits = entry.Attribute( "hits" ).Value;

        // TODO: Megjelenítés vezérlőkben...
    }
    else
    {
        string error = doc.Descendants( "err" ).First().Attribute( "msg" ).Value;

        // TODO: hibaüzenet megjelenítése
    }

Ez persze jól működik, de mégis elgondolkodtam azon, hogy mindezt meg lehet-e oldani deklaratív módon, C# kód nélkül?

XML adatkötés

A jó hír az, hogy pontosan erre szolgál az XML adatkötés, ami már ASP.NET 2.0 óta elérhető, csak éppen szeretünk megfeledkezni róla. Ha mégis eszünkbe jut, akkor hajlamosak vagyunk hierarchikus adatszerkezetekkel társítani és csak hierarchikus vezérlőkben, menü vagy fa kontrollokban gondolkodni. Pedig az XML adatkötés tökéletesen működik GridView vagy az új ListView vezérlőkkel is.

Először szükségünk lesz egy XmlDataSource vezérlőre:

    <asp:XmlDataSource ID="xdsFeedService" runat="server" 
        XPath="//rsp/feed/entry" 
        DataFile="http://api.feedburner.com/awareness/1.0/GetFeedData?uri=myfeeduri" />

A DataFile paraméterben akár HTTP-s URL-t is megadhatunk, az XPath attribútum pedig arra szolgál, hogy a válasz XML-ból kiemeljünk egy részt egy XPath kifejezés segítségével, jelen esetben az entry tag-et.

A megjelenítéshez egy ListView vezérlőt használhatunk, ezzel ugyanis pontosan kézben tartható a generált HTML kód. Például így:

    <asp:ListView runat="server" DataSourceID="xdsFeedService">
      <LayoutTemplate>
        <asp:PlaceHolder runat="server" ID="itemPlaceholder" />
      </LayoutTemplate>
        
      <ItemTemplate>
        <asp:PlaceHolder runat="server" 
          Visible='<%# XPath("//rsp/@stat").ToString().Equals( "ok", StringComparison.OrdinalIgnoreCase ) %>'>                
          Date: <asp:Literal runat="server" Text='<%# XPath("@date") %>' />
          <br />
          Circulation: <asp:Literal runat="server" Text='<%# XPath("@circulation") %>' />
          <br />
          Hits: <asp:Literal runat="server" Text='<%# XPath("@hits") %>' />                        
        </asp:PlaceHolder>
                
        <asp:PlaceHolder runat="server" 
          Visible='<%# !XPath("//rsp/@stat").ToString().Equals( "ok", StringComparison.OrdinalIgnoreCase ) %>'>
          Error: <asp:Literal runat="server" Text='<%# XPath("//rsp/err/@msg") %>' />
        </asp:PlaceHolder>
      </ItemTemplate>
    </asp:ListView>

Néhány érdekesség a fenti kódból:

  1. A ListView vezérlőnek szüksége van egy itemPlaceholder azonosítójú vezérlőre az oldalon, amit én egy LayoutTemplate megadásával oldottam meg.
  2. Az XML adatkötéshez nem az Eval, hanem az XPath metódust kell használnunk, aminek egy XPath kifejezést kell megadnunk. Mivel az XmlDataSource-ban már leszűkítettük a válasz XML-t, most olyan XPath kifejezéseket kellett írnom, amelyek ebben a leszűkített kontextusban értelmezhetőek. A @hits például az aktuális entry elem hits attribútumára vonatkozik, míg a //rsp/err/@msg megadásával gyökér relatív útvonalat adtam meg.
  3. Az ItemTemplate-et kettébontottam két Placeholder segítségével, és amennyiben a válasz helyes az elsőt jelenítem meg, amennyiben pedig hibaüzenetet kapunk vissza, a második Placeholdert használom. Az elrejtéshez szintén XML adatkötést használtam, a Visible tulajdonságban szereplő kifejezés a stat attribútum értékét vizsgálja.
  4. Minden esetben a legegyszerűbb vezérlőt használtam, nem Labelt és Panelt, hanem Literalt és Placeholdert. Ezek nem generálnak extra kódot a kimenetbe, de támogatják az adatkötést.

Érdemes megfigyelni, hogy ez a megoldás teljesen deklaratív, nem igényel C# kódot.

Felmerülhet a kérdés, hogy akkor most melyik a jobb? Én általában jobban szeretem a deklaratív megoldást, mert számomra általában áttekinthetőbb és így a kód karbantarthatóbb. Webalkalmazásról lévén szó, természetesen a teljesítmény is fontos szempont, marad tehát a mérés, mérés, mérés.

 

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: , , ,

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: , , , , ,

LinqDataSource mint ObjectDataSource

A LINQ óriási előnye, hogy a relációs adatainkat szinte észrevétlenül fordítja át objektumok halmazára, emiatt a LinqDataSource inkább rokon az ObjectDataSource vezérlővel, mint az SqlDataSource-szal. Íme egy példa ennek illusztrálására.

Vegyük a Northwind adatbázis Products tábláját és egy SqlDataSource segítségével jelenítsük meg belőle a ProductID és ProductName mezőket, valahogy így:

    <asp:GridView ID="gvProducts" runat="server" 
DataKeyNames="ProductID" DataSourceID="sdsProducts" AutoGenerateColumns="false" onrowdatabound="gvProducts_RowDataBound"> <Columns> <asp:BoundField DataField="ProductID" HeaderText="Azonosító" /> <asp:HyperLinkField
DataNavigateUrlFields="ProductID"
DataNavigateUrlFormatString="Products.aspx?id={0}" DataTextField="ProductName"
HeaderText="Termék" /> </Columns> </asp:GridView>
<asp:SqlDataSource ID="sdsProducts" runat="server"
ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>" SelectCommand="SELECT * FROM [Products]" />

Legyen az a feladat, hogy a ProductName mező linkként viselkedjen, ha a Discontinued mező értéke true, egyébként pedig csak jelenjen meg a termék neve, de ne legyen kattintható. Ezért a ProductName mező megjelenítéséhez HyperLinkFieldet használtam és hozzárendeltem egy eseménykezelőt a RowDataBound eseményhez. Ebben az eseménykezelőben az a cél, hogy megszerezzük a Discontinued mező értékét, ami nekem csak elég körülményesen sikerült:

    protected void gvProducts_RowDataBound( object sender, GridViewRowEventArgs e )
    {
        if( e.Row.RowType == DataControlRowType.DataRow )
        {
            DataRowView row = (DataRowView) e.Row.DataItem;
            int productID = (int) row[ "ProductID" ];

            // Product megszerzése ProductID alapján...
        }
    }

Látható, hogy bár burkoltan, de még mindig rekordokkal kell bajlódni.

Próbáljuk ki mindezt egy LinqDataSource segítségével:

    <asp:LinqDataSource ID="ldsNorthwind" runat="server" 
ContextTypeName="NorthwindDataContext" TableName="Products"> </asp:LinqDataSource>

Mivel a LinqDataSource már objektumokat köt, az eseménykezelőnk lényegesen egyszerűbb:

    protected void gvProducts_RowDataBound( object sender, GridViewRowEventArgs e )
    {
        if( e.Row.RowType == DataControlRowType.DataRow )
        {
            Product p = (Product) e.Row.DataItem;

            if( p.Discontinued )
            {
                HyperLink hl = e.Row.Cells[ 1 ].Controls[ 0 ] as HyperLink;
                hl.NavigateUrl = String.Empty;                
            }
        }
    }

Ebben a megközelítésben tehát teljesen elfelejthetjük a rekordokat, objektum gyűjteményekkel dolgozunk, mint az ObjectDataSource esetén. Nem tudom, ki hogyan van vele, nekem ez így szimpatikusabb.

Kiegészítettem a korábbi példát, így már ennek a forrása is megtalálható benne és letölthető az MSDN Kompetencia Központ oldaláról.

 

Hova tűnt az Add?

A minap azzal küzdöttem, hogy Visual Studio 2008 Release Candidate alatt próbáltam működésre bírni egy LINQ to SQL-es projektet, ami Beta 2 alatt készült. Keserves küzdés volt, mert nagyon nem hagyta magát, hiányzott neki a generált entitás osztályokon az Add metódus. Reménykedtem, hogy nem azt képzeli, majd én fogom megírni?! A Beta 2-ben még biztosan volt a Table<T> típusnak Add metódusa, most meg csak ezt kántálja a fordító:

‘System.Data.Linq.Table<AccountRequest>’ does not contain a definition for ‘Add’ and no extension method ‘Add’ accepting a first argument of type ‘System.Data.Linq.Table<AccountRequest>’ could be found (are you missing a using directive or an assembly reference?)

Megjegyzem az jópofa, hogy már itt is megjelentek az extension metódusok 🙂  Irány a gugli meg a nagy koppanás, hiszen a Visual Studio RC változat nem publikus, csak egy aránylag szűk kör kapta meg, így nem túl sok sikerrel kerestem arra, hogy breaking changes.

Kereskényi Roby kellett hozzá, hogy megtudjam, a Beta 2 után bizony átneveztek néhány metódust, mert a régi név nem vot egyértelmű:

  • Add –> InsertOnSubmit
  • AddAll –> InsertAllOnSubmit
  • Remove –> DeleteOnSubmit
  • RemoveAll –> DeleteAllOnSubmit

Így valóban egyértelműbb, hogy mikor mi történik, és talán nem fogjuk elfelejteni meghívni a SubmitChanges metódust sem. De ami még jobb, hogy az OnValidate partial method végre megkapja paraméterben, hogy mikor fut, még pedig egy enumeráció formájában:

partial void OnValidate(System.Data.Linq.ChangeAction action);

ahol a ChangeAction így fest:

  namespace System.Data.Linq {
    public enum ChangeAction {
      None = 0,
      Delete,
      Insert,
      Update
    }
  }

Nem hiába, olvasott embernek párja nincs! Különösen, ha Silverlightról bloggol, lehet kapaszkodni…

 
Technorati tags: , , ,