wordpress hit counter
Welcome to OpenXML Developer Sign in | Join | Help

Adding Headers and Footers with SDK2.0

by Lawrence Hodson (aka Sharky)

For Microsoft .NET developers the Open XML SDK v2.0 promises a friendlier API for delving into the new Office document formats. It is built over the System.IO.Packaging API, but adds strongly-typed classes, so I thought I’ve give it a whirl.

I’ve not used either API’s before, nor was I familiar with the Open XML format. I came at it with fresh eyes, and no preconceptions – other than being happy to avoid old school Office Automation.

So let’s get started. 

The Goal

I set out to build a simple application that simply added (or replaced) Headers and Footers of a Word document with those defined in the application. 

I specifically wanted to tinker with the API so I deliberately avoided taking an XML approach seen in some other samples out there. Instead I do everything with purely code based approach using the API methods and properties provided.

I’m undecided on whether this is the best approach, but it was a good experiment, and achieved the end goal.

The Header I will be adding is just a simple left-aligned “Simple header” text.

The footer will be a little more complicated however. It is a Left-aligned “TITLE” FieldCode, and a Right-aligned “Page “ + “PAGE” FieldCode.

 

...and again with field codes toggled...

 

The Sample Solution

Go ahead and download the sample code.  It is the complete solution, whereas this article will only cover snippets of particular interest.

The solution is made up of the following:

      ·         OOXMLSandpit.HeadersAndFooters.Library
this is a class library that does all the Open XML specific stuff.

      ·         OOXMLSandpit.HeadersAndFooters.App
this is a simple Console application that uses the Library to modify the specified Document.

      ·         Lib
the Lib folder contains the DocumentFormat.OpenXml.dll referenced by the rest of the code.

 

The console application is pretty simple, and needs little explanation for this article.  Simply run it at the command line specifying the Source File name and Output file name. Using the Library assembly, the source document will be opened, headers and footers applied, and saved to the output file name. 

NOTE: If an output file is not specified the source file will be overwritten with the changes.

The usage is as follows:

OOXMLSandpit.HeadersAndFooters.App.exe [/?]|[sourceFile [outputFile]]

Where:

/?           : show help
sourceFile   : path of the source file
outputFile   : path to save the new file

 

About the code: OOXMLSandpit.HeadersandFooters.Library

The library assembly is where all the real action happens.

Firstly, I’ll just explain the approach I took.  I decided to create a simple WordprocessingWorker class that pretty much abstracts any Open XML complexity. A typical use of it would look something like this...

using (WordprocessingWorker worker = new WordprocessingWorker())

{

            worker.Open(_sourceFile);

            worker.ApplyHeaderAndFooter();

            worker.Validate();

            worker.SaveAs(_outputFile);

            worker.Close();

}

 

The observant will notice it’s all wrapped in a “using”, which is good practice when using a class that implements IDisposable. It ensures Dispose is called releasing system resources the instance may hold.

 

In the case of my WordprocessingWorker class I need to implement IDisposable because of the design I have chosen.

Most samples I see wrap Open XML WordprocessingDocument objects in a “using” statement because they do everything in one call.  I decided to take another approach and let my class be a little more interactive. It is potentially more useful that way, and is a good foundation for future samples too.

So my object has a lifecycle of Open a document > Change stuff > SaveAs the changed document. 

Now let’s take a look at the Open method

 

public void Open(string fileName)

{

    //if the document is already Open well close it first to free up resources.

    if (IsOpen)

    {

        Close();

    }

 

    _sourceFile = fileName;

 

    //populate a MemoryStream with byte array of the Document

    byte[] sourceBytes = File.ReadAllBytes(_sourceFile);

    _inMemoryStream = new MemoryStream();

    _inMemoryStream.Write(sourceBytes, 0, (int)sourceBytes.Length);

 

    //Create the in-memory Document from the stream for modification

    _inMemoryDocument = WordprocessingDocument.Open(_inMemoryStream, true);

}

 

The Close() at the beginning is just a bit of housekeeping to make the class more robust.

The rest of this method is focused on loading the specified document file into a MemoryStream and Open XML WordprocessingDocument instance.

I could have had the WordprocessingDocument open the file directly, instead of using a MemoryStream. Using the MemoryStream means I can manipulate a copy of the source document in-memory and SaveAs to a different file later.

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.

Published Tuesday, April 07, 2009 8:23 AM by jenw
Filed Under: ,
Attachment(s): Headers and Footers.zip

Comments

No Comments
Anonymous comments are disabled