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