XML címkéhez tartozó bejegyzések

GUID definiálása XSD-ben

XML dokumentumokban gyakran előfordul, hogy egy elem vagy attribútum GUID értéket tartalmaz. Mivel egy XML a hozzá tartozó XSD nélkül fabatkát sem ér, nem ritka feladat, hogy XSD-ben elő kellene írnunk, hogy az adott érték csak GUID lehet. Az XSD sokféle beépített típust támogat, de sajnos GUID nincs, így marad a klasszikus regexes megoldás:

<xs:schema ...>

  <xs:simpleType name="guid">
    <xs:restriction base="xs:string">
      <xs:pattern value="[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-
[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" />
    </xs:restriction>
  </xs:simpleType>

Ha ez megvan, akkor pontosan úgy hivatkozhatunk a saját guid típusunkra, mint a beépítettekre:

<xs:attribute name="id" type="guid" use="required" />

Érdemes megfigyelni, hogy a regex elején és végén nincs $ és ^ jel, ugyanis a minta mindig a teljes értékre vonatkozik.

 

Technorati-címkék: ,

RSS generálás ASP.NET webalkalmazásban WCF alapokon

Webalkalmazások fejlesztésekor gyakran merül fel az igény, hogy a tartalmat RSS formában is publikálnunk kell. Erre számtalan megoldás létezik, a .NET Framework 3.5 megjelenése óta azonban közvetlen támogatást ad a keretrendszer a System.ServiceModel.Syndication névtér osztályainak segítségével.

A névtér nevéből látszik, hogy a WCF részeként kapjuk ezt a szolgáltatást és a weben számtalan példát látunk arra, hogy akár egy konzol alkalmazásban hogyan hozhatunk létre egy WCF alapú szolgáltatást, amely RSS formában válaszol, arra azonban alig van utalás, hogy ez a funkció közvetlenül használható egy ASP.NET-es webalkalmazásban is.

A nem-HTML jellegű válasz előállításához szerintem leginkább IHttpHandlert érdemes használni:

Visual Studio Add New Item: Generic Handler

Az IHttpHandler legfontosabb metódusa a ProcessRequest, amelyben közvetlenül a HttpContextbe kell beleírnunk a HTTP választ. Ha RSS 2.0 formában akarjuk visszaküldeni a választ és szeretnénk az elejére XML fejlécet, akkor azt megtehetjük például így:

    public void ProcessRequest( HttpContext context )
    {
        // XML válasz típus beállítása.
        context.Response.ContentType = "application/xml";

        // XML fejléc beírása a válasz elejére.
        XmlTextWriter writer = new XmlTextWriter( context.Response.Output );
        writer.Formatting = Formatting.Indented;
        writer.WriteProcessingInstruction( "xml", @"version=""1.0"" encoding=""utf-8""" );

        // SyndicationFeed sorosítása RSS 2.0 formátumban.
        this.BuildFeed().SaveAsRss20( writer );        
    }

Itt felhasználtam egy általam írt BuildFeed nevű segédmetódust, amely visszaadja az adatbázisban tárolt információk alapján összeállított SyndicationFeed objektumot.

A feed összeállítása az alap attribútumok beállításával kezdődik:

    SyndicationFeed feed = new SyndicationFeed
    {
        Title = SyndicationContent.CreatePlaintextContent( "MSDN Kompetencia Központ" ),
        Description = SyndicationContent.CreatePlaintextContent( "MSDN Kompetencia Központ RSS feedje" ),
        Copyright = SyndicationContent.CreatePlaintextContent( "Copyright (C) MSDN Kompetencia Központ" ),
        Language = "hu-HU",
        ImageUrl = new Uri( imageUrl ),
        Generator = "Cube"
    };

Az egyes elemeket a feed objektum Items gyűjteményébe kell felvennünk SyndicationItemek formájában. Az elemek összerakása az új C# szintaktikával szintén aránylag tömören megoldható:

    SyndicationItem item = new SyndicationItem
    {
        Title = SyndicationContent.CreatePlaintextContent( title ),
        Content = SyndicationContent.CreateHtmlContent( body ),
        PublishDate = date.ToUniversalTime(),
        Id = articleUrl
    };

Az itt felsoroltakon kívül még természetesen számos tulajdonságot tudunk megadni, melyek közül egyik-másik gyűjtemény, ilyen például a <link> tag:

    item.Links.Add( SyndicationLink.CreateAlternateLink( new Uri( articleUrl ) ) );

Az RSS egyik érdekessége, hogy a séma bővíthető, akár más névtérbe tartozó XML elemekkel. A .NET-es objektum modell erre is ad támogatást az ElementExtensions és AttributeExtensions tulajdonságokon keresztül. Például ha szeretnénk felvenni egy <comments> tag-et a generált XML-be, amely egy URL címre mutat, akkor arra nem találunk saját tulajdonságot, de így mégis megtehetjük:

    item.ElementExtensions.Add( "comments", null, commentsUrl );

Ha azonban azt is szeretnénk tárolni, hogy egy adott cikkhez hány hozzászólás tartozik, akkor egy slash:comments elemre lesz szükségünk, amihez a slash névteret definiálnunk kell az XML elején. Hasonló a helyzet akkor, ha a szerző nevét egy dc:creator elemben szeretnénk publikálni, szintén meg kell adnunk a névtér feloldását a feed elején:

<channel xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">

Ezt így tehetjük meg:

    feed.AttributeExtensions.Add( new XmlQualifiedName( "xmlns:dc" ), "http://purl.org/dc/elements/1.1/" );
    feed.AttributeExtensions.Add( new XmlQualifiedName( "xmlns:slash" ), "http://purl.org/rss/1.0/modules/slash/" );

Az értékek beszúrását pedig így:

    item.ElementExtensions.Add( "slash:comments", null, commentCount );
    item.ElementExtensions.Add( "dc:creator", null, userName );

Mindezekből látható, hogy tetszőleges szerkezetű XML-t össze tudunk állítani, az objektumok szabványos sorosításáról a SaveAsRss20 vagy SaveAsAtom10 metódus gondoskodik. Ahhoz, hogy az eredmény valóban szabványos legyen, célszerű használni a CreatePlaintextContent, CreateHtmlContent, CreateAlternateLink stb. metódusokat.

Bár a felhasznált osztályok olyan névtérben találhatóak, amelyek elvileg a WCF részei, ASP.NET esetén nincs szükség arra, hogy WCF endpointokat definiáljunk, közvetlenül használhatjuk ezeket az osztályokat egy HTTP handlerben és a kisorosított XML-t közvetlenül írhatjuk a válasz streambe.

Technorati Tags: ,,,,

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