Изменение данных в XML

40

Благодаря LINQ to XML, модификация данных XML теперь стала проще, чем когда-либо ранее. Оперируя всего лишь небольшим набором методов, можно выполнять любые желаемые модификации. Будь то добавление, изменение или удаление узлов или элементов — всегда найдется метод, который может выполнить работу.

Как уже неоднократно повторялось, в LINQ to XML большая часть времени приходится на работу с объектами XElement. Поэтому большинство приведенных примеров связано с обработкой элементов. Классы LINQ to XML, унаследованные от XNode, рассматриваются первыми, а за ними следует раздел, посвященный атрибутам.

Добавление узлов

Этот раздел, посвященный добавлению новых узлов к дереву XML, начинается с базового примера кода, представленного ниже:

XDocument xDoc = new XDocument(
                new XElement("Employees",
                    new XElement("Employee",
                        new XAttribute("type", "Programmer"),
                        new XElement("FirstName", "Alex"),
                        new XElement("LastName", "Erohin"))));
Console.WriteLine(xDoc);

Этот код производит дерево XML с единственным сотрудником. Вот его вывод:

Базовый пример с единственным сотрудником

Описания различных методов добавления узлов будут начинаться с этого базового кода.

Хотя во всех последующих примерах добавляются элементы, тот же прием работает со всеми классами LINQ to XML, унаследованными от XNode.

В дополнение к следующим способам добавления узлов не забудьте заглянуть в раздел "Вызов XElement.SetElementValue() на дочерних объектах XElement" далее в этой статье.

XContainer.Add() (AddLast)

Метод, который будет использоваться наиболее часто для добавления элементов в дерево XML — это Add. Он добавляет узел в конец списка дочерних узлов по отношению к указанному узлу. Ниже приведен пример:

...
xDoc.Element("Employees").Add(
            new XElement("Employee",
                        new XAttribute("type", "Editor"),
                        new XElement("FirstName", "Elena"),
                        new XElement("LastName", "Volkova")));

Console.WriteLine(xDoc);

В приведенном коде видно, что базовый код дополнен кодом добавления элемента Employee к элементу документа Employees. Здесь с помощью метода Element документа получается элемент Employees, после чего с использованием метода Add к списку его дочерних элементов добавляется новый элемент. Ниже показан результат:

Добавление узла в конец списка дочерних узлов методом Add

Метод Add добавляет вновь сконструированный элемент Employee в конец списка дочерних узлов элемента Employees. Как видите, метод Add столь же гибок, как и конструктор XElement, и следует тем же правилам относительно аргументов, что позволяет применять функциональное конструирование.

XContainer.AddFirst()

Для добавления узла в начало списка дочерних узлов служит метод AddFirst. Взяв за основу тот же самый код, что и раньше, за исключением вызова метода Add, получим код, приведенный ниже:

...
xDoc.Element("Employees").AddFirst(
                    new XElement("Employee",
                        new XAttribute("type", "Editor"),
                        new XElement("FirstName", "Elena"),
                        new XElement("LastName", "Volkova")));

Console.WriteLine(xDoc);

Как и можно было ожидать, вновь добавленный элемент Employee появится в голове списка дочерних узлов элемента Employees:

Добавление узла в начало списка дочерних узлов методом AddFirst

Можно ли себе представить более простой способ манипуляций XML? Вряд ли.

XNode.AddBeforeSelf() и XNode.AddAfterSelf

Чтобы вставить узел в определенное место внутри списка дочерних узлов, необходимо получить ссылку либо на предшествующий узел, либо на узел, непосредственно следующий за местом вставки, и вызвать метод AddBeforeSelf или AddAfterSelf.

В качестве отправной точки используется дерево XML, произведенное примером метода Add. В это дерево будет вставлен новый узел между двумя существующими элементами Employee:

XDocument xDoc = new XDocument(
                new XElement("Employees",
                    new XElement("Employee",
                        new XAttribute("type", "Programmer"),
                        new XElement("FirstName", "Alex"),
                        new XElement("LastName", "Erohin"))));

            xDoc.Element("Employees").Add(
                    new XElement("Employee",
                        new XAttribute("type", "Editor"),
                        new XElement("FirstName", "Elena"),
                        new XElement("LastName", "Volkova")));

            xDoc.Element("Employees")
                .Elements("Employee")
                .Where(e => ((string)e.Element("FirstName")) == "Elena")
                .Single<XElement>()
                .AddBeforeSelf(
                   new XElement("Employee",
                       new XAttribute("type", "Technical Reviewer"),
                       new XElement("FirstName", "Dmitry"),
                       new XElement("LastName", "Morozec")));

            Console.WriteLine(xDoc);
Добавление узла в список дочерних узлов методом AddBeforeSelf

Чтобы освежить в памяти описание стандартных операций запросов, которое давалось в разделе LINQ to Objects, и для интеграции этих сведений с материалом, рассмотренным в этой статье, элемент Employee, перед которым должен быть вставлен новый, ищется с использованием арсенала операций LINQ.

С помощью метода Element производится углубление в документ с целью выбора элемента Employees. Затем выбираются дочерние элементы Employees с именем Employee, у которых есть дочерний элемент по имени FirstName со значением "Elena". Поскольку известно, что этому критерию отвечает только один элемент Employee, и оттого, что необходим объект типа XElement, на котором можно вызвать метод AddBeforeSelf, вызывается операция Single для получения объекта Employee типа XElement. Это дает ссылку на элемент Employee, перед которым требуется вставить новый XElement.

Также обратите внимание, что в вызове операции Where выполняется приведение элемента FirstName к string для использования средства извлечения значения узла. Это позволяет получить значение элемента FirstName с целью его сравнения с "Elena".

Имея ссылку на правильный элемент Employee, можно просто вызвать метод AddBeforeSelf. Вот результат:

Добавление узла в список дочерних узлов методом AddBeforeSelf

Как и требовалось, новый элемент Employee вставлен перед элементом Employee, у которого значение элемента FirstName равно "Elena".

После того объема усилий, предпринятых для получения ссылки на второй элемент Employee в предыдущем примере, код ниже может несколько разочаровать. Здесь с помощью метода Element просто получается ссылка на первый элемент Employee, после чего вызовом метода AddAfterSelf сразу после него добавляется новый элемент Employee:

...
xDoc.Element("Employees")
                .Element("Employee")
                .AddAfterSelf(
                   new XElement("Employee",
                       new XAttribute("type", "Technical Reviewer"),
                       new XElement("FirstName", "Dmitry"),
                       new XElement("LastName", "Morozec")));

По сравнению с предыдущим этот пример покажется тривиальным.

Удаление узлов

Удаление узлов выполняется с помощью одного из двух методов: Remove или RemoveAll.

XNode.Remove()

Метод Remove удаляет из дерева XML любой узел, а также все его дочерние узлы и атрибуты. В первом примере конструируется дерево XML и сохраняется ссылка на первый элемент, описывающий сотрудника, как это делалось в некоторых примерах из предыдущей статьи. Далее сконструированное дерево XML отображается перед удалением узлов. Затем удаляется первый элемент и отображается полученное в результате дерево XML:

// Это используется для сохранения ссылки на один из элементов дерева XML
            XElement firstEmployee;
            Console.WriteLine("Перед удалением узла: ");

            XDocument xDoc = new XDocument(
                new XElement("Employees", firstEmployee =
                    new XElement("Employee",
                        new XAttribute("type", "Programmer"),
                        new XElement("FirstName", "Alex"),
                        new XElement("LastName", "Erohin")),
                    new XElement("Employee",
                        new XAttribute("type", "Editor"),
                        new XElement("FirstName", "Elena"),
                        new XElement("LastName", "Volkova"))));
            Console.WriteLine(xDoc + "\n\nПосле удаления узла: ");

            firstEmployee.Remove();
            Console.WriteLine(xDoc);

Если все верно, то должно быть получено дерево XML сначала с первым элементом, описывающим сотрудника, а затем без него:

Удаление определенного узла методом Remove

Как видите, первый элемент Employee исчез в результате удаления.

IEnumerable<T>.Remove()

В предыдущем случае метод Remove вызывался на единственном объекте XElement. Однако его можно также вызвать на последовательности (IEnumerable<T>). Ниже представлен пример, в котором с помощью метода Descendants документа осуществляется рекурсивный обход вниз всего дерева XML с возвратом только элементов с именем FirstName, для чего используется операция Where. Затем на результирующей последовательности вызывается метод Remove:

XDocument xDoc = new XDocument(
                new XElement("Employees", firstEmployee =
                    new XElement("Employee",
                        new XAttribute("type", "Programmer"),
                        new XElement("FirstName", "Alex"),
                        new XElement("LastName", "Erohin")),
                    new XElement("Employee",
                        new XAttribute("type", "Editor"),
                        new XElement("FirstName", "Elena"),
                        new XElement("LastName", "Volkova"))));

xDoc.Descendants().Where(e => e.Name == "FirstName").Remove();
Console.WriteLine(xDoc);

Этот пример интересен тем, что здесь можно связать вместе все элементы LINQ. С помощью метода XDocument.Descendants извлекаются в виде последовательности все дочерние узлы, а затем вызывается стандартная операция запроса Where для фильтрации только узлов, отвечающим критерию поиска, которым в данном случае является имя элемента FirstName. Она возвращает последовательность, на которой затем вызывается метод Remove. Ниже показан результат:

Удаление последовательности узлов методом Remove

XElement.RemoveAll()

Иногда может понадобиться удалить содержимое элемента, но не сам элемент. Именно для этого предназначен метод RemoveAll. Ниже приведен пример:

Console.WriteLine("Перед удалением узла: ");

            XDocument xDoc = new XDocument(
                new XElement("Employees",
                    new XElement("Employee",
                        new XAttribute("type", "Programmer"),
                        new XElement("FirstName", "Alex"),
                        new XElement("LastName", "Erohin")),
                    new XElement("Employee",
                        new XAttribute("type", "Editor"),
                        new XElement("FirstName", "Elena"),
                        new XElement("LastName", "Volkova"))));
            Console.WriteLine(xDoc + "\n\nПосле удаления узла: ");

            xDoc.Element("Employees").RemoveAll();
            Console.WriteLine(xDoc);

В коде сначала отображается документ перед удалением содержимого узла Employees. Затем содержимое узла Employees удаляется, и документ отображается заново. Вот как выглядит результат:

Удаление содержимого узла методом RemoveAll

Обновление узлов

Несколько подклассов XNode, такие как XElement, XText и XComment, имеют свойство Value, которое может быть обновлено непосредственно. Другие, вроде XDocumentType и XProcessingInstruction, имеют свои специфичные свойства, которые также могут обновляться. В дополнение к модификации свойства Value, значения элементов можно изменять с помощью методов XElement.SetElementValue и XContainer.ReplaceAll, которые описаны далее.

XElement.Value на объектах XElement, XText.Value на объектах XText и XComment.Value на объектах XComment

Каждый из этих подклассов XNode имеет свойство Value, которое может быть установлено для обновления значения узла. Все они продемонстрированы ниже:

// Это используется для сохранения ссылки на один из элементов дерева XML
            XElement firstEmployee;
            Console.WriteLine("Перед обновлением узлов: ");

            XDocument xDoc = new XDocument(
                new XElement("Employees", firstEmployee =
                    new XElement("Employee",
                        new XAttribute("type", "Programmer"),
                        new XComment("Some comment"),
                        new XElement("FirstName", "Alex"),
                        new XElement("LastName", "Erohin"))));
            Console.WriteLine(xDoc + "\n\nПосле обновления узлов: ");

            // Теперь обновим элемент, комментарий и текстовый узел
            firstEmployee.Element("FirstName").Value = "Alexandr";
            firstEmployee.Nodes().OfType<XComment>().Single().Value =
                "Это программист :)";
            ((XElement)firstEmployee.Element("FirstName").NextNode)
                .Nodes().OfType<XText>().Single().Value = "Mr. Erohin";

            Console.WriteLine(xDoc);

В коде сначала обновляется элемент FirstName с использованием свойства Value, затем — комментарий с помощью его свойства Value и, наконец — элемент LastName за счет обращения к его значению через свойство Value его дочернего объекта XText. Обратите внимание на гибкость, которую обеспечивает LINQ to XML при получении ссылок на разные объекты, которые должны быть обновлены. Только имейте в виду, что вообще-то обращаться к значению элемента LastName, получая объект XText из его дочерних узлов, необязательно. Это сделано исключительно для целей демонстрации. В принципе можно было напрямую обратиться к его свойству Value. Ниже показан результат выполнения кода:

Обновление значения узла

Как видите, все значения узлов обновлены.

XElement.ReplaceAll()

Метод ReplaceAll удобен для замены целого поддерева XML, начинающегося с элемента. Этому методу можно передавать простое значение, такое как новая строка или значение числового типа. Поскольку доступна перегруженная версия метода, принимающая множество объектов через ключевое слово params, можно также заменить целое поддерево. Метод ReplaceAll также заменяет атрибуты:

// Это используется для сохранения ссылки на один из элементов дерева XML
            XElement firstEmployee;
            Console.WriteLine("Перед обновлением элементов: ");

            XDocument xDoc = new XDocument(
                new XElement("Employees", firstEmployee =
                    new XElement("Employee",
                        new XAttribute("type", "Programmer"),
                        new XComment("Some comment"),
                        new XElement("FirstName", "Alex"),
                        new XElement("LastName", "Erohin"))));
            Console.WriteLine(xDoc + "\n\nПосле обновления элементов: ");

            firstEmployee.ReplaceAll(
                       new XElement("FirstName", "Dmitry"),
                       new XElement("LastName", "Morozec"));

            Console.WriteLine(xDoc);

Обратите внимание, что при замене содержимого с помощью метода ReplaceAll опущено указание атрибута. Как и можно было ожидать, содержимое заменено:

Применение ReplaceAll для замены всего поддерева элемента

Обратите внимание, что атрибут типа Employee исчез. Интересно, что атрибуты не являются дочерними узлами элемента. Тем не менее, метод ReplaceAll заменяет их также.

Метод XElement.SetElementValue()

Не позволяйте этому методу с простым именем ввести вас в заблуждение. Он обладает способностью добавлять, изменять и удалять элементы. Более того, он выполняет эти операции на дочерних элементах того элемента, на котором вызывается. Другими словами, метод SetElementValue вызывается на родительском элементе, чтобы оказать влияние на его содержимое, т.е. на дочерние элементы.

При вызове методу SetElementValue передается имя дочернего элемента, значение которого должно быть установлено.Если дочерний элемент найден по имени, его значение обновляется, если только переданное значение не равно null. Когда передается null, найденный дочерний элемент удаляется. Если же элемент с указанным именем не обнаружен, он будет добавлен с переданным значением. Замечательный метод!

Также метод SetElementValue затрагивает только первый дочерний элемент с указанным именем, который он находит. Все последующие элементы с тем же именем не затрагиваются — будь то изменение значения или удаление элемента по причине передачи значения null. Ниже демонстрируются все применения этого метода: обновление, добавление и удаление:

// Это используется для сохранения ссылки на один из элементов дерева XML
            XElement firstEmployee;
            Console.WriteLine("Перед обновлением элементов: ");

            XDocument xDoc = new XDocument(
                new XElement("Employees", firstEmployee =
                    new XElement("Employee",
                        new XAttribute("type", "Programmer"),
                        new XComment("Some comment"),
                        new XElement("FirstName", "Alex"),
                        new XElement("LastName", "Erohin"))));
            Console.WriteLine(xDoc + "\n\nПосле обновления элементов: ");

            // Обновление
            firstEmployee.SetElementValue("FirstName", "Alexandr");

            // Добавление элементов
            firstEmployee.SetElementValue("Age", "26");

            // Удаление элемента
            firstEmployee.SetElementValue("LastName", null);

            Console.WriteLine(xDoc);

Как видите, сначала метод SetElementValue вызывается на дочернем элементе firstEmployee по имени FirstName. Поскольку элемент с таким именем существует, его значение будет обновлено. Затем метод SetElementValue вызывается на дочернем элементе firstEmployee по имени Age. Поскольку элемента с таким именем нет, он будет добавлен. И, наконец, метод SetElementValue вызывается на дочернем элементе firstEmployee по имени LastName с передачей значения null. Поскольку передано null, элемент LastName будет удален. Как видите метод SetElementValue обеспечивает высокую степень гибкости. А вот как выглядят результаты:

Применение SetElementValue для обновления, добавления и удаления дочерних элементов

Весьма неплохо. Значение элемента FirstName обновлено, добавлен элемент Age и удален элемент LastName.

Исходя из того, что вызов метода SetElementValue со значением null приводит к удалению узла, не следует думать, что установка вручную значения элемента в null - это то же самое что и удаление его средствами LINQ to XML. Это просто поведение метода SetElementValue. Если вы попытаетесь установить значение элемента в null, используя свойство value, будет сгенерировано исключение.

Лучший чат для C# программистов