Praca z drzewem węzłów dokumentu
Praca z drzewem węzłów dokumentu
Ten przewodnik pokazuje, jak nawigować i manipulować strukturą dokumentu bezpośrednio poprzez jego drzewo węzłów — model obiektowy, na którym opierają się Document, Section, Paragraph, Run, Table oraz każdy inny element dokumentu. Każdy element w dokumencie dziedziczy po Node, a każdy element zdolny do przechowywania dzieci dziedziczy po CompositeNode, więc zrozumienie tych dwóch klas bazowych wyjaśnia, jak pracować z treścią, której nie obejmuje żaden dedykowany pomocnik API.
Klasy bazowe Node i CompositeNode
Node jest abstrakcyjną klasą bazową dla każdego obiektu w drzewie dokumentu. Udostępnia NodeType (wartość wyliczeniowa identyfikująca typ węzła), ParentNode (otaczający CompositeNode), Document (dokument macierzysty) oraz PreviousSibling/NextSibling do przemieszczania się po węzłach na tym samym poziomie. Node.IsComposite informuje, czy dany węzeł może zawierać dzieci. CompositeNode rozszerza Node dla elementów kontenerów — FirstChild, LastChild i Count opisują jego bezpośrednie dzieci, a HasChildNodes informuje, czy jakiekolwiek istnieją. CompositeNode.GetChildNodes(nodeType, isDeep) pobiera wszystkie dzieci danego NodeType, opcjonalnie przeszukując cały poddrzew (isDeep) zamiast tylko bezpośrednich dzieci, a CompositeNode.GetChild(nodeType, index, isDeep) pobiera pojedyncze dziecko według indeksu.
Wyliczenie NodeType
NodeType wymienia każdy rodzaj węzła definiowany przez model dokumentu — typy strukturalne takie jak Document, Section, Body, Paragraph, Table, Row i Cell; typy treści inline takie jak Run, Shape i Comment; oraz typy mechanizmu pól takie jak FieldStart, FieldSeparator i FieldEnd, które oznaczają granice i tekst przełącznika pola Word w strumieniu run. Przekazanie wartości NodeType do GetChildNodes lub GetChild ogranicza wyszukiwanie do węzłów dokładnie tego typu, lub przekazanie NodeType.Any dopasowuje każdy typ węzła.
Przeglądanie i modyfikowanie drzewa
CompositeNode.GetEnumerator() pozwala iterować po bezpośrednich dzieciach węzła, a CompositeNode implementuje również SelectNodes(xpath) i SelectSingleNode(xpath) dla zapytań XPath w całym poddrzewie. Aby modyfikować drzewo, AppendChild(newChild) i PrependChild(newChild) dodają węzeł na początek lub koniec dzieci, InsertBefore(newChild, refChild) i InsertAfter(newChild, refChild) wstawiają względem istniejącego dziecka, a RemoveChild(oldChild) lub RemoveAllChildren() usuwają węzły. CompositeNode.IndexOf(child) zwraca pozycję dziecka. Na samym Node, Clone(isCloneChildren) tworzy kopię — opcjonalnie głęboką — i Remove() odłącza węzeł od jego rodzica. Node.NextPreOrder(rootNode) i PreviousPreOrder(rootNode) przechodzą drzewo w kolejności dokumentu, ograniczone do podanego węzła korzenia, co jest powszechnym sposobem skanowania całego poddrzewa węzeł po węźle bez kodu rekurencyjnego.
Kolekcje węzłów i powiadomienia o zmianach
NodeCollection jest konkretną, mutowalną kolekcją węzłów obsługującą Add(node), Insert(index, node), Remove(node), RemoveAt(index), Clear(), Contains(node), IndexOf(node) oraz ToArray(), wraz z własnością Count. NodeList i NodeEnumerator wspierają iterację w stylu odczytu nad wynikami zapytań, takimi jak węzły zwrócone przez SelectNodes. Aby obserwować zmiany strukturalne w czasie rzeczywistym, zaimplementuj INodeChangingCallback — jego metody NodeInserting(args), NodeInserted(args), NodeRemoving(args) i NodeRemoved(args) otrzymują NodeChangingArgs opisujący dotknięty Node, jego OldParent/NewParent oraz NodeChangingAction, które wystąpiło.
Odwiedzanie węzłów przy użyciu DocumentVisitor
Aby przetwarzać każdy typ węzła w ustrukturyzowany sposób bez ręcznego przechodzenia drzewa, utwórz podklasę DocumentVisitor i nadpisz metody Visit*Start/Visit*End dla interesujących Cię typów węzłów — VisitParagraphStart/VisitParagraphEnd, VisitTableStart/VisitTableEnd, VisitRun i tak dalej — a następnie wywołaj Node.Accept(visitor) na korzeniu poddrzewa, aby go uruchomić. Jest to standardowy wzorzec eksportowania, transformacji lub inspekcji dokumentu bez konieczności pisania własnych rekurencyjnych przeglądów dla każdego przypadku użycia.
Wskazówki i najlepsze praktyki
- Używaj
NodeType.AnyzGetChildNodes(NodeType.Any, true), gdy potrzebujesz wszystkich potomków niezależnie od typu, zamiast wywoływaćGetChildNodesraz na typ. - Preferuj
Node.NextPreOrder(rootNode)/PreviousPreOrder(rootNode)do płaskiego, nierrekursywnego skanowania poddrzewa zamiast ręcznego pisania rekurencyjnego zstępowania. - Zaimplementuj
DocumentVisitorzamiast ad hoc kodu przeglądającego drzewo, gdy to samo przetwarzanie musi być uruchamiane na wielu różnych typach węzłów. - Sprawdź
Node.IsCompositeprzed rzutowaniem naCompositeNode, gdy pracujesz z węzłem nieznanego typu. - Zarejestruj
INodeChangingCallback, gdy musisz reagować na wstawienia lub usunięcia dokonywane przez inny kod (w tym wbudowane operacje), a nie tylko na zmiany wprowadzane bezpośrednio przez Twój własny kod.
Typowe problemy
| Problem | Przyczyna | Poprawka |
|---|---|---|
Rzutowanie Node na CompositeNode powoduje błąd nieprawidłowego rzutowania | Węzeł jest typem liścia (na przykład Run lub FieldStart), który nie może zawierać dzieci | Najpierw sprawdź Node.IsComposite, lub użyj NodeType, aby potwierdzić, że węzeł jest typem kontenera przed rzutowaniem |
GetChildNodes zwraca tylko bezpośrednie dzieci | isDeep został pozostawiony false (lub pominięty, gdy przeciążenie domyślnie jest płytkie) | Przekaż isDeep: true, aby przeszukać całe poddrzewo |
| Modyfikowanie kolekcji podczas iteracji po niej powoduje błąd | Węzły zostały dodane lub usunięte wewnątrz foreach nad NodeCollection lub w wyniku GetChildNodes | Najpierw materializuj węzły do modyfikacji przy użyciu ToArray(), a następnie iteruj po tablicy, mutując drzewo |
Podklasa DocumentVisitor nie jest wywoływana dla niektórych treści | Odwiedzający został wywołany przez Accept() na węźle, który nie zawiera docelowej treści, lub odpowiadająca metoda Visit* nie została nadpisana | Wywołaj Accept() na przodku, który zawiera treść, i nadpisz każdą parę Visit*Start/Visit*End istotną dla przetwarzanych typów węzłów |
FAQ
Jaka jest różnica między Node a CompositeNode?
Node jest klasą bazową, z której dziedziczy każdy element dokumentu. CompositeNode rozszerza Node dla elementów, które mogą zawierać dzieci (takie jak Body, Paragraph i Table); elementy liściaste (takie jak Run) dziedziczą bezpośrednio po Node i nie mogą posiadać dzieci.
Jak znaleźć wszystkie węzły określonego typu w dokumencie?
Wywołaj CompositeNode.GetChildNodes(nodeType, isDeep) na węźle, który jest korzeniem twojego wyszukiwania (często sam Document), przekazując NodeType, którego potrzebujesz, oraz isDeep: true, aby przeszukać cały poddrzewo, a nie tylko bezpośrednie dzieci.
Jak bezpiecznie usuwać węzły podczas iteracji?
Zbierz najpierw węzły do usunięcia w tablicę — na przykład przy użyciu NodeCollection.ToArray() —, a następnie iteruj po tej tablicy i wywołaj Remove() na każdym węźle, zamiast modyfikować bieżącą kolekcję, gdy foreach ją iteruje.
Co to są węzły FieldStart, FieldSeparator i FieldEnd?
Są to wartości NodeType oznaczające mechanizm pola Word osadzonego w strumieniu uruchamiania: FieldStart i FieldEnd wiążą pole, a FieldSeparator oddziela kod pola od jego aktualnie wyświetlanego wyniku.
Jak przetworzyć każdy akapit, tabelę i run w dokumencie?
Utwórz podklasę DocumentVisitor, nadpisz VisitParagraphStart/VisitParagraphEnd, VisitTableStart/VisitTableEnd oraz VisitRun dla potrzebnych typów węzłów, a następnie wywołaj Accept(visitor) na Document (lub dowolnym korzeniu poddrzewa), aby go uruchomić.
API Reference Podsumowanie
| Klasa/Metoda | Opis |
|---|---|
Node | Abstrakcyjna klasa bazowa dla każdego węzła w drzewie dokumentu |
CompositeNode | Klasa bazowa dla węzłów, które mogą zawierać dzieci |
CompositeNode.GetChildNodes(nodeType, isDeep) | Pobierz wszystkie dzieci określonego typu, opcjonalnie w całym poddrzewie |
CompositeNode.AppendChild(newChild) / InsertBefore(newChild, refChild) | Dodaj węzeł do kontenera na końcu lub przed/po istniejącym dziecku |
NodeType | Wyliczenie identyfikujące rodzaj węzła (Paragraph, Table, Run, FieldStart i tak dalej) |
NodeCollection | Konkretna, modyfikowalna kolekcja obiektów Node |
Node.Clone(isCloneChildren) | Skopiuj węzeł, opcjonalnie łącznie z jego dziećmi |
Node.NextPreOrder(rootNode) / PreviousPreOrder(rootNode) | Przejdź przez poddrzewo w kolejności dokumentu |
INodeChangingCallback | Interfejs wywołania zwrotnego powiadamia, gdy węzły są wstawiane lub usuwane |
DocumentVisitor | Klasa bazowa dla strukturalnego, typowo-rozsyłanego przeglądania za pomocą Node.Accept(visitor) |