فصل ۱۱: XmlReader و XmlWriter؛ خواندن و نوشتن روبهجلوی XML
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
فصل ۱۱ — فناوریهای دیگر XML و JSON
تصویر آغاز فصل ۱۱ — Other XML and JSON Technologies
در فصل ۱۰، API مربوط به LINQ to XML و XML بهطور کلی بررسی شد. در این فصل، کلاسهای سطحپایین XmlReader/XmlWriter و نوعهای کار با JavaScript Object Notation یا JSON را بررسی میکنیم؛ JSON به یک جایگزین محبوب برای XML تبدیل شده است.
در مکمل آنلاین کتاب، ابزارهای کار با XML schema و stylesheetها توضیح داده میشوند.
XmlReader
XmlReader کلاسی با کارایی بالا برای خواندن یک stream از XML بهصورت سطحپایین و فقط رو به جلو (forward-only) است.
فایل customer.xml زیر را در نظر بگیرید:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<customer id="123" status="archived">
<firstname>Jim</firstname>
<lastname>Bo</lastname>
</customer>
برای ساخت XmlReader، متد static XmlReader.Create را با Stream، TextReader یا رشتهٔ URI صدا میزنید:
using XmlReader reader = XmlReader.Create ("customer.xml");
...
برای ساخت XmlReader که از string بخواند:
using XmlReader reader = XmlReader.Create (
new System.IO.StringReader (myString));
همچنین میتوانید XmlReaderSettings بدهید تا گزینههای parsing و validation کنترل شوند. سه property مهم برای ردکردن محتوای اضافی:
bool IgnoreComments // Skip comment nodes?
bool IgnoreProcessingInstructions // Skip processing instructions?
bool IgnoreWhitespace // Skip whitespace?
مثال، جلوگیری از انتشار whitespace nodeها:
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
using XmlReader reader = XmlReader.Create ("customer.xml", settings);
...
property مفید دیگر ConformanceLevel است. مقدار پیشفرض Document فرض میکند XML یک سند معتبر با یک root node است. اگر فقط fragment داخلی با چند node بخوانید، مثلاً:
<firstname>Jim</firstname>
<lastname>Bo</lastname>
باید ConformanceLevel را روی Fragment بگذارید تا exception رخ ندهد.
XmlReaderSettings.CloseInput تعیین میکند با بستهشدن reader، stream زیرین نیز بسته شود یا نه. property مشابه CloseOutput در XmlWriterSettings وجود دارد. پیشفرض هر دو false است.
خواندن Nodeها
واحدهای یک XML stream، XML nodeها هستند. reader stream را به ترتیب متنی و depth-first پیمایش میکند. property Depth عمق فعلی cursor را میدهد.
ابتداییترین روش خواندن، Read است. مانند MoveNext در IEnumerator به node بعدی میرود؛ اولین Read cursor را روی اولین node میگذارد. وقتی false برگرداند یعنی از آخرین node عبور کرده و reader باید بسته و کنار گذاشته شود.
دو property رشتهای Name و Value محتوای node را میدهند؛ بسته به نوع node یکی یا هر دو پر میشوند.
مثال زیر هر node را میخواند و نوع آن را چاپ میکند:
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
using XmlReader reader = XmlReader.Create ("customer.xml", settings);
while (reader.Read())
{
Console.Write (new string (' ', reader.Depth * 2));
Console.Write (reader.NodeType.ToString());
if (reader.NodeType == XmlNodeType.Element ||
reader.NodeType == XmlNodeType.EndElement)
{
Console.Write (" Name=" + reader.Name);
}
else if (reader.NodeType == XmlNodeType.Text)
{
Console.Write (" Value=" + reader.Value);
}
Console.WriteLine ();
}
خروجی:
XmlDeclaration
Element Name=customer
Element Name=firstname
Text Value=Jim
EndElement Name=firstname
Element Name=lastname
Text Value=Bo
EndElement Name=lastname
EndElement Name=customer
NodeType از enum XmlNodeType است که اعضای آن عبارتاند از: None, XmlDeclaration, Element, EndElement, Text, Attribute, Comment, Entity, EndEntity, EntityReference, ProcessingInstruction, CDATA, Document, DocumentType, DocumentFragment, Notation, Whitespace, SignificantWhitespace.
خواندن Elementها
اغلب ساختار سند XML را از قبل میدانید. XmlReader برای این حالت متدهایی دارد که حین خواندن ساختار خاصی را فرض میکنند؛ کد را ساده و همزمان بخشی از validation را انجام میدهند.
ReadStartElement بررسی میکند NodeType فعلی Element باشد و سپس Read میکند. اگر name بدهید، تطابق نام را هم بررسی میکند. ReadEndElement بررسی میکند NodeType فعلی EndElement باشد و بعد Read میکند.
خواندن <firstname>Jim</firstname>:
reader.ReadStartElement ("firstname");
Console.WriteLine (reader.Value);
reader.Read();
reader.ReadEndElement();
ReadElementContentAsString همهٔ این مراحل را یکجا انجام میدهد: start element، text node و end element را میخواند و content را string برمیگرداند:
string firstName = reader.ReadElementContentAsString ("firstname", "");
آرگومان دوم namespace است که اینجا خالی است. نسخههای typed مانند ReadElementContentAsInt نیز وجود دارد و نتیجه را parse میکند.
اگر XML اصلی را با element creditlimit گسترش دهیم:
<customer id="123" status="archived">
<firstname>Jim</firstname>
<lastname>Bo</lastname>
<creditlimit>500.00</creditlimit> <!-- OK, we sneaked this in! -->
</customer>
میتوانیم چنین بخوانیم:
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
using XmlReader r = XmlReader.Create ("customer.xml", settings);
r.MoveToContent();
r.ReadStartElement ("customer");
string firstName = r.ReadElementContentAsString ("firstname", "");
string lastName = r.ReadElementContentAsString ("lastname", "");
decimal creditLimit = r.ReadElementContentAsDecimal ("creditlimit", "");
r.MoveToContent();
r.ReadEndElement();
Elementهای اختیاری
اگر lastname اختیاری باشد:
r.ReadStartElement ("customer");
string firstName = r.ReadElementContentAsString ("firstname", "");
string lastName = r.Name == "lastname"
? r.ReadElementContentAsString() : null;
decimal creditLimit = r.ReadElementContentAsDecimal ("creditlimit", "");
ترتیب تصادفی Elementها
مثالهای این بخش به ترتیب ثابت elementها متکیاند. اگر elementها بتوانند در هر ترتیبی بیایند، آسانترین راه این است که آن بخش XML را در X-DOM بخوانید. بخش «Patterns for Using XmlReader/XmlWriter» این موضوع را توضیح میدهد.
Elementهای خالی
رفتار XmlReader با empty element یک دام ناخوشایند دارد. این دو در XML معادلاند:
<customerList></customerList>
<customerList/>
اما XmlReader آنها را متفاوت میبیند. در حالت اول:
reader.ReadStartElement ("customerList");
reader.ReadEndElement();
در حالت دوم ReadEndElement exception میدهد چون end element جداگانهای وجود ندارد. workaround:
bool isEmpty = reader.IsEmptyElement;
reader.ReadStartElement ("customerList");
if (!isEmpty) reader.ReadEndElement();
این nuisance عمدتاً برای elementهایی است که ممکن است child element داشته باشند. برای elementهای text ساده، متدهایی مانند ReadElementContentAsString هر دو نوع empty element را درست مدیریت میکنند.
سایر متدهای ReadXXX
جدول ۱۱-۱ متدهای ReadXXX در XmlReader را خلاصه میکند. اکثر آنها برای elementها طراحی شدهاند.
جدول ۱۱-۱ — متدهای Read| عضو | NodeType | نمونهٔ XML | پارامتر ورودی | دادهٔ خروجی |
| ReadContentAsXXX | Text | <a>x</a> | — | x |
| ReadElementContentAsXXX | Element | <a>x</a> | — | x |
| ReadInnerXml | Element | <a>x</a> | — | x |
| ReadOuterXml | Element | <a>x</a> | — | <a>x</a> |
| ReadStartElement | Element | <a>x</a> | — | — |
| ReadEndElement | Element | <a>x</a> | — | — |
| ReadSubtree | Element | <a>x</a> | — | <a>x</a> |
| ReadToDescendant | Element | <a>x<b></b></a> | "b" | — |
| ReadToFollowing | Element | <a>x<b></b></a> | "b" | — |
| ReadToNextSibling | Element | <a>x</a><b></b> | "b" | — |
| ReadAttributeValue | Attribute | بخش «Reading Attributes» | — | — |
ReadContentAsXXX یک text node را به type XXX parse میکند و درون خود از XmlConvert برای string-to-type conversion استفاده میکند. text node میتواند داخل element یا attribute باشد.
ReadElementContentAsXXX wrapper روی ReadContentAsXXX متناظر است و روی element node بهجای text node درون آن عمل میکند.
ReadInnerXml معمولاً روی element اعمال میشود و XML داخلی/descendantها را برمیگرداند؛ اگر روی attribute باشد value آن را میدهد. ReadOuterXml مشابه است اما خود element در موقعیت cursor را نیز شامل میکند.
ReadSubtree یک proxy reader میدهد که فقط current element و descendantهایش را میبیند. باید پیش از ادامهٔ reader اصلی بسته شود؛ با بستهشدن proxy، cursor reader اصلی به انتهای subtree میرود.
ReadToDescendant به آغاز اولین descendant با name/namespace مشخص میرود. ReadToFollowing به اولین node مطابق، مستقل از عمق، میرود.
ReadToNextSibling cursor را به آغاز اولین sibling node با name/namespace مشخص میبرد.
دو متد legacy نیز وجود دارند: ReadString و ReadElementString مانند ReadContentAsString و ReadElementContentAsString هستند، اما اگر داخل element بیش از یک text node باشد exception میدهند. بهتر است استفاده نشوند، چون وجود comment داخل element هم میتواند باعث exception شود.
Reading Attributes
XmlReader indexer برای دسترسی مستقیم و random به attributeهای یک element بر اساس نام یا موقعیت دارد؛ استفاده از indexer معادل GetAttribute است.
<customer id="123" status="archived"/>
Console.WriteLine (reader ["id"]); // 123
Console.WriteLine (reader ["status"]); // archived
Console.WriteLine (reader ["bogus"] == null); // True
با اینکه ترتیب attribute از نظر معنا اهمیتی ندارد، با ordinal position هم قابل دسترسی است:
Console.WriteLine (reader [0]); // 123
Console.WriteLine (reader [1]); // archived
indexer میتواند namespace attribute را نیز بگیرد. AttributeCount تعداد attributeهای node جاری را میدهد.
Attribute nodeها
برای پیمایش صریح attribute nodeها باید از مسیر عادیِ Read موقتاً منحرف شوید؛ دلیل خوب، parse کردن value به typeهای دیگر با ReadContentAsXXX است. این diversion باید از start element شروع شود. در attribute traversal قاعدهٔ forward-only شلتر است و با MoveToAttribute میتوانید به هر attribute جلو یا عقب بروید.
با fragment قبل:
reader.MoveToAttribute ("status");
string status = reader.ReadContentAsString();
reader.MoveToAttribute ("id");
int id = reader.ReadContentAsInt();
MoveToAttribute اگر attribute وجود نداشته باشد false میدهد. پیمایش ترتیبی attributeها:
if (reader.MoveToFirstAttribute())
do { Console.WriteLine (reader.Name + "=" + reader.Value); }
while (reader.MoveToNextAttribute());
// id=123
// status=archived
Namespaces و Prefixها
XmlReader دو سیستم موازی برای اشاره به نام element/attribute دارد:
Name؛NamespaceURI و LocalName.
وقتی Name را میخوانید یا متدی با یک آرگومان name صدا میزنید، سیستم اول است. بدون namespace/prefix خوب کار میکند، اما در حضور آنها literal است: namespace را نادیده میگیرد و prefix را دقیقاً همانطور که نوشته شده داخل Name نگه میدارد.
| fragment | Name |
|---|
<customer ...> | customer |
<customer xmlns='blah' ...> | customer |
<x:customer ...> | x:customer |
پس این برای دو حالت اول:
reader.ReadStartElement ("customer");
و برای حالت سوم:
reader.ReadStartElement ("x:customer");
سیستم دوم با NamespaceURI و LocalName کار میکند. اینها prefixها و default namespaceهای parent را در نظر میگیرند و prefix را خودکار expand میکنند؛ بنابراین NamespaceURI همیشه namespace معنایی درست و LocalName همیشه بدون prefix است.
وقتی دو آرگومان name به متدی مانند ReadStartElement میدهید، همین سیستم namespace-aware بهکار میرود. XML:
<customer xmlns="DefaultNamespace" xmlns:other="OtherNamespace">
<address>
<other:city>
...
خواندن:
reader.ReadStartElement ("customer", "DefaultNamespace");
reader.ReadStartElement ("address", "DefaultNamespace");
reader.ReadStartElement ("city", "OtherNamespace");
abstract کردن prefix معمولاً همان چیزی است که میخواهید. اگر لازم باشد prefix واقعی را با property Prefix ببینید و با LookupNamespace به namespace تبدیل کنید.
XmlWriter
XmlWriter نویسندهٔ forward-only برای XML stream است و طراحی آن متقارن با XmlReader است.
با Create و settings اختیاری ساخته میشود. مثال با indent:
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
using XmlWriter writer = XmlWriter.Create ("foo.xml", settings);
writer.WriteStartElement ("customer");
writer.WriteElementString ("firstname", "Jim");
writer.WriteElementString ("lastname", "Bo");
writer.WriteEndElement();
خروجی:
<?xml version="1.0" encoding="utf-8"?>
<customer>
<firstname>Jim</firstname>
<lastname>Bo</lastname>
</customer>
XmlWriter بهطور پیشفرض declaration را مینویسد، مگر در XmlWriterSettings مقدار OmitXmlDeclaration=true یا ConformanceLevel=Fragment باشد. Fragment نوشتن چند root node را هم مجاز میکند؛ در غیر این صورت exception.
WriteValue یک text node مینویسد و string و نوعهای nonstring مانند bool و DateTime را میپذیرد؛ درون خود از XmlConvert برای conversion سازگار XML استفاده میکند:
writer.WriteStartElement ("birthdate");
writer.WriteValue (DateTime.Now);
writer.WriteEndElement();
در مقابل:
WriteElementString ("birthdate", DateTime.Now.ToString());
میتواند هم ناسازگار با XML و هم مستعد parse اشتباه باشد. WriteString معادل WriteValue با string است. XmlWriter کاراکترهای غیرقانونی در attribute/element مانند &، <، > و extended Unicode را خودکار escape میکند.
نوشتن Attributeها
بلافاصله پس از start element میتوان attribute نوشت:
writer.WriteStartElement ("customer");
writer.WriteAttributeString ("id", "1");
writer.WriteAttributeString ("status", "archived");
برای value غیرstring، WriteStartAttribute، WriteValue و سپس WriteEndAttribute را صدا بزنید.
نوشتن سایر Node Typeها
XmlWriter این متدها را نیز دارد:
WriteBase64 // binary data
WriteBinHex // binary data
WriteCData
WriteComment
WriteDocType
WriteEntityRef
WriteProcessingInstruction
WriteRaw
WriteWhitespace
WriteRaw رشته را مستقیم در output stream تزریق میکند. WriteNode یک XmlReader میگیرد و هر چیزی را که از reader میآید echo میکند.
Namespaces و Prefixها در XmlWriter
overloadهای Write* اجازه میدهند element/attribute را به namespace متصل کنید. مثال منبع همهٔ elementها را به namespace نمونه متصل و prefix o را روی customer اعلام میکند:
writer.WriteStartElement ("o", "customer", "[namespace نمونهٔ کتاب]");
writer.WriteElementString ("o", "firstname", "[namespace نمونهٔ کتاب]", "Jim");
writer.WriteElementString ("o", "lastname", "[namespace نمونهٔ کتاب]", "Bo");
writer.WriteEndElement();
خروجی مفهومی:
<?xml version="1.0" encoding="utf-8"?>
<o:customer xmlns:o='[namespace نمونهٔ کتاب]'>
<o:firstname>Jim</o:firstname>
<o:lastname>Bo</o:lastname>
</o:customer>
برای اختصار، XmlWriter declaration namespace روی child element را وقتی parent قبلاً آن را تعریف کرده حذف میکند.
الگوهای استفاده از XmlReader/XmlWriter
کار با دادهٔ سلسلهمراتبی
کلاسهای زیر را در نظر بگیرید:
public class Contacts
{
public IList<Customer> Customers = new List<Customer>();
public IList<Supplier> Suppliers = new List<Supplier>();
}
public class Customer { public string FirstName, LastName; }
public class Supplier { public string Name; }
فرض کنید میخواهید با XmlReader/XmlWriter یک object از Contacts را به XML serialize کنید:
<?xml version="1.0" encoding="utf-8"?>
<contacts>
<customer id="1">
<firstname>Jay</firstname>
<lastname>Dee</lastname>
</customer>
<customer>
<firstname>Kay</firstname>
<lastname>Gee</lastname>
</customer>
<supplier>
<name>X Technologies Ltd</name>
</supplier>
</contacts>
در مثال منبع فرض شده attribute id اختیاری است. ادامهٔ الگوهای serialization/deserialization در مقالهٔ بعدی دنبال میشود.