From b460faeef825260b6685e7805533f6dd85506932 Mon Sep 17 00:00:00 2001 From: Matthew Odumosu Date: Fri, 20 Mar 2026 00:16:34 +0100 Subject: [PATCH] Fix ChangeDocumentType not removing VBA parts when converting to non-macro type When changing a macro-enabled document (.docm) to a standard document (.docx) via ChangeDocumentType(), VBA-related parts (VbaProjectPart, CustomizationPart) were carried over to the new document type, leaving orphaned content type entries. Add a virtual OnDocumentTypeChanged hook in TypedPackageFeatureCollection and override it in WordprocessingDocumentFeatures to recursively remove VBA parts when the new type is not macro-enabled. Fixes #618 --- .../TypedPackageFeatureCollection.cs | 9 ++++ .../Packaging/WordprocessingDocument.cs | 15 +++++++ .../DocxTests01.cs | 44 +++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/src/DocumentFormat.OpenXml/Packaging/TypedPackageFeatureCollection.cs b/src/DocumentFormat.OpenXml/Packaging/TypedPackageFeatureCollection.cs index 768fc5f45..f3f0441ea 100644 --- a/src/DocumentFormat.OpenXml/Packaging/TypedPackageFeatureCollection.cs +++ b/src/DocumentFormat.OpenXml/Packaging/TypedPackageFeatureCollection.cs @@ -96,6 +96,7 @@ void IDocumentTypeFeature.ChangeDocumentType(TDocumentType newTyp try { Package.ChangeDocumentTypeInternal(CreateMainPart()); + OnDocumentTypeChanged(newType); } catch (OpenXmlPackageException e) { @@ -116,5 +117,13 @@ void IDocumentTypeFeature.ChangeDocumentType(TDocumentType newTyp protected abstract TMainPart CreateMainPart(); + /// + /// Called after the document type has been changed. Subclasses can override to perform + /// cleanup such as removing parts that are not valid for the new document type. + /// + protected virtual void OnDocumentTypeChanged(TDocumentType newType) + { + } + bool IKnownDataPartFeature.IsKnown(string relationshipId) => false; } diff --git a/src/DocumentFormat.OpenXml/Packaging/WordprocessingDocument.cs b/src/DocumentFormat.OpenXml/Packaging/WordprocessingDocument.cs index b3bd6384d..80449ffcf 100644 --- a/src/DocumentFormat.OpenXml/Packaging/WordprocessingDocument.cs +++ b/src/DocumentFormat.OpenXml/Packaging/WordprocessingDocument.cs @@ -582,6 +582,21 @@ public WordprocessingDocumentFeatures(OpenXmlPackage package) "application/vnd.ms-word.template.macroEnabledTemplate.main+xml" => WordprocessingDocumentType.MacroEnabledTemplate, _ => default, }; + + protected override void OnDocumentTypeChanged(WordprocessingDocumentType newType) + { + if (newType is WordprocessingDocumentType.MacroEnabledDocument + or WordprocessingDocumentType.MacroEnabledTemplate) + { + return; + } + + if (MainPart is { } mainPart) + { + mainPart.DeletePartsRecursivelyOfTypeBase(); + mainPart.DeletePartsRecursivelyOfTypeBase(); + } + } } } } diff --git a/test/DocumentFormat.OpenXml.Tests/DocxTests01.cs b/test/DocumentFormat.OpenXml.Tests/DocxTests01.cs index 48321869f..c214fb3a7 100644 --- a/test/DocumentFormat.OpenXml.Tests/DocxTests01.cs +++ b/test/DocumentFormat.OpenXml.Tests/DocxTests01.cs @@ -361,6 +361,50 @@ public void W039_ChangeDocumentType() } } + [Fact] + public void ChangeDocumentType_MacroEnabledToDocument_RemovesVbaParts() + { + using var stream = new MemoryStream(); + + // Create a macro-enabled document with a VbaProjectPart + using (var doc = WordprocessingDocument.Create(stream, WordprocessingDocumentType.MacroEnabledDocument)) + { + var mainPart = doc.AddMainDocumentPart(); + mainPart.Document = new W.Document(new W.Body(new W.Paragraph(new W.Run(new W.Text("Test"))))); + + // Add a VbaProjectPart (the part that causes the issue) + mainPart.AddNewPart(); + + Assert.NotNull(mainPart.VbaProjectPart); + Assert.Equal(WordprocessingDocumentType.MacroEnabledDocument, doc.DocumentType); + } + + // Re-open and change type to a standard document + stream.Position = 0; + + using (var doc = WordprocessingDocument.Open(stream, true)) + { + Assert.Equal(WordprocessingDocumentType.MacroEnabledDocument, doc.DocumentType); + Assert.NotNull(doc.MainDocumentPart.VbaProjectPart); + + doc.ChangeDocumentType(WordprocessingDocumentType.Document); + + Assert.Equal(WordprocessingDocumentType.Document, doc.DocumentType); + + // After changing to a non-macro type, VbaProjectPart should be removed + Assert.Null(doc.MainDocumentPart.VbaProjectPart); + } + + // Re-open and verify the VBA part is no longer present + stream.Position = 0; + + using (var doc = WordprocessingDocument.Open(stream, false)) + { + Assert.Null(doc.MainDocumentPart.VbaProjectPart); + Assert.Empty(doc.MainDocumentPart.GetPartsOfType()); + } + } + [Fact] public void W038_DocxCreation_Package() {