本文针对.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的工作就全部完成了!