Tag Archives: LINQ

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

DLINQ: Egyszerű tárolt eljárások esete az MSDN-nel

Egyszerűnek tekinthetjük az alábbi, elég tipikusnak mondható tárolt eljárást, ami visszaadja egy adott tábla rekordjainak számát:

    CREATE PROCEDURE dbo.GetCustomerCount
    AS
        SELECT Count(*) FROM Customers

Amikor megpróbáljuk meghívni, ugyanúgy járunk el, mint máskor, rádobjuk a tárolt eljárást a Visual Studio O/R Designerére, ami generál nekünk egy éppen megfelelő metódust. Szinte eláll a szívünk, amikor meglátjuk az így előállt C# metódust és már hívjuk is lelkesen, hiszen int a visszatérési érték:

    NorthwindDataContext dc = new NorthwindDataContext();
    int i = dc.GetCustomerCount();
    Console.WriteLine( i );

Bár a tárolt eljárás szinte művészien tökéletes, szomorúan konstatáljuk, hogy bizony mindig nullát ad vissza. Nézzük meg a generált metódus belsejét:

    [StoredProcedure( Name = "dbo.GetCustomerCount" )]
    public int GetCustomerCount()
    {
        IExecuteResults result = this.ExecuteMethodCall(
            this,
            (MethodInfo) MethodInfo.GetCurrentMethod() );
        return ( (int) ( result.ReturnValue ) );
    }

A kérdés – amire az MSDN-ből aligha kapunk választ a mostani béta változatban -, hogy mi az az IExecuteResults és mit tud a ReturnValue tulajdonsága? Némi Reflectorozással odáig sikerült eljutnom, hogy a DataContext.ExecuteMethodCall meghívja az aktuális provider ExecuteNonQuery metódusát, de mivel az is interfész alapú (IProvider), így az implementációval először nem sokáig jutottam. (Ezen a ponton arra tippeltem, hogy ez az ExecuteNonQuery ugyanúgy viselkedik, mint az IDbCommand esetén és az "érintett sorok" – rows affected – számát adja vissza. Mint kiderült nem egészen…)

Aztán tovább ásva megtaláltam a System.Data.Linq.SqlClient.SqlProvider osztályt, ahol az ExecuteNonQuery egy SqlExecuteResults típussal tér vissza, ami implementálja az IExecuteResults interfészt. Már csak az maradt kérdés, hogy hogyan kap értéket a ReturnValue tulajdonság? A konstruktorból kiderül, hogy a @RETURN_VALUE paramétertől.

Na de ki beszél itt SQL paraméterről, hiszen ennek a tárolt eljárásnak nincs se input, se output paramétere! Nosza kérdezzük meg, mi megy az adatbázisba, amihez használhatjuk a DataContext.Log tulajdonságát. Az eredmény:

    EXEC @RETURN_VALUE = [dbo].[GetCustomerCount]
    -- @RETURN_VALUE: Output Int (Size = 0; Prec = 0; Scale = 0) NOT NULL []

Hűha! Irány az SQL Server Books Online, hányféleképpen lehet beállítani egy tárolt eljárás visszatérési értékét? Nekem SELECT-tel nem sikerült, maradt a RETURN, amihez át kellett írni a tárolt eljárást, például így:

    DECLARE @Result int
    SELECT @Result = Count(*) FROM Customers
    RETURN @Result

Ez engem zavar, ezért kedves SQL guruk, mondjatok valami egyszerűbbet!

Ami pedig az MSDN-t illeti, egyetlen rövid mondat rengeteg időt megspórolhatott volna. Persze, tudom, béta termék, meg ez csak egy metódus a sok közül, de akkor is. Gondoltam egyet és megkeresetem az ExecuteMethodCall oldalát az MSDN Wikiben: abban reménykedtem, hogy ha ide megírom a felfedezéseimet, az majd segít másoknak és valamilyen formában bekerül az RTM dokumentációba. Nos, erről lepattantam, egyrészt mert úgy látszik a béta dokumentációhoz nem lehet hozzáírni, másrészt pedig ennek a metódusnak a leírása angolul nem található meg, csak japánul smile_sniff Ez remélem átmeneti bug és nem feature smile_sad

 

Technorati tags:

DLINQ: Tárolt eljárások visszatérési típusai (VS trükk)

DLINQ-ből tárolt eljárásokat hívni pofon egyszerű: ráncigálni kell egy kicsit a VS dizájnert, míg ki nem pottyan egy metódus, aminek a szintaktikája éppen a tárolt eljáráséval egyezik meg. Az viszont nagyon nem mindegy, hogy a dizájnerben hogyan kattintgatunk, és mivel a jelenlegi bétában nem sok visszajelzést kapunk, íme egy trükk!

Példaként használjuk a Northwind adatbázist, és akarjuk meghívni a következő nagy bonyolultságú tárolt eljárást:

    CREATE PROCEDURE dbo.GetCustomersInCity
    (
        @City nvarchar( 15 )
    )
    AS
        SELECT * 
        FROM Customers
        WHERE City = @City

Van tehát egy string bemenő paraméterünk, válaszként pedig a Customers tábla rekordjait adjuk vissza. Ennek burkolására úgy készíthetünk DLINQ-es metódust, hogy a Visual Studio "Orcas" O/R Designerében dolgozgatunk, ami akkor jelenik meg, ha Linq to SQL File típusú elemet adunk a projekthez. A dizájner mellé nyissuk ki a Server Explorert, majd ragadjuk meg a tárolt eljárásunkat és dobjuk a dizájner jobb felére, mert az a metódusok helye. Ennek eredménye a következő metódus lesz a DataContext osztályunkban (kicsit kipofozva és leegyszerűsítve):

    [StoredProcedure( Name="dbo.GetCustomersInCity" )]
    public IEnumerable<GetCustomersInCity> GetCustomersInCity( 
        [Parameter( Name="@City" )] string City )
    {
        IQueryResults<GetCustomersInCity> result = 
            this.ExecuteMethodCall<GetCustomersInCity>( 
                this, 
                (MethodInfo) MethodInfo.GetCurrentMethod(), 
                City ); 
        return (IEnumerable<GetCustomersInCity>) result;
    }

A problémát a piros aláhúzott típusok jelentik: a VS lelkesen generált egy új osztályt, aminek olyan tulajdonságai vannak, mint amilyen oszlopokkal a tárolt eljárás visszatér és azt elnevezte GetCustomersInCity-nek. Hagyjuk figyelmen kívül egy pillanatra ennek a típusnak a közel sem bájos nevét és koncentráljunk inkább arra, hogy ez nem a Customer típus, hanem egy ugyanolyan szerkezetű, de másik típus! Ugyanez történik akkor is, ha a dizájnerben már van Customer, a VS nem képes rájönni, hogy a kettő szemantikailag azonos.

Lelkesen felcsaphatjuk a Properties ablakot és örülhetünk, amikor meglátjuk, hogy létezik egy Return Type nevű tulajdonság, aminek az értéke éppen (Auto-generated Type), ám az örömünk hamar elillan, amikor rájövünk, hogy ez bizony read-only.

A rejtély kulcsa abban rejlik, hogy nem mindegy, hova ejtjük a Server Explorerből kifogott tárolt eljárást! Előbb tegyük be a Customers táblát a dizájner bal felére, majd utána fogjuk meg a tárolt eljárást és ne a dizájner jobb felére tegyük, hanem a Customers tábla felett engedjük el az egérgombot. Voila, a fenti kódrészletben az aláhúzott generált típus helyett mindenhol Customer fog szerepelni!

Még egy tanulság a fenti kódrészletből: ha azt akarjuk, hogy a metódus paramétereinek casingjére az FxCop ne panaszkodjon, akkor használjunk a tárolt eljárásokban kisbetűs paraméter neveket, különben kénytelen leszünk módosítani a Parameter attribútum Name tulajdonságát.

 

Technorati tags:

Dlinq alapok és SqlMetal, a Dlinq rabszolga

A márciusi Orcas CTP-t elindítva eleinte nehézkesnek tűnik a LINQ-kel történő ismerkedés, mert a 2006. májusi előzetestől eltérően itt már nincs külön LINQ projekt típus a Visual Studioban. 

Íme a szükséges első lépések:

  1. Hozzunk létre egy új konzol alkalmazást, például LinqApp néven.
  2. Referenciaként adjuk a projekthez a System.Data.Linq.dll-t, amit a C:WINDOWSMicrosoft.NETFrameworkv3.5.20209 mappában fogunk megtalálni. Érdekes módon a VS az Add Reference ablak .NET fülén nem sorolja fel, ezért használjuk a Browse fület és keressük meg a DLL-t.
  3. Júzingoljuk a System.Data.Linq névteret.

Miután ezzel így megvagyunk, bele is vethetjük magunkat a DLINQ-be. Ehhez először is adatbázisra lesz szükségünk, legyen a jól ismert Northwind, ami innen tölthető le.

Ahhoz, hogy a Northwindben lévő partner adatokat elérhessük, készítsünk egy Customer osztályt, ami C# szinten definiál egy partnert. A partnernek legyen ContactName, ContactTitle és City tulajdonsága, ezeket ugyanis tartalmazza az adatbázisban lévő Customers tábla.

Fontos, hogy az osztályunkat fel kell címkéznünk attribútumokkal, melyek azt jelzik, hogy ez az osztály melyik táblához tartozik és az egyes tulajdonságok a tábla mely oszlopához tartoznak. Ehhez a Table és Column attribútumokat használhatjuk, melyek a System.Data.Linq névtérben találhatóak. (Az Orcas VPC-ben lévő MSDN-ben megtalálhatjuk az osztályok összes tulajdonságát, itt csak a legegyszerűbbeket fogjuk használni.)

Ha mindent jól csináltunk, az osztály végül valahogy így fest, kihasználva az automatikus property gyártás új szintaxisát:

    using System;
    using System.Data.Linq;

    namespace LinqApp
    {
        [Table( Name = "Customers" )]
        class Customer
        {
            [Column]
            public string ContactName { get; set; }

            [Column]
            public string ContactTitle { get; set; }

            [Column]
            public string City { get; set; }
        }
    }

Definiáltuk a "sémát", már csak adatokkal kellene feltöltenünk. Ehhez persze kell egy connection string:

    string connStr = @"Data Source=.;Initial Catalog=Northwind;Integrated Security=True;";

Az objektumtér és az adatbázis között a kapcsolatot egy DataContext példány teremti meg, amely hasonló az ADO.NET Connection objektumához, ez ismeri például a connection stringet:

    DataContext dc = new DataContext( connStr );

A DataContext ojjektumtól kérhetjük el a táblákat is:

    Table<Customer> customers = dc.GetTable<Customer>();

A táblákon pedig lekérdezéseket definiálhatunk:

    var query = from c in customers        
                orderby c.ContactName
                where c.City == "London"
                select c;

A lekérdezés eredményét pedig feldolgozhatjuk, például így:

    foreach( Customer c in query )
    {
        Console.WriteLine( "{0}t{1}", c.ContactName, c.ContactTitle );
    }

Előfordulhat, hogy fordítás közben a következő hibaüzenetet kapjuk:

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

A megoldás:

    using System.Linq;

Ezzel készen is van az első DLINQ alkalmazásunk!

 

Néhány dolgot mindenképp érteni kell:

1. Mi az a var, oda a típusosság?

Szó sincs róla! A var kulcsszó annyit jelent, hogy nem mondom meg a típust, kedves fordító, találd ki a jobb oldalról. A fordító tehát olyan típusúvá fogja tenni query változót, amilyennek azt a lekérdezés meghatározza. A lekérdezések egyébként System.Linq.IQueryable típusúak, ami lényegében egy felturbózott IEnumerable, ezért lehet forícselni.

2. Mikor fut a lekérdezés?

A LINQ egyik alapelve az úgynevezett deferred execution, azaz késleltetett lekérdezés, melynek lényege, hogy a lekérdezések nem akkor futnak, amikor definiáljuk őket, hanem amikor az eredményüket fel akarjuk dolgozni. Mivel a lekérdezések IEnumerable származékok, valójában akkor futnak, amikor végigiterálunk rajtuk.

Ezt egyébként a VS is tudja! Tessék betenni egy breakpointot a var és a foreach közé és megnézni a query változót az Autos ablakban! Az eredmények kibontása előtt ott a figyelmeztetés:

Expanding the Results View will enumerate the IEnumerable.

És valóban, ha SQL Profilerrel megnézzük, valóban csak akkor kap kérést az adatbázis szerver.

3. Milyen SQL utasításokra fordul a lekérdezés?

Ha nem akarunk SQL Profilerezni, akkor legegyszerűbben úgy tudhatjuk meg, hogy mit kell az adatbázisnak megennie, ha használjuk a DataContext Log tulajdonságát, aminek bármilyen TextWritert átadhatunk:

    dc.Log = Console.Out;

Jelen esetben ezt kapja meg az SQL Server:

    SELECT [t0].[ContactName], [t0].[ContactTitle], [t0].[City]
    FROM [Customers] AS [t0]
    WHERE [t0].[City] = @p0
ORDER BY [t0].[ContactName] -- @p0: Input NVarChar (Size = 6; Prec = 0; Scale = 0) NOT NULL [London]

Az IQueryable belső reprezentációja egy Expression objektum, amely az ún expression tree-t tartalmazza, tehát akár ezt is írhattuk volna (ha esetleg kedvelnénk a lambda expressionöket):

    var query = customers.OrderBy( c => c.ContactName ).Where( c => (c.City == "London") );

4. Tényleg minden táblához kell írnom egy osztályt?

Osztály kell, de nem kell megírni, van ugyanis osztálygyártó rabszolga, úgy hívják: SqlMetal.exe. Ez a kis jószág a C:Program FilesMicrosoft Visual Studio 9.0SDKv3.5Bin mappában található, ami sajnos nincs bent a PATH-ban a március CTP image-ben, így vagy hozzávesszük, vagy nyitunk egy SDK command promptot és belépünk a Bin almappába. Az alkalmazás egyékbént egyetlen 98KB-os exe, amit át is másolhatunk a projekt mappánkba.

Van neki jó sok paramétere, a mi példánkban használhatjuk így:

    SqlMetal.exe /server:. /database:Northwind /code:Northwind.cs /namespace:LinqAppMetal /pluralize

Azt hiszem minden kapcsoló magáért beszél, a gyakorlatban még valószínűleg a /sprocs kapcsolót fogjuk használni, amivel tárolt eljárások hívását is egy csapásra metódus hívásokká burkolhatjuk. A generált fájlban találunk egy DataContextből származó Northwind osztályt, illetve minden egyes táblának egy saját osztályt. Érdemes megnézni, hogy miben más a generált Customer osztály, ahhoz képest, amit mi írtunk!

Az SqlMetal által generált 2599 sornyi forráskódot felhasználva a következőre egyszerűsödik az adatbázis lekérdezésünk:

    Northwind dc = new Northwind( connStr );
    var query = from c in dc.Customers
                orderby c.ContactName
                where c.City == "London"
                select c;

Így már szerintem sokkal barátságosabb a feladat.

Technorati tags: ,