Open-XML-SDK
Open-XML-SDK copied to clipboard
w:sdtContent-related classes should inherit from SdtContentElement or similar
Description
In a code example produced for the DevDays Redmond 2021, I wanted to show how developers can use the strongly-typed classes to remove (potentially nested) w:sdt and related elements (e.g., w:sdtContent) from the markup. The good news is that SdtBlock, SdtRun, SdtRow, and SdtCell are all derived from SdtElement, meaning that all kinds of w:sdt elements can be matched by SdtElement. However, the same is unfortunately not true for SdtContentBlock, SdtContentRun, SdtContentRow, and SdtContentCell. They are directly derived from OpenXmlCompositeElement instead of a (non-existent) base class like SdtContentElement that represents any w:sdtContent element. Therefore, we can't match w:sdtContent elements using just one class. We must use the four concrete strongly typed classes to catch all potential w:sdtContent elements.
Therefore, while the SdtElement class would be enough to match w:sdt elements, there is no SdtContent property of type SdtContentElement that would let us match the w:sdtContent child elements. Thus, we have to match all kinds of w:sdt elements individually and specifically.
While this is not the end of the world, the strongly typed classes could provide an easier and more straightforward way to do such things.
The Open XML SDK also provides other ways to achieve this, e.g., by getting elements by their LocalName (e.g., "sdtContent") or even XName (e.g., W.sdtContent). But that means we don't use the strongly typed classes. Instead, we use OpenXmlElement and some very generic ways to deal with XML markup, much as with Linq to XML.
Information
- .NET Target: Any recent target
- DocumentFormat.OpenXml Version: Any version
Repro
Here is the code example that shows how the strongly typed classes are used to match w:sdt and w:sdtContent elements.
private static object RemoveSdtsTransform(OpenXmlElement element)
{
return element switch
{
SdtBlock sdt => sdt.SdtContentBlock.Elements().AggregateResultsOf(RemoveSdtsTransform),
SdtRun sdt => sdt.SdtContentRun.Elements().AggregateResultsOf(RemoveSdtsTransform),
SdtRow sdt => sdt.SdtContentRow.Elements().AggregateResultsOf(RemoveSdtsTransform),
SdtCell sdt => sdt.SdtContentCell.Elements().AggregateResultsOf(RemoveSdtsTransform),
_ => element.TransformAggregating(RemoveSdtsTransform)
};
}
Here is the corresponding Linq to XML example, in which w:sdt and w:sdtContent elements are matched in a straightforward fashion:
private static object RemoveSdtsTransform(XNode node)
{
return node switch
{
XElement element when element.Name == W.sdt =>
element.Element(W.sdtContent)?.Nodes().Select(RemoveSdtsTransform),
XElement element =>
new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(RemoveSdtsTransform)),
_ => node
};
}
Observed
The strongly typed classes don't make it as easy as it could be.
Expected
The strongly typed classes should make it as easy as with Linq to XML. For example, it would be nice if we could write something like the following in case we simply want to catch all w:sdt and w:sdtContent elements.
private static object RemoveSdtsTransform(OpenXmlElement element)
{
return element switch
{
SdtElement sdt => sdt.SdtContent.Elements().AggregateResultsOf(RemoveSdtsTransform),
_ => element.TransformAggregating(RemoveSdtsTransform)
};
}
Where we want to only match specific kinds of w:sdt elements (e.g., just block-level), we can still use the derived classes (e.g., SdtBlock).
@twsouthwick, what are your thoughts on this? Is it possible to add an SdtContentElement class from which SdtContentBlock, SdtContentRun, SdtContentRow, and SdtContentCell derive?
If so, we should also add the following property to the SdtElement class:
public abstract SdtContentElement? SdtContent { get; set; }
For example, the SdtBlock class could implement that property like this:
public override SdtContentElement? SdtContent
{
get => GetElement<SdtContentBlock>();
set
{
if (value is null)
{
SetElement<SdtContentBlock>(null);
}
else if (value is SdtContentBlock sdtContentBlock)
{
SetElement(sdtContentBlock);
}
else
{
throw new NotSupportedException();
}
}
}
I believe we've had some questions around this idea before, and not sure the best way to handle it. I like the idea of having this kind of thing, but would probably prefer an interface due to multiple inheritance not being a thing. Could this be adapted to an interface pattern?
We'd not have multiple inheritance issues here. For example, SdtContentBlock directly inherits from OpenXmlCompositeElement. The change I am talking about would mean that SdtContentBlock would inherit from the new SdtContentElement, which would inherit from OpenXmlCompositeElement. This is the same pattern as for SdtElement, where SdtBlock inherits from SdtElement, which inherits from OpenXmlCompositeElement.
@ThomasBarnekow I've added this to milestone 3.2 (3.1 is mostly for some office updates we'll be doing in the coming weeks). Is this still of interest to you? If you want to prep a PR for this, it seems reasonable. The code generation will need to be updated to handle this, but that's all in the sdk here to play with however you want :)