本文针对.NET初学者介绍LINQ TO XML,你会看见很多为了通俗易懂而故意描述得不专业的语句,所以高手勿入!本文介绍的方法不只针对WP,其他任何.NET项目也可以参考。
我在Windows Phone平台上发布的《上海轨道交通》使用的是离线XML作为数据存储。好处是不需要安装任何三方库,.NET自己就有能力解析。如果用JSON装逼还得下载JSON.NET呢。又因为都是只读数据,也用不着为了装逼而用SQL Lite或SQL CE什么的数据库杀鸡用牛刀。
下面介绍一下如何在工程里内嵌XML文件并解析到对象的一般方法。
《上海轨道交通》的XML文件保存的是所有的站点信息,单个节点看起来就像这样:
<Station IsTrainStation="False" IsAirport="False" EnglishName="Xinzhuang" Warning="" StationName="莘庄" StationId="843f504f-e006-4d9e-a782-7c4d5bcb2eb3"> <Exits> <Exit Name="南" Info="梅陇西路都市路" /> <Exit Name="北一" Info="莘建东路水清南路" /> <Exit Name="北二" Info="莘建东路水清南路" /> </Exits> <WCs> <WC LineNo="1" Location="站外-北出入口外" /> <WC LineNo="5" Location="站外-无" /> </WCs> <StationTimes> <StationTime LineNo="1" Direction="往火车站" StartTime="5:30" EndTime="22:32" /> <StationTime LineNo="1" Direction="往富锦路" StartTime="5:23" EndTime="22:32" /> <StationTime LineNo="5" Direction="往闵行开发区" StartTime="6:00" EndTime="22:30" /> </StationTimes> <MapBrowser Long="121.3853" Lat="31.11108" /> <Elevators> <Elevator LineNo="1" Type="直升梯" Location="站厅-站台 南出入口楼梯口" /> <Elevator LineNo="5" Type="直升梯" Location="站厅-站台 站厅站台中部" /> </Elevators> </Station>
由此可见,一个站点包括了多个Exit(出入口)、WC(卫生间)、StationTime(首末班车时间)、Elevator(无障碍电梯)及一个MapBrowser(地图)信息。
站点本身也具有如EnglishName这样的属性值而非节点值信息。
最终解析完成显示到界面上的效果看起来就像这样:
一、XML文件的处理
在构建完成XML文件之后,你当然需要把丢到类库或App本身的工程下面,这边我用的是一个类库:ShanghaiMetro.Core,并在下面建立了一个Data文件夹,把XML丢在这里面:
然后很重要的一步是在这个XML文件的属性里把Build Action改成Embedded Resource!不然运行时会找不到文件!
二、构建model对象
我们的目的是要把XML数据文件解析到程序能用的对象上去,所以首先得有对象(呵呵),Station类的关键定义如下(意思是没有给出所有源代码,这里仅仅是贫血模型):
public class Station { public Guid StationId { get; set; } public string StationName { get; set; } public string EnglishName { get; set; } public bool IsTrainStation { get; set; } public bool IsAirport { get; set; } public string Warning { get; set; } public bool HasWarning { get { return !string.IsNullOrEmpty(Warning); } } public GeoCoordinate StationCoordinate { get; set; } private List<MetroLine> _lines { get; set; } public List<MetroLine> Lines { set { _lines = value; } get { return _lines ?? LineContext.Instance.MetroLines.FindAll(l => l.Stations.Contains(this)); } } public IEnumerable<Exit> Exits { get; set; } public IEnumerable<WC> WCs { get; set; } public IEnumerable<Elevator> Elevators { get; set; } public IEnumerable<StationTime> StationTimes { get; set; } public bool? HasTransportationCardService { get; set; } public bool HasTransportationCardServiceFixedBool { get { return HasTransportationCardService.GetValueOrDefault(); } } public Station(string stationName = null) { StationName = stationName; } ... }
和XML的定义对应可以发现:
1. XML节点的各个属性值就是C#模型的各属性,比如
<Station IsTrainStation="False" IsAirport="False" EnglishName="Xinzhuang" Warning="" StationName="莘庄" ...
对应的C#属性为
public string StationName { get; set; }
2. XML子节点映射成集合类型,表示包含关系,比如:
<Station IsTrainStation="False" IsAirport="False" EnglishName="Xinzhuang" Warning="" StationName="莘庄" StationId="843f504f-e006-4d9e-a782-7c4d5bcb2eb3">
<Exits>
<Exit Name="南" Info="梅陇西路都市路" />
<Exit Name="北一" Info="莘建东路水清南路" />
<Exit Name="北二" Info="莘建东路水清南路" />
</Exits>
...
对应的C#属性为:
public IEnumerable<Exit> Exits { get; set; }
当然,每个集合类型必须定义一个新的对象,比如这里的Exit,定义如下:
public class Exit { public string Name { get; set; } public string Info { get; set; } ... }
定义model的时候允许属性名和XML属性名不一致,因为稍后的LINQ to XML是可以指定映射关系的,但为了代码的清晰易懂和可谓呼吸,不建议这么做!
另外,LINQ select new对象的时候貌似不支持构造函数,所以最好是给每个属性都允许get和set操作。构造函数当然你可以留着,只是别在LINQ里面用。
三、LINQ TO XML
在准备好XML和C#模型后就可以读取数据了。由于之前XML文件是作为Embedded Resource嵌入工程中的,所以我们要拿到这个XML不可以像读取磁盘上的文件那样用路径去访问了(System.IO)。.NET中读取Embedded Resource的一般方法如下(意思是不仅仅适合Windows Phone应用):
private const string StrFileName = @"Data.Stations.xml"; … private string GetXmlFromEmbeddedResource() { Assembly assembly = Assembly.GetExecutingAssembly(); Stream stream = assembly.GetManifestResourceStream(GetType(), StrFileName); if (stream == null) { throw new FileNotFoundException("Could not find embedded mappings resource file.", StrFileName); } var reader = new StreamReader(stream); string s = reader.ReadToEnd(); return s; }
对于Embedded Resource,文件的“路径”分隔符是".",其实就是命名空间分隔符。所以我们既然把XML放在了Data目录下,就要在文件名前面加上"Data."。
读取Embedded Resource的方法是 Assembly.GetManifestResourceStream,它返回的是文件的stream流。
assembly.GetManifestResourceStream(GetType(), StrFileName);
GetType()获得的是当前所在的程序集,在这里就是ShanghaiMetro.Core。
把XML文件读成string后我们就可以着手LINQ TO XML了。XML文件里保存了全上海285个地铁站信息,显然对应的是一个Station的集合,于是定义:
public List<Station> AllStations;
作为目标,读取到的Station都会往这个List里塞。
贴上本文最关键的代码:LINQ TO XML读取Station信息完整代码
public void InitData() { string s = GetXmlFromEmbeddedResourceAsync(); var doc = XDocument.Parse(s); var stations = from p in doc.Descendants("Station") select new Station { StationId = Guid.Parse((string)p.Attribute("StationId")), Warning = (string)p.Attribute("Warning"), StationName = (string)p.Attribute("StationName"), EnglishName = (string)p.Attribute("EnglishName"), IsTrainStation = bool.Parse((string)p.Attribute("IsTrainStation")), IsAirport = bool.Parse((string)p.Attribute("IsAirport")), HasTransportationCardService = (bool?)p.Attribute("HasTransportationCardService"), Exits = from e in p.Descendants("Exit") select new Exit { Name = (string)e.Attribute("Name"), Info = (string)e.Attribute("Info") }, WCs = from w in p.Descendants("WC") select new WC { LineNo = Convert.ToInt32((string)w.Attribute("LineNo")), Location = (string)w.Attribute("Location") }, Elevators = from ev in p.Descendants("Elevator") select new Elevator { LineNo = Convert.ToInt32((string)ev.Attribute("LineNo")), Type = (string)ev.Attribute("Type"), Location = (string)ev.Attribute("Location") }, StationTimes = from t in p.Descendants("StationTime") select new StationTime { LineNo = Convert.ToInt32((string)t.Attribute("LineNo")), Direction = (string)t.Attribute("Direction"), StartTime = ((string)t.Attribute("StartTime")), EndTime = ((string)t.Attribute("EndTime")), }, StationCoordinate = (from c in p.Descendants("MapBrowser") select new GeoCoordinate { Longitude = (double)c.Attribute("Long"), Latitude = (double)c.Attribute("Lat") }).FirstOrDefault() }; if (null == AllStations) { AllStations = new List<Station>(); } AllStations.AddRange(stations); }
在这个方法中,为了让LINQ知道自己是TO XML的,首先就得把刚才从Embedded Resource里读到的string类型的XML撸成系统认识的XML文档类型:XDocument
var doc = XDocument.Parse(s);
再来看
from p in doc.Descendants("Station") select new Station {...}
doc.Descendants("Station") 的意思是 这个XML文档里所有的Station节点,select new Station的意思是把读到的每一个XElement(就是p)创建一个Station对象。接下来就是C#的对象初始化器语法了,大家应该都认识。
要注意的是,对于不同类型的数据我们要做类型转换,因为p.Attribute返回的是XAttribute类型,不是我们C#对象中定义的属性类型。对于最基本的string类型,比如站点名称StationName,直接强转就行:
(string)p.Attribute("StationName")
对于其他类型,比如GUID,建议用类型自带的转换操作去撸,而不要直接强转(通过编译检查但小心运行时爆)
Guid.Parse((string)p.Attribute("StationId"))
对于包含关系的对象,就是C#模型中定义的集合类型的属性,比如出口Exit,就需要再一次select new一下:
Exits = from e in p.Descendants("Exit") select new Exit { Name = (string)e.Attribute("Name"), Info = (string)e.Attribute("Info") }
如果仅仅包含一个对象,而不是集合类型,记得FirstOrDefault一下:
StationCoordinate = (from c in p.Descendants("MapBrowser") select new GeoCoordinate { Longitude = (double)c.Attribute("Long"), Latitude = (double)c.Attribute("Lat") }).FirstOrDefault()
不要用First,因为万一XML里漏定义了这个节点,First会爆,而FirstOrDefault是安全的,会把属性设置为null。
读取完成后用AddRange把结果一次性加入List,而不要傻乎乎的用foreach去做:
AllStations.AddRange(stations);
至此,读取XML的工作就全部完成了!