Which brings me nicely onto the SaveAs() method. I will get onto code that actually manipulates the document later, but for now it’s good to know how SaveAs() doesn’t really care. It simply takes the in-memory document (that we may or may not have changed) and saves it to the filename provided.
public void SaveAs(string fileName)
{
if (!IsOpen)
{
throw new DocumentNotOpenException("The object must be Open before calling SaveAs()");
}
_outputFile = fileName;
if (!string.IsNullOrEmpty(_outputFile))
{
//Close the in-memory document to ensure the memory stream is ready for saving
CloseInMemoryDocument();
try
{
//Now save the stream to file
using (FileStream fileStream = new FileStream(_outputFile, System.IO.FileMode.Create))
{
_inMemoryStream.WriteTo(fileStream);
}
}
finally
{
//Now close the memory stream.
//The in-memory document has already been closed above
//and there's no point having one without the other!
CloseInMemoryStream();
}
}
}
Again, there’s a little housekeeping up top, and then the following...
First the in-memory WordprocessingDocument instance is Closed using the CloseInMemoryDocument() method. Closing this first ensures the MemoryStream is in a complete state (including our changes) and ready for us to write to file.
To do so I simply use a FileStream as follows...
using (FileStream fileStream = new FileStream(_outputFile, System.IO.FileMode.Create))
{
_inMemoryStream.WriteTo(fileStream);
}
Once this is saved, the MemoryStream is also closed since it has fully served its purpose.
So with Open and SaveAs described, now let’s delve into how we can additionally modify the in-memory document before saving.
The method ApplyHeaderAndFooter() modifies the in-memory document by adding or replacing any header and footers with those defined in code. It is a bit long to show in its entirety, so I will describe it bit by bit.
Firstly we get an instance of the in-memory document’s MainDocumentPart.
MainDocumentPart mainPart = _inMemoryDocument.MainDocumentPart;
if (mainPart == null)
{
mainPart = _inMemoryDocument.AddMainDocumentPart();
mainPart.Document = MakeEmpyDocument();
}
else
{
// Delete the existing header part.
mainPart.DeleteParts(mainPart.HeaderParts);
mainPart.DeleteParts(mainPart.FooterParts);
}
My code is going to replace any existing Headers and Footers, so here any existing header/footer parts are removed.
(You’ll also see the code handles the case where there isn’t a MainDocumentPart, though I’m not actually sure whether this can ever happen in practice.)
The next step is to create the new Headers and Footers and add them into the in-memory document.
A header/footer is made up of the following...
· A header/footer Part
· And a reference to that header/footer
· The reference’s for each header/footer need to be put into a SectionProperties element.
This is where things get a little gnarly. During testing I found SectionProperties would occur in different places depending on how many Sections the document had. From what I can tell it’s like this...
· Typically documents are single section anyway, so SectionProperties would be directly under the MainDocumentPart’s > Document > Body element. Header & Footer References would be put in it.
· For multi-sectioned documents SectionProperties not only appear under the Body, but also under Paragraphs within the document. In the cases I have tested (up to 3 section Documents) the Body SectionProperties didn’t seem to get Header/Footer references. They were instead under Paragraph SectionProperties of which there could be many.
With this in mind, my logic does the following...
1. Iterates through any SectionProperties found under Paragraphs
· If found adds the header/footer references
· Sets a flag so later code can determine if any were found.
2. Then locates the last SectionProperty element in the Body
· If there were none found in Paragraphs earlier the header/footer references are added
· Otherwise, they must have already been added to paragraphs so any existing header/footer references can be removed.
The remainder of the method is for identifying the correct SectionProperties to apply the new header & footer to (calls to ApplyHeaderToSectionProperties & ApplyFooterToSectionProperties), and Document.Save() back to the MainDocumentPart.
The ApplyHeaderToSectionProperties method actually makes the header and applies it to the provided SectionProperties.
private void ApplyHeaderToSectionProperties(MainDocumentPart mainPart, SectionProperties sectionProps)
{
// Create a new header part and grab its id for the header reference.
HeaderPart headerPart = mainPart.AddNewPart<HeaderPart>();
string rId = mainPart.GetIdOfPart(headerPart);
//create our Header reference
HeaderReference headerRef = new HeaderReference();
headerRef.Id = rId;
sectionProps.RemoveAllChildren<HeaderReference>();
sectionProps.Append(headerRef);
//Now populate the header contents
headerPart.Header = MakeHeader();
headerPart.Header.Save();
}
A new HeaderPart and its string Id is obtained via the MainDocumentPart retrieved earlier.
A new HeaderReference is created and the Id is set to match the HeaderPart.
Any existing HeaderReferences are removed from the SectionProperties, before adding the new one.
The process is similar for Footers in the ApplyFooterToSectionProperties method...
private void ApplyFooterToSectionProperties(MainDocumentPart mainPart, SectionProperties sectionProps)
{
// Create a new Footer part and grab its id for the Footer reference.
FooterPart footerPart = mainPart.AddNewPart<FooterPart>();
string rId = mainPart.GetIdOfPart(footerPart);
//create our Footer reference
FooterReference footerRef = new FooterReference();
footerRef.Id = rId;
sectionProps.RemoveAllChildren<FooterReference>();
sectionProps.Append(footerRef);
//Now populate the Footer contents
footerPart.Footer = MakeFooter();
footerPart.Footer.Save();
}
Introducing the DocumentRelector Tool
At this point I’m going to jump out of the sample and explain how I coded the actual Header and Footer content.
Doing simple formatting in code isn’t too bad, but it gets complicated fast. I found the DocumentReflector.exe really helped. It is provided with the Open XML SDK and you can find it under your SDK install folder. Typically C:\Program Files\Open XML Format SDK\V2.0\tools\DocumentReflector.exe
You simply run it, point it to a document and it will synthesize C# code that would generate the same document using the SDK.
I doubt you’d ever take this code as read though. In my case, I noticed it would hard code a lot of IDs, so I tended to use it as a starting point, and then refine what I can.
For my Footer code it suggested something like this...
var element =
new Footer(
new Paragraph(
new ParagraphProperties(
new ParagraphStyleId(){ Val = "Footer" },
new Tabs(
new TabStop(){ Val = TabStopValues.Clear, Position = 4320 },
new TabStop(){ Val = TabStopValues.Clear, Position = 8640 },
new TabStop(){ Val = TabStopValues.Center, Position = 4820 },
new TabStop(){ Val = TabStopValues.Right, Position = 9639 })),
new Run(
new FieldChar(){ FieldCharType = FieldCharValues.Begin }),
new Run(
new FieldCode(" TITLE \\* MERGEFORMAT "){ Space = "preserve" }
){ RsidRunAddition = "006C0A3A" },
new Run(
new FieldChar(){ FieldCharType = FieldCharValues.End }),
new Run(
new PositionalTab(){ Alignment = AbsolutePositionTabAlignmentValues.Center, RelativeTo = AbsolutePositionTabPositioningBaseValues.Margin, Leader = AbsolutePositionTabLeaderCharValues.None }
){ RsidRunAddition = "006C0A3A" },
new Run(
new PositionalTab(){ Alignment = AbsolutePositionTabAlignmentValues.Right, RelativeTo = AbsolutePositionTabPositioningBaseValues.Margin, Leader = AbsolutePositionTabLeaderCharValues.None }
){ RsidRunAddition = "006C0A3A" },
new Run(
new Text("Page "){ Space = "preserve" }
){ RsidRunAddition = "006C0A3A" },
new SimpleField(
new Run(
new RunProperties(
new NoProof()),
new Text("1")
){ RsidRunAddition = "008038E8" }
){ Instruction = " PAGE \\* MERGEFORMAT " }
){ RsidParagraphMarkRevision = "005D2110", RsidParagraphAddition = "002D26D8", RsidParagraphProperties = "007F0645", RsidRunAdditionDefault = "00050871" });
return element;
I pretty much trimmed out all the Rsid* statements, and it seemed to work fine for my purposes.
I should point out at this stage, that there is bound to be more to this tool than I describe. I’d certainly recommend exploring it further.
Now back to the code: MakeHeader and MakeFooter
Having used the DocumentReflector tool as a guide, my MakeHeader & MakeFooter methods are as follows...
public Header MakeHeader()
{
Header header = new Header();
Paragraph paragraph = new Paragraph();
Run run = new Run();
Text text = new Text();
text.Text = "Simple header";
run.Append(text);
paragraph.Append(run);
header.Append(paragraph);
return header;
}
public Footer MakeFooter()
{
Footer footer = new Footer();
ParagraphProperties paragraphProperties = new ParagraphProperties(
new ParagraphStyleId() { Val = "Footer" },
new Tabs(
new TabStop() { Val = TabStopValues.Clear, Position = 4320 },
new TabStop() { Val = TabStopValues.Clear, Position = 8640 },
new TabStop() { Val = TabStopValues.Center, Position = 4820 },
new TabStop() { Val = TabStopValues.Right, Position = 9639 }));
Paragraph paragraph = new Paragraph(
paragraphProperties,
new Run(
new FieldChar() { FieldCharType = FieldCharValues.Begin }),
new Run(
new FieldCode(" TITLE \\* MERGEFORMAT ") { Space = "preserve" }
),
new Run(
new FieldChar() { FieldCharType = FieldCharValues.End }),
new Run(
new PositionalTab() { Alignment = AbsolutePositionTabAlignmentValues.Center, RelativeTo = AbsolutePositionTabPositioningBaseValues.Margin, Leader = AbsolutePositionTabLeaderCharValues.None }
),
new Run(
new PositionalTab() { Alignment = AbsolutePositionTabAlignmentValues.Right, RelativeTo = AbsolutePositionTabPositioningBaseValues.Margin, Leader = AbsolutePositionTabLeaderCharValues.None }
),
new Run(
new Text("Page ") { Space = "preserve" }
),
new SimpleField(
new Run(
new RunProperties(
new NoProof()),
new Text("1")
)
) { Instruction = " PAGE \\* MERGEFORMAT " }
);
footer.Append(paragraph);
return footer;
}
Conclusion
So to wrap things up, I’m pretty happy with how the new SDK can be used.
The strangeness about the SectionProperties is not the fault of the SDK though. It seems some understanding of the underlying Open XML format may be required at times.
Perhaps that complexity will be further abstracted in SDK v.next?
Hope you get some value out of my sample. It is structured in such a way that you should be able to extend it to do other document manipulations.