通用XML读写和配置(一)
在软件开发过程中,经常使用到XML文件作为配置文件,保存一些配置信息。 为了方便对程序配置文件的读写,微软特别为.Net程序提供了程序配置文件,如web.config,App.config,这些配置文件通常会自动生成在程序启动目录下,并具有特定的格式。同时,.Net Framework还提供了一组读写配置文件的接口,这些接口被包含在命名空间System.Configuration中。 但由于接口固定,系统自带的配置文件格式不容易扩展,这些配置文件通常只能用来保存一些简单的信息,比如数据库连接信息或应用程序标识。
那如何保存复杂的配置信息呢?比如数据持久化、系统界面布局,或者是程序运行过程中产生的临时数据等,这些信息通常都需要自己定义对应的类和结构,多个类之间还存在组合、连接等关系。
一般的方法是:实现一个解析类,用来存取要保存的数据。在实现读写方法时,需要逐一的解析XML文件的节点,并在解析每个节点过程中找到对应的对象,分别赋值。此时,解析类相当于一个控制器(Controller),负责与所有数据对象(类)的交互,并组织它们的存储结构。示意图如下:
由于解析类和数据对象类之间存在紧耦合,存在以下不足:
1.代码无法复用。如果有新的数据对象需要配置,还得重新实现一个解析类。
2.解析类的维护成本加大。每个数据对象类的变化和更改,都需要修改解析类中的相应部分,从而增加维护成本。
3.不利于数据对象扩展。如果有新的数据也需要通过配置保存,则需要修改解析类,并调整和组织所有数据对象的存储结构。
基于此,不断尝试将解析类和数据对象类之间解耦,以降低代码维护成本,并希望最大程度的复用代码。通过多次的项目实践,发现如下思路:
1.各个数据对象负责解析自己所对应的XML节点,实现对该节点的写入和读取(序列化和反序列化)。
2.对象只负责解析自己,如果有子对象,则要求子对象具备自我解析接口,供上级对象调用。
3.最终的与XML文件的接口由最大一级数据对象来实现,外部通过该接口来实现XML配置文件的读写。
下面以一个小示例来说明,这个示例中假设要使用XML文件来配置数据库信息,数据库信息共有三类数据:数据库,数据表,列,其中,每个数据库有名称和创建日期等信息,包含0或多个数据表;数据表有名表和描述信息,由1至多个列构成;列的信息由列名、数据类型、是否为空等常用属性构成。这个示例可能没有多少实际意义(除非是用于数据持久化,但通常是不用我们自己实现了:)),只用于说明当多种数据之间具有组合关系时,如何实现较灵活的XML读写功能。类图如下:
在示例中,首先定义一个读写XML的接口,所有需要XML文件中存取配置的数据对象必须实现此接口。该接口的定义如下:
代码
/// <summary>
/// XML保存和设置接口
/// </summary>
public interface IXmlStorage
{
/// <summary>
/// 根据对应的XML节点来获取各属性值
/// </summary>
/// <param name="factoTypeNode">读取信息的节点</param>
void GetValueFromXml(System.Xml.XmlNode objectNode);
/// <summary>
/// 将对象本身格式化为XML
/// </summary>
/// <param name="xmldoc">需要写入的文档</param>
/// <param name="parentEle">生成节点的父元素节点</param>
void SetValueToXml(System.Xml.XmlDocument xmldoc, XmlElement parentEle);
}
其它的数据类定义如下:
代码
/// <summary>
/// 数据库对象
/// </summary>
public class DataBase : IXmlStorage
{
/// <summary>
/// 数据库名称
/// </summary>
public string Name = string.Empty;
/// <summary>
/// 描述
/// </summary>
public string Description = string.Empty;
/// <summary>
/// 创建日期
/// </summary>
public DateTime CreateDate = DateTime.Now;
/// <summary>
/// 数据表集合
/// </summary>
public List<Table> TableList = new List<Table>();
/// <summary>
/// 配置文件实例
/// </summary>
XmlDocument xmldoc = new XmlDocument();
/// <summary>
/// 默认配置文件名称
/// </summary>
public static readonly string DefaultConfigXml = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + Path.DirectorySeparatorChar + "DatabaseConfig.xml";
#region IXmlStorage Members
public void GetValueFromXml(XmlNode objectNode)
{
if (objectNode == null)
return;
if (objectNode.Name.Equals("database", StringComparison.CurrentCultureIgnoreCase))
{
XmlAttributeCollection attCol = objectNode.Attributes;
//从节点属性中获取值
for (int i = 0; i < attCol.Count; i++)
{
XmlAttribute att = attCol[i];
if (att.Name.Equals("name", StringComparison.CurrentCultureIgnoreCase))
this.Name = att.Value;
if (att.Name.Equals("description", StringComparison.CurrentCultureIgnoreCase))
this.Description = att.Value;
if (att.Name.Equals("date", StringComparison.CurrentCultureIgnoreCase))
{
try
{
this.CreateDate = DateTime.Parse(att.Value);
}
catch
{
this.CreateDate = DateTime.Now;
}
}
}
//先清空原来的下级节点内容(因为有可能多次重复加载)
this.TableList.Clear();
//获取下一级子节点的值
XmlNodeList nodeList = objectNode.ChildNodes;
if (nodeList != null && nodeList.Count > 0)
{
for (int j = 0; j < nodeList.Count; j++)
{
XmlNode node = nodeList[j];
if (node.Name.Equals("table", StringComparison.CurrentCultureIgnoreCase))
{
Table t = new Table();
t.GetValueFromXml(node);
this.TableList.Add(t);
}
}
}
}
}
public void SetValueToXml(XmlDocument xmldoc, XmlElement parentEle)
{
XmlElement dbEle = xmldoc.CreateElement("Database");
dbEle.SetAttribute("name", this.Name);
dbEle.SetAttribute("description", this.Description);
dbEle.SetAttribute("date", this.CreateDate.ToString());
xmldoc.AppendChild(dbEle); //由于此时是根节点,故parentEle为空
//添加下一级的子节点
for (int i = 0; i < this.TableList.Count; i++)
{
Table t = this.TableList[i];
t.SetValueToXml(xmldoc, dbEle);
}
}
#endregion
}
/// <summary>
/// 数据表
/// </summary>
public class Table : IXmlStorage
{
/// <summary>
/// 表名
/// </summary>
public string Name = string.Empty;
/// <summary>
/// 描述
/// </summary>
public string Description = string.Empty;
/// <summary>
/// 列集合
/// </summary>
public List<Column> ColumnList = new List<Column>();
#region IXmlStorage Members
public void GetValueFromXml(System.Xml.XmlNode objectNode)
{
if (objectNode == null)
return;
if (objectNode.Name.Equals("table", StringComparison.CurrentCultureIgnoreCase))
{
XmlAttributeCollection attCol = objectNode.Attributes;
//从节点属性中获取值
for (int i = 0; i < attCol.Count; i++)
{
XmlAttribute att = attCol[i];
if (att.Name.Equals("name", StringComparison.CurrentCultureIgnoreCase))
this.Name = att.Value;
if (att.Name.Equals("description", StringComparison.CurrentCultureIgnoreCase))
this.Description = att.Value;
}
//先清空原来的下级节点内容(因为有可能多次重复加载)
this.ColumnList.Clear();
//获取下一级子节点的值
XmlNodeList nodeList = objectNode.ChildNodes;
if (nodeList != null && nodeList.Count > 0)
{
for (int j = 0; j < nodeList.Count; j++)
{
XmlNode node = nodeList[j];
if (node.Name.Equals("column", StringComparison.CurrentCultureIgnoreCase))
{
Column c = new Column();
c.GetValueFromXml(node);
this.ColumnList.Add(c);
}
}
}
}
}
public void SetValueToXml(System.Xml.XmlDocument xmldoc, System.Xml.XmlElement parentEle)
{
XmlElement tblEle = xmldoc.CreateElement("Table");
tblEle.SetAttribute("name", this.Name);
tblEle.SetAttribute("description", this.Description);
parentEle.AppendChild(tblEle); //由于此时是根节点,故parentEle为空
//添加下一级的子节点
for (int i = 0; i < this.ColumnList.Count; i++)
{
Column c = this.ColumnList[i];
c.SetValueToXml(xmldoc, tblEle);
}
}
#endregion
}
其中,作为所有数据对象中的最大(级别)的对象,DataBase除了实现XML节点读写接口外,还需要提供加载和写入XML文件的方法,供外部代码调用,这也是读写XML文件的唯一入口。实现代码如下:
代码
#region 读取和保存XML文件
public bool LoadFromFile(string xmlPath)
{
try
{
this.xmldoc.Load(xmlPath);
XmlNode rootNode = this.xmldoc.SelectSingleNode("//Database");
if (rootNode != null)
{
this.GetValueFromXml(rootNode);
}
}
catch (Exception ex)
{
string msg = "加载配置文件失败,发生异常:" + ex.Message;
throw new Exception(msg);
}
return true;
}
public bool SaveToFile(string xmlPath)
{
try
{
this.xmldoc = new XmlDocument(); //重新构造一个XML文档
XmlDeclaration xDeclare = this.xmldoc.CreateXmlDeclaration("1.0", "GB2312", null);
this.xmldoc.AppendChild(xDeclare);
this.SetValueToXml(this.xmldoc, null);
this.xmldoc.Save(xmlPath);
return true;
}
catch (XmlException ex)
{
throw new Exception("保存配置文件失败,发生异常:" + ex.Message);
}
}
#endregion
完成了所有数据对象的定义,我们就可以通过XML文件来配置和存储这些数据。
示例代码如下:
代码
DataBase db = new DataBase();
db.Name = "数据库1";
db.Description = "for test";
Table tbl = new Table();
tbl.Name = "Student";
tbl.Description = "学生信息表";
db.TableList.Add(tbl);
Column c1 = new Column();
c1.Name = "StudentName";
c1.ColumnType = 1;
c1.IsNullable = false;
c1.Remark = "姓名";
Column c2 = new Column();
c2.Name = "Number";
c2.ColumnType = 1;
c2.IsPrimaryKey = true;
c2.Remark = "学号";
Column c3 = new Column();
c3.Name = "Age";
c3.ColumnType = 2;
c3.Remark = "年龄";
tbl.ColumnList.Add(c1);
tbl.ColumnList.Add(c2);
tbl.ColumnList.Add(c3);
//保存数据到XML文件
db.SaveToFile("D:\\mytest.xml");
//从XML文件中加载数据
DataBase db2 = new DataBase();
db2.LoadFromFile("D:\\mytest.xml");
总结:
1.在数据对象的定义过程中,我们除了要定义数据本身的属性和方法,还要定义对象本身所对应的XML节点结构,从而使对象能够自我解析XML节点,达到了解耦的目的。
2.数据对象负责解析自己,这算是“职责单一”的设计原则的体现吧?哈哈,后来看了设计模式,这似乎就是传说中的装饰者模式呢。
3.虽然这已经改进了XML文件的存取配置,但依然不算很通用,能不能实现一种更通用甚至是万能的XML配置方法呢?请看下回分解。
示例代码:下载