首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

.NET 操作 XML 通用類別 MyXml:XPath 查詢、節點增刪改查、屬性處理、事件自動儲存與路徑支援完整實作

     編輯:關於C語言

前言:為什麼需要封裝 XmlDocument?

雖然 .NET 提供 `XmlDocument`、`XDocument` (LINQ to XML) 等原生 API,但在**舊專案維護、需統一 XPath 風格、自動儲存機制、虛擬/絕對路徑切換、批次屬性操作**等場景,一個經過實戰驗證的 `MyXml` 封裝類別能大幅降低樣板碼與錯誤率。

核心功能清單

  • **雙路徑模式**:絕對路徑 (`AbsolutePath`) / 虛擬路徑 (`VirtualPath` → `Server.MapPath`)。
  • **XPath 查詢**:`SelectSingleNode` / `SelectNodes` 統一入口。
  • **節點值/屬性 讀寫**:單節點/批次節點支援。
  • **節點/屬性 增刪**:`AddParentNode`、`AddChildNode`、`AddAttribute`、`DeleteXmlNode`、`DeleteAttribute`。
  • **XML 事件自動儲存**:`NodeChanged`/`NodeInserted`/`NodeRemoved` 觸發 `Save()`。
  • **編碼/縮排輸出**:預設 UTF-8、縮排格式化。

完整類別實作 (現代化重構)

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` 即時性導致遍歷中集合變更例外。

避坑指南與最佳實踐

  • ⚠️ **`HttpContext.Current` 為 null**:在 `Application_Start`、`BackgroundService`、單元測試、Console App 中**無法使用虛擬路徑**。**強制要求絕對路徑**或注入 `IWebHostEnvironment` 解析 `ContentRootPath`/`WebRootPath`。
  • ⚠️ **大檔案效能**:`XmlDocument` 全量載入 DOM,**>10MB XML 記憶體壓力大**。大檔案/串流場景改用 `XmlReader`/`XmlWriter` 或 `XDocument` (仍載入記憶體但 API 更優)。
  • ⚠️ **編碼宣告**:`XmlWriterSettings.Encoding = new UTF8Encoding(false)` (無 BOM) 避免某些舊系統讀取 BOM 報錯。
  • ⚠️ **XPath 注入**:拼接使用者輸入進 XPath 字串 (`/user[@name='` + input + `']`) 會導致注入。**參數化查詢**或嚴格驗證/轉義輸入 (單引號雙寫)。
  • ✅ **IDisposable 模式**:實作 `Dispose()` 移除事件訂閱,防止記憶體洩漏 (靜態事件持有實例)。
  • ✅ **單元測試**:建構函數注入 `XmlDocument` 或路徑解析策略介面,便於 Mock 測試。

現代替代:LINQ to XML (XDocument) 片段對比

// 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 正確釋放** 是通用鐵律。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved