雖然 .NET 提供 `XmlDocument`、`XDocument` (LINQ to XML) 等原生 API,但在**舊專案維護、需統一 XPath 風格、自動儲存機制、虛擬/絕對路徑切換、批次屬性操作**等場景,一個經過實戰驗證的 `MyXml` 封裝類別能大幅降低樣板碼與錯誤率。
using System;
using System.Xml;
using System.Web;
public enum XmlPathType { AbsolutePath, VirtualPath }
public class MyXml : IDisposable
{
private readonly XmlDocument _doc = new XmlDocument();
private readonly string _filePath;
private readonly XmlPathType _pathType;
private bool _disposed;
public MyXml(string filePath, XmlPathType pathType = XmlPathType.VirtualPath)
{
_filePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
_pathType = pathType;
LoadDocument();
// 訂閱事件:任何修改自動持久化
_doc.NodeChanged += OnNodeChanged;
_doc.NodeInserted += OnNodeChanged;
_doc.NodeRemoved += OnNodeChanged;
}
private void LoadDocument()
{
string physicalPath = _pathType == XmlPathType.AbsolutePath
? _filePath
: HttpContext.Current?.Server.MapPath(_filePath)
?? throw new InvalidOperationException("HttpContext.Current 為 null,無法解析虛擬路徑。請確保在 Web 請求上下文中使用,或改用絕對路徑。");
_doc.Load(physicalPath);
_doc.PreserveWhitespace = false; // 格式化輸出
}
private void OnNodeChanged(object sender, XmlNodeChangedEventArgs e)
{
if (!_disposed) Save(); // 自動儲存
}
// === 讀取 ===
public string GetNodeValue(string xpath) => _doc.SelectSingleNode(xpath)?.InnerText ?? "";
public string GetAttribute(string xpath, string attrName)
=> _doc.SelectSingleNode(xpath)?.Attributes?[attrName]?.Value ?? "";
// 批次讀取屬性 (較少用,保留)
public string[] GetAttributes(string xpath, string attrName)
=> _doc.SelectNodes(xpath)?.Cast()
.Select(n => n.Attributes?[attrName]?.Value).Where(v => v != null).ToArray() ?? Array.Empty();
// === 寫入/修改 ===
public void SetNodeValue(string xpath, string value)
{
foreach (XmlNode node in _doc.SelectNodes(xpath) ?? Enumerable.Empty())
node.InnerText = value;
}
public void SetAttribute(string xpath, string attrName, string value)
{
foreach (XmlNode node in _doc.SelectNodes(xpath) ?? Enumerable.Empty())
{
XmlAttribute attr = node.Attributes[attrName] ?? _doc.CreateAttribute(attrName);
attr.Value = value;
node.Attributes.Append(attr); // 若已存在 Append 會先移除舊的
}
}
// === 結構操作 ===
public XmlNode GetRoot() => _doc.DocumentElement;
public void AddParentNode(string parentName)
{
var root = GetRoot() ?? throw new InvalidOperationException("XML 無根節點");
var newNode = _doc.CreateElement(parentName);
root.AppendChild(newNode);
}
public void AddChildNode(string parentXPath, string childName)
{
var parent = _doc.SelectSingleNode(parentXPath) ?? throw new ArgumentException($"父節點不存在: {parentXPath}");
var child = _doc.CreateElement(childName);
parent.AppendChild(child);
}
public void AddAttribute(string xpath, string attrName, string value = "")
{
foreach (XmlNode node in _doc.SelectNodes(xpath) ?? Enumerable.Empty())
{
if (node.Attributes[attrName] == null)
{
var attr = _doc.CreateAttribute(attrName);
attr.Value = value;
node.Attributes.Append(attr);
}
}
}
public void DeleteAttribute(string xpath, string attrName, string attrValue = null)
{
foreach (XmlNode node in _doc.SelectNodes(xpath) ?? Enumerable.Empty())
{
var attr = node.Attributes[attrName];
if (attr != null && (attrValue == null || attr.Value == attrValue))
node.Attributes.Remove(attr);
}
}
public void DeleteNode(string xpath)
{
var nodes = _doc.SelectNodes(xpath);
if (nodes == null) return;
foreach (XmlNode node in nodes.Cast().ToList()) // ToList 避免集合修改中遍歷
node.ParentNode?.RemoveChild(node);
}
// === 儲存 ===
public void Save(string customPath = null)
{
if (_disposed) throw new ObjectDisposedException(nameof(MyXml));
string path = customPath ?? (_pathType == XmlPathType.AbsolutePath ? _filePath : HttpContext.Current.Server.MapPath(_filePath));
using (var writer = XmlWriter.Create(path, new XmlWriterSettings { Indent = true, Encoding = new UTF8Encoding(false) }))
{
_doc.Save(writer);
}
}
public void Dispose()
{
if (!_disposed)
{
_doc.NodeChanged -= OnNodeChanged;
_doc.NodeInserted -= OnNodeChanged;
_doc.NodeRemoved -= OnNodeChanged;
_disposed = true;
}
}
}
// WebForms / ASP.NET MVC 控制器中
string virtualPath = "~/App_Data/config.xml";
using (var xml = new MyXml(virtualPath, XmlPathType.VirtualPath))
{
// 讀取
string dbConn = xml.GetNodeValue("/configuration/connectionStrings/add[@name='MainDB']/@connectionString");
string apiKey = xml.GetAttribute("/settings/api", "key");
// 修改 (自動觸發儲存)
xml.SetNodeValue("/settings/lastUpdate", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
xml.SetAttribute("/settings/api", "key", "new-api-key-12345");
xml.SetAttribute("/settings/api", "enabled", "true");
// 新增節點
xml.AddParentNode("modules");
xml.AddChildNode("/configuration/modules", "module");
xml.SetAttribute("/configuration/modules/module[last()]", "name", "Payment");
xml.SetAttribute("/configuration/modules/module[last()]", "version", "2.0");
// 刪除
xml.DeleteAttribute("/settings/obsolete", "enabled", "false");
xml.DeleteNode("/configuration/oldSection");
// 手動另存為備份
xml.Save(Server.MapPath("~/App_Data/config_backup.xml"));
}
| 決策點 | 選擇 | 理由 |
|---|---|---|
| 底層 API | `XmlDocument` (DOM) | 支援 XPath、節點事件 (`NodeChanged`)、`PreserveWhitespace` 格式化、舊專案相容。新專案建議 `XDocument` (LINQ to XML) 但無節點事件自動儲存。 |
| 自動儲存觸發 | 三大節點事件 | 開發者無需記得呼叫 `Save()`,降低遺漏風險。高頻批次修改時可暫存 `bool _autoSave = false` 手動控制。 |
| 虛擬路徑解析 | `HttpContext.Current.Server.MapPath` | Web 應用標準做法。非 Web 環境 (Console/Service) **必須用絕對路徑**,否則 `HttpContext.Current` 為 null 拋異常。 |
| 批次操作回傳 | `SelectNodes` + 迴圈 | XPath 可匹配多節點,`SetNodeValue`/`SetAttribute` 設計為**批次生效**,符合「配置多節點同步修改」常見需求。 |
| 屬性新增/修改合一 | `Append` 會自動替換同名屬性 | 簡化 API:呼叫端無需判斷屬性是否存在。 |
| 刪除節點安全 | `ToList()` 快照後再移除 | 避免 `XmlNodeList` 即時性導致遍歷中集合變更例外。 |
// XDocument 寫法 (無自動儲存事件,需手動 Save)
var xdoc = XDocument.Load(path);
string val = xdoc.XPathSelectElement("/config/key")?.Value;
xdoc.XPathSelectElement("/config/key").SetValue("newVal");
xdoc.Save(path); // 手動
// 優點:語法更現代、Lambda 查詢、匿名型別投影、無命名空間前綴煩惱
// 缺點:無 NodeChanged 事件、XPath 擴充方法需引用 System.Xml.XPath
`MyXml` 封裝了 **XPath 查詢、節點/屬性增刪改查、虛絕對路徑支援、節點事件自動儲存** 等高頻需求,是 **ASP.NET WebForms / MVC 維護期專案** 的實用工具類。新專案若無「自動儲存」剛需,**優先使用 `XDocument` + `XPathSelectElement`**,語法更現代、型別安全、效能略優。無論選哪種,**編碼無 BOM、路徑注入防禦、大檔案串流處理、IDisposable 正確釋放** 是通用鐵律。