2007. november havi bejegyzések

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.

 

Linkblog: .NET 3.5 és Visual Studio 2008 momentum

Bizonyára már mindenkihez eljutott a hír, hogy "elkészült a nagy mű, igen", megjelent a .NET Framework 3.5 és a Visual Studio 2008 verziója. Az első levél erről hétfő délelőtt 11-kor jött a Regional Directors levelezőlistára, fél órával később már le is halt az MSDN – gondolom mindenki rávetette magát a letöltésre. A hibaüzenetek között volt sokat – talán túlságosan is sokat – mondó:

Server error

És volt kissé szűkszavú is:

MSDN 2a 2007-11-19

Azt hiszem ez elég jól jelzi a Visual Studio népszerűségét 🙂 Át is szervezték az MSDN letöltőrendszert, hogy kevesebb oldal lekéréssel lehessen megszerezni a friss kincset: kitették a kezdőoldalra Top Downloads címszó alatt. Csakhogy sajnos a letöltéshez egy speciális letöltő kliensre van szükség, ami nekem elég kezelhetetlennek bizonyult. Miután sikerült letölteni az ISO image-et, azzal a problémával találtam szembe magam, hogy fogalmam sincs, hogy hova került: az általam megadott mappában nem volt, de a szabad hely jelentősen csökkent a diszken, tehát valahol ott kellett rejtőznie.

Az XP-hez képest a Vistában szinte használhatatlan a fájl kereső, különösen az SP1 RC telepítése után – képtelen volt megtalálni egy bazi nagy ISO fájlt. Maradt tehát a jó öreg parancssor:

dir *.iso /s

A fájl pedig a világ talán leglehetetlenebb mappájába került:

C:UsersbalassyAppDataLocalMicrosoftWindowsTemporary Internet FilesVirtualizedCUsersbalassy

 

Mit is lehet letölteni és elolvasni:

Jó hír, hogy nem kell külön letölteni a VSTO-t, az már a Visual Studio 2008 része. Szintén pozitív újdonság, hogy a Team Explorer telepítője megtalálható a VS telepítő DVD-n a TFC mappában. Bár a setup.exe-t külön kell elindítani és nem éppen villámgyors, még mindig jobb, mintha külön kellene TFS telepítőt szerezni, mint a 2005-ös verziónál. Az MSDN Library szintén megtalálható a VS 2008 telepítő DVD-n, de külön is letölthető az MSDN előfizetői oldaláról.

 

Hova kattintottam?

Gyakori feladat, hogy megjelenítjük az adatokat egy weboldalon és szeretnénk biztosítani a felhasználónak, hogy valamelyik rekordra kattintva ilyen vagy éppen olyan műveletet végezzen el az adott soron. Valami oknál fogva a szokásos fejlesztői hozzáállás a feladathoz az, hogy a gomb kattintás eseménykezelőjében elkezdjük keresgélni, hogy vajon melyik rekordra kattinthatott szerencsétlen felhasználó és ilyenkor az ember hajlamos elveszni a SelectedItem, SelectedIndex, DataItem, DataRow, DataRowView, DataRowItem és társai között.

Ennél sokkal szerencsésebb hozzáállás, ha a gomb eleve tudja magáról, hogy melyik rekordhoz tartozik!

Íme egy példa, még pedig Visual Studio 2008-cal, Northwind adatbázissal és természetesen LINQ-kel. A VS 2008 remek grafikus dizájnerében létrehoztam egy LINQ to SQL adatmodellt NorthwindDataContext néven, majd ehhez konfiguráltam egy LinqDataSource vezérlőt egy ASPX oldalon:

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

Ezek után felhasználtam az új ListView vezérlőt, hogy megjelenítsem az adatokat az adatforrás segítségével. A célom az volt, hogy az adatok egy felsorolásos listában jelenjenek meg és mindegyik mellett legyen egy Összegez és egy Számol feliratú gomb, amivel az adott termékhez tartozó megrendeléseket tudom megszámolni és összesíteni. Ezekhez a műveletekhez definiáltam két gombot:

  <asp:ListView ID="lvProducts" runat="server" 
DataSourceID="ldsNorthwind" onitemcommand="lvProducts_ItemCommand"> <LayoutTemplate> <ul> <asp:PlaceHolder ID="itemPlaceholder" runat="server" /> </ul> </LayoutTemplate> <ItemTemplate> <li> [<asp:LinkButton id="btnSum" runat="server" Text="Összegez"
CommandName="Sum" CommandArgument='<%# Eval( "ProductID" ) %>' />] [<asp:LinkButton id="btnCount" runat="server" Text="Számol"
CommandName="Count" CommandArgument='<%# Eval( "ProductID" ) %>' />] - <%# Eval( "ProductName" ) %> </li> </ItemTemplate> </asp:ListView>

A gombnyomások kezeléséhez feliratkoztam a ListView ItemCommand eseményére. Hogy egyszerűen le tudjam kérdezni ebben az eseménykezelőben, hogy melyik gombra kattintott a felhasználó, a két gombnak eltérő CommandName értéket állítottam be. Bár szerver oldalon természetesen le lehet kérdezni a gombok feliratát is, azzal a gond, hogy azt lokalizálhatjuk, az ID értéke pedig szerintem nem annyira beszédes a kódban.

A lényeg: a gombok CommandArgument tulajdonságához odakötöttem a rekordot egyedileg azonosító ProductID mező értékét. Mivel a CommandArgument értékét egyszerű lekérdeznem az eseménykezelőben, így nem kell azzal küzdenem, hogy vajon melyik sorra kattintott a felhasználó, sőt a sor száma nem is érdekel, rögtön megkapom a rekord egyedi azonosítóját, ahonnan már csak egy ugrás a teljes rekord megszerzése.

Az ItemCommand eseménykezelő pedig így sikerült:

  protected void lvProducts_ItemCommand( object sender, ListViewCommandEventArgs e )
  {            
    NorthwindDataContext dc = new NorthwindDataContext();
    Product product = dc.Products.First( p => p.ProductID == Convert.ToInt32( e.CommandArgument ) );

    string result = default( string );

    switch( e.CommandName )
    {
      case "Sum":
        result = String.Format( "{0:c}", product.OrderDetails.Sum( o => o.Quantity * o.UnitPrice ) );
        break;
      case "Count":
        result = String.Format( "{0} darab", product.OrderDetails.Count() );
        break;
    }

    this.litResult.Text = result;
  }

Kihasználom, hogy az e.CommandArgument a rekord egyedi azonosítója és LINQ to SQL szintaktikával egyetlen sor kódot igényel a ProductID alapján egy Product példány megszerzése. Kihasználom, hogy az e.CommandName jól megkülönbözteti egymástól a két gombot és ezzel a két elvégzendő műveletet is, ami itt a példában egy összegzés és egy számlálás.

Ennél egyszerűbben talán csak úgy lehetne megoldani a feladatot, ha az egyes ListView rekordokba nem csak az ID-t, hanem a teljes objektum példányt el tudnánk menteni, azonban ezt valószínűleg sávszélesség okokból amúgy sem akarnánk.

A lényeg: ne keresgéljük a cél rekordot, mondja el az, hogy mit akar.

A teljes forráskód letölthető az MSDN Kompetencia Központ honlapjáról.

 

Workflow Foundation hosztolása ASP.NET-ben

Hear me speak at TechEd Developers 2007 Ha már Lipi volt olyan kedves és Világszám! címmel blogbejegyzést írt a TechEd előadásomról, igazán tartozom némi bővebb információval az itthon maradottak számára.

A dolog úgy kezdődött, hogy a tavaly novemberi MSDN konferencia után jobban beleástam magam a Windows Workflow Foundation és az ASP.NET kapcsolatába, amiről pár rövidebb blogbejegyzést írtam is (lásd hosting és threading). Mélyebbre ásva magam a témába hamar kiderült, hogy WF-et ASP.NET-ben hosztolni bár lehet, csak mazochistáknak ajánlott. Erőltetés a köbön. Sokkal ésszerűbbnek tűnik helyette egy Windows service-t írni és a webalkalmazásból remotingon keresztül kommunikálni vele.

Akárhogy is, nem lehet elmenni amellett, hogy a Microsoft szerint a WF minden fajta CLR AppDomainben hosztolható. Bár ez kétségkívül igaz és a VS által generált projekt típusokban simán működik is, ASP.NET-ben visszatérő fejfájást okozhat. Végigküzdve magam némi kódon rájöttem, hogy nem egyszerűen a hosztolás a gond. A probléma részét képezi az, hogy a webfejlesztők nem akarnak tudni a WF-ről, a workflow fejlesztők pedig nem akarják, hogy az ASP.NET guruk belepiszkáljanak a kódjaikba. A hosztolás technikai igényei mellett figyelembe venni ezt az igényt is, nem egyszerű olyan architektúrát találni, ami ideálisnak mondható. Az ExternalDataExchange-re épülő kommunikáció annyira nyakatekert, hogy nem triviális szétvágni a kódot fejlesztői szerepkörök és felelősség szerint.

Összeszedtem néhány kényes területet, és erről beadtam egy előadás javaslatot a TechEdre, amit el is fogadtak:

  • WF és ASP.NET technológiák összekapcsolása
  • WF és ASP.NET specifikus kódok szétválasztása
  • Szálkezelés
  • Hosszú ideig futó (long running) folyamatok
  • Szekvenciális vagy állapotgép?
  • Hosszú ideig futó activity-k
  • Eseményvezérelt activity-k
  • Kommunikáció
  • Monitorozás

Ha pedig már ott voltam, akkor "jól kihasználtak" és még egy másik előadásra is felkértek a workflow kommunikációs infrastruktúrával kapcsolatban. Ha van rá igény, egyszer szívesen leírom, hogy milyen egy ilyen méretű konferencia előadói szemmel.

Visszatérve az eredeti témára, az Avoiding Pitfalls When Hosting Windows Workflow Foundation in Real World ASP.NET Applications című előadás úgy látszik többeket szíven talált, mert sokan kérték a demó alkalmazás forráskódját, amit az MSDN Kompetencia Központ oldalán meg is osztok minden érdeklődővel. A példa egy ASP.NET standard regisztrációs oldalt egészít ki egy workflow-val, amivel ellenőrizzük a felhasználó e-mail címét és beépítünk még egy manuális jóváhagyást vagy elutasítást is. Nem bonyolult, de sok problémát érint. Lehet benne példát találni nem csak a hosztolásra, hanem a kommunikációra, állapotgépre és hosztolásra is, sőt még arra is, hogy a folyamat aktuális állapotát hogyan jeleníthetjük meg egy weboldalon grafikusan.

Minden visszajelzést szívesen fogadok!