http://en.wikipedia.org/wiki/Image:Cardinalplace.jpgI am very excited about the Document Interoperability Initiative (DII) event that Doug recently announced, which is coming up in May. The event is taking place in London where Fraunhofer will be sharing a community project they recently started to create an IS29500 validator and test document library. This project was started to address feedback from developers at past DII workshops about the need for a validator to ensure that the IS29500 documents they create will interoperate well with other implementations. They clearly stated that implementers need a place where they can go to download tools and resources that allow them to validate their documents against the IS29500 standard. While Microsoft is one of the contributors to this project, this is a community project that anyone can contribute to.

The DII event in London is a free event that anyone can attend. I’ll be there to update everyone on some of the things we’re currently doing to enable interoperability, and I’m also managing registrations for the event so if you would like to come, send me an email and I will provide you with the event details. Fraunhofer will be sharing details on how you can contribute to the community project. If you are not able to make it to the event, but still want to contribute, soon you will be able to go to the project website to read about it and sign up. I will post details as soon as they are available. If you have any specific questions about the project, let me know and I will do my best to answer your questions.

Custom Document Format Interoperability

You may have heard that Office 2007 SP2 will now support editing files in the OpenDocument 1.1 (ODF) format. This document format was added to Office’s long list of supported documents formats to give customers more choices for the format they use to save their documents.

In addition to allowing you to edit the ODF 1.1 format within Office 2007, SP2 also supports a new External File Format API that can be used to edit other document formats as well. With this API, users can choose to save their documents in any format they want. In this post we will explore how to use the API to enable Office 2007 to edit our own custom document format. We will then use Office 2007 to save our custom format as DOCX, ODT and HTML.

Our Custom Document Format

For the purpose of this article, we have a company who needs to manage their sales pipeline information. The data is available as XML, but they do not want to spend the money to build a custom editor. They just want to let their users edit the pipeline data in Word, as a table. They give these files an extension of SPLX (i.e. Sales PipeLine Xml)

The sales pipeline information is made up of a series of SalesItem tags, each with a unique id that represents the index of the item. They track the name of the customer (CustomerName), how much the deal represents (DealValue) and a percent that represents how confident they are that the sales opportunity will close (ConfidencePercent).

Here is the sample XML file:


<?xml version="1.0" encoding="utf-8"?>
<SalesPipeline>
    <SalesItem id="1">
        <CustomerName>ABC Company</CustomerName>
        <DealValue>1000000</DealValue>
        <ConfidencePercent>.2</ConfidencePercent>
    </SalesItem>
    <SalesItem id="2">
        <CustomerName>123 Company</CustomerName>
        <DealValue>1200000</DealValue>
        <ConfidencePercent>.15</ConfidencePercent>
    </SalesItem>
    <SalesItem id="3">
        <CustomerName>XNA Company</CustomerName>
        <DealValue>500000</DealValue>
        <ConfidencePercent>.65</ConfidencePercent>
    </SalesItem>
    <SalesItem id="4">
        <CustomerName>Defender Company</CustomerName>
        <DealValue>60000</DealValue>
        <ConfidencePercent>.9</ConfidencePercent>
    </SalesItem>
</SalesPipeline>

We will create an External File Converter that will transform the XML into a WordprocessingML, document when opened; and then transform the respective document format back to the XML format when saved. This will allow the users to edit the sales pipeline information in Office 2007, while keeping the data in their own XML document format.

Implementing our Custom External File Converter

Create an Out-of-Process COM Object

Use the following list of steps to create an out-of-process COM object.

  1. Open Visual Studio with Administrator privileges. You can do this by right-clicking on the Visual Studio link in the start menu and selecting Run as Administrator. Administrative privileges will be needed because the COM object will make changes to the registry when registering with COM+ services.


  2. Create a new Project (File -> New ->Project)

  3. In the Project types list, select Visual C# -> Windows. In the Templates list, select the Empty Project item. Type "MyEFC" for the Solution Name and "SalesPipeline" for the Name. Your New Project window should look like the picture below, then click the Ok button.


  4. Right-click on the project References, and select Add References...


  5. Select the .NET tab, and select the System.EnterpriseServices item with Version 2.0.0.0. Click the Ok button to add the reference.


  6. Right-click on the project References, and select Add References…. Select the .NET tab, and select the System.Windows.Forms item with Version 2.0.0.0. Click the Ok button to add the reference.

  7. Right-click on the project References, and select Add References…. Select the COM tab, and select the Microsoft Office 12.0 Object Library item with TypeLib Version 2.4. Click the Ok button to add the reference.

  8. Now we will add a class for our COM server. Right-click on the project and select Add -> Class.

  9. Enter MyCOMServer.cs for the Name; then click the Ok button to create the class. Visual Studio will automatically add some additional references that are needed.

  10. Update the class in the following ways.
    • Add a using statement to the System.Windows.Forms namespace.
    • Add a static Main() entry point method to the class.
    • Mark the entry point method as single threaded by applying the [STAThread] attribute.
    • Create a Windows message loop by calling Application.Run() method.
    • Your code should now look something like this:
     
    using System;
    using System.Windows.Forms;

    namespace SalesPipeline
    {
        static class MyCOMServer
        {
            [STAThread]
            static void Main()
            {
                Application.Run();
            }
        }
    }

  11. We now need to add assembly flags to make our assembly COM visible and assign it a GUID. Right-click on the project and select Properties. Select the Application tab, then click the Assembly Information… button. Enter "{BD3489D9-EAE7-4c9d-BF88-D7B7C05DDE45}" into the GUID field, then click Ok. IMPORTANT: Click the Assembly Information… button a second time and this time, check the Make assembly COM-Visible checkbox; then click Ok button again. Doing this a second time is important.


  12. Next, we need to set the Project Output type to a Windows Application. While the tab is selected on the project properties, select Windows Application from the Output Application type drop-down.


  13. Next we need to sign the assembly. While still in the project properties, select the Signing tab. Check the Sign the assembly checkbox, and select <New...> from the drop down.


  14. Type SalesPipelineKey into the Key file name field. Uncheck the Protect my key file with a password checkbox and click the Ok button.


  15. Open the AssemblyInfo.cs file make the following list of changes:
    • Add a reference to the System.EnterpriseServices namespace
    • Add the ApplicationActivation assembly attribute with the ActivationOption.Server parameter
    • Add the ApplicationAccessControl assembly attribute with a false parameter
    • Make sure the that ComVisible assembly attribute has a true parameter
    • Make sure the Guid assembly attribute is set to the correct Guid
    • The following is the code that reflects these steps
     
    ...
    using System.EnterpriseServices;
    ...
    [assembly: ComVisible(true)]
    [assembly: ApplicationActivation(ActivationOption.Server)]
    [assembly: ApplicationAccessControl(false)]
    [assembly: Guid("BD3489D9-EAE7-4c9d-BF88-D7B7C05DDE45")]
    ...

  16. At this point, you should have a windowless COM server that is ready to host our out-of-process COM object. Compile and run the application to ensure that things are working correctly. Note: When you run the application, nothing will happen, but Visual Studio should be in a debug state. Click the Stop Debugging to stop the application.

Create a Basic External File Converter

Now that we have created a COM server it is time to create our External File Converter COM object. Use the following steps to create the COM object:

  1. Right-click on the Project and select Add -> Class. Enter SalesPipelineConverter.cs in the Name field and click the Add button to create the class.
    • Add references to the Microsoft.Office.Core, System.EnterpriseServices and System.Runtime.InteropServices namespaces.
    • Add the ComVisible attribute with a parameter of true.
    • Add the Guid attribute with a parameter of "CC03A6F5-8517-48c6-B8A5-DD287855F9BA"
    • Mark the class as public, and inherit it from the ServicedComponent class
    • Inherit the class from the IConverter interface, and add the default implementation
    • Compile your code to make sure that there are no syntax mistakes. Your code should now look something like this:
     
    using System;
    using Microsoft.Office.Core;
    using System.EnterpriseServices;
    using System.Runtime.InteropServices;

    namespace SalesPipeline
    {
        [ComVisible(true)]
        [Guid("CC03A6F5-8517-48c6-B8A5-DD287855F9BA")]
        public class SalesPipelineConverter : ServicedComponent,
            IConverter
        {
            //
            // IConverter Members
            //
            public void HrExport(
                string bstrSourcePath,
                string bstrDestPath,
                string bstrClass,
                IConverterApplicationPreferences pcap,
                out IConverterPreferences ppcp,
                IConverterUICallback pcuic)
            {
                throw new NotImplementedException();
            }
            public void HrGetErrorString(
                int hrErr,
                out string pbstrErrorMsg,
                IConverterApplicationPreferences pcap)
            {
                throw new NotImplementedException();
            }

            public void HrGetFormat(
                string bstrPath,
                out string pbstrClass,
                IConverterApplicationPreferences pcap,
                out IConverterPreferences ppcp,
                IConverterUICallback pcuic)
            {
                throw new NotImplementedException();
            }

            public void HrImport(
                string bstrSourcePath,
                string bstrDestPath,
                IConverterApplicationPreferences pcap,
                out IConverterPreferences ppcp,
                IConverterUICallback pcuic)
            {
                throw new NotImplementedException();
            }

            public void HrInitConverter(
                IConverterApplicationPreferences pcap,
                out IConverterPreferences ppcp,
                IConverterUICallback pcuic)
            {
                throw new NotImplementedException();
            }

            public void HrUninitConverter(
                IConverterUICallback pcuic)
            {
                throw new NotImplementedException();
            }
        }
    }


  2. Right-click on the Project and select Add -> Class. Enter SalesPipelineConverterPreferences.cs in the Name field and click the Add button to create the class.
    • Add references to the Microsoft.Office.Core namespace.
    • Mark the class as public, inherit it from the IConverterPreferences interface, and add the default implementation
    • Compile your code to make sure that there are no syntax mistakes. Your code should now look something like this:
     
    using System;
    using Microsoft.Office.Core;

    namespace SalesPipeline
    {
        public class SalesPipelineConverterPreferences :         IConverterPreferences
        {
            //
            // IConverterPreferences Members
            //
            public void HrCheckFormat(
                out int pFormat)
            {
                throw new NotImplementedException();
            }
            
            public void HrGetLossySave(
                out int pfLossySave)
            {
                throw new NotImplementedException();
            }
            
            public void HrGetMacroEnabled(
                out int pfMacroEnabled)
            {
                throw new NotImplementedException();
            }
        }
    }


  3. Next we will provide a default implementation for the IConverterPreferences interface.
    • Update the HrCheckFormat method, setting the pFormat output parameter to a value of 1. This setting specifies that we support the WordprocessingML ECMA376 macro-free document format.
    • Update the HrGetLossySave method, setting the pfLossySave output parameter to the integer value of false. This setting specifies that there is no loss of data when saved through our converter.
    • Update the HrGetMacroEnabled method, setting the pfMacroEnabled output parameter to the integer value of false. This setting specifies that we do not support macro enabled formats.
    • Compile your code to make sure that there are no syntax mistakes. The code for your IConverterPreferences implementation should now look something like this:
     
    //
    // IConverterPreferences Members
    //
    public void HrCheckFormat(
        out int pFormat)
    {
        pFormat = 1;
    }

    public void HrGetLossySave(
        out int pfLossySave)
    {
        pfLossySave = Convert.ToInt32(false);
    }

    public void HrGetMacroEnabled(
        out int pfMacroEnabled)
    {
        pfMacroEnabled = Convert.ToInt32(false);
    }


  4. Next we will provide a default implementation for the IConverter interface:
    • Update the HrExport method, setting the ppcp output parameter to a new SalesPipelineConverterPreferences instance.
    • Update the HrGetErrorString, setting the pbstrErrorMsg output parameter to null.
    • Update the HrGetFormat method, setting the pbstrClass output parameter to “SalesPipelineConverter” and setting the ppcp output parameter to a new SalesPipelineConverterPreferences instance.
    • Update the HrImport method, setting the ppcp output parameter to a new SalesPipelineConverterPreferences instance.
    • Update the HrInitConverter method, setting the ppcp output parameter to a new SalesPipelineConverterPreferences instance.
    • Update the HrUninitConverter method, to have no code in it.
    • Compile your code to make sure that there are no syntax mistakes. The code for your IConverter implementation should now look something like this:
     
    //
    // IConverter Members
    //
    public void HrExport(
        string bstrSourcePath,
        string bstrDestPath,
        string bstrClass,
        IConverterApplicationPreferences pcap,
        out IConverterPreferences ppcp,
        IConverterUICallback pcuic)
    {
        ppcp = new SalesPipelineConverterPreferences();
    }
    public void HrGetErrorString(
        int hrErr,
        out string pbstrErrorMsg,
        IConverterApplicationPreferences pcap)
    {
        pbstrErrorMsg = null;
    }

    public void HrGetFormat(
        string bstrPath,
        out string pbstrClass,
        IConverterApplicationPreferences pcap,
        out IConverterPreferences ppcp,
        IConverterUICallback pcuic)
    {
        pbstrClass = "SalesPipelineConverter";
        ppcp = new SalesPipelineConverterPreferences();
    }

    public void HrImport(
        string bstrSourcePath,
        string bstrDestPath,
        IConverterApplicationPreferences pcap,
        out IConverterPreferences ppcp,
        IConverterUICallback pcuic)
    {
        ppcp = new SalesPipelineConverterPreferences();
    }

    public void HrInitConverter(
        IConverterApplicationPreferences pcap,
        out IConverterPreferences ppcp,
        IConverterUICallback pcuic)
    {
        ppcp = new SalesPipelineConverterPreferences();
    }

    public void HrUninitConverter(
        IConverterUICallback pcuic)
    {
        // do nothing for now
    }


  5. Now that we have created a default implementation for an External File Converter, we need to update our COM Server to register and unregister our COM object when the COM server starts and ends. Update the main method using the following steps:
    • Add a call to the Application.OleRequired method before the call to Application.Run.
    • Create our COM object before the call to Application.Run
    • Dispose of our COM object when the call to the Application.Run method returns
    • Compile your code to make sure that there are no syntax mistakes. Your Main method should now look something like this:
     
    using System;
    using System.Windows.Forms;

    namespace SalesPipeline
    {
        static class MyCOMServer
        {
            [STAThread]
            static void Main()
            {
                Application.OleRequired();
                SalesPipelineConverter salesPipelineConverter =
                    new SalesPipelineConverter();
                Application.Run();
                if (salesPipelineConverter != null)
                    salesPipelineConverter.Dispose();
                salesPipelineConverter = null;
            }
        }
    }


  6. Set a breakpoint on the call to the Application.OleRequired method and Run your application. Step through the code till it gets to the Application.Run method, then Run the application. Make sure there are no runtime errors, and the call to create the SalesPipelineConverter object should take a minute as your COM object should register with COM+ services. The COM object is now ready to test with the Word 2007 SP2 application.
    • Note: If you receive an error that the application must be run as an Administrator. Close all instances of Visual Studio, and open it using the right-click, Run as Administrator steps described earlier. This may cause an issue with the registration of your object in COM+ services and you may need to restart from the beginning, using different names and Guid IDs.

  7. To test our External File Converter, we need to register our COM object with the Word application through the registry. Add the following registry keys:
     
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\12.0\Word\Text Converters\OOXML Converters]

    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\12.0\Word\Text Converters\OOXML Converters\Export]

    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\12.0\Word\Text Converters\OOXML Converters\Export\Sales Pipeline]
    "Clsid"="{CC03A6F5-8517-48c6-B8A5-DD287855F9BA}"
    "Name"=" Sales Pipeline"
    "Extensions"="splx"

    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\12.0\Word\Text Converters\OOXML Converters\Import]

    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\12.0\Word\Text Converters\OOXML Converters\Import\Sales Pipeline]
    "Clsid"="{CC03A6F5-8517-48c6-B8A5-DD287855F9BA}"
    "Name"=" Sales Pipeline"
    "Extensions"="splx"

  8. Before we test our file converter, we need to create a file with the .splx extension that includes the sales pipeline data. Create a file named Sales Pipeline Data Jan 2009.splx, and copy the sales pipeline data listed earlier in this post into the file; then save and close the file.

Implement Import/Export for our Custom File Format

Now that we have created a basic External File Converter, it is time to customize the HrImport and HrExport methods. The HrImport method will be customized to convert our Sales Pipeline XML into a Word table when opened. The HrExport method will be customized to convert the Word table into our Sales Pipeline XML when saved.

  1. Add a reference to System.Xml.Linq, DocumentFormat.OpenXml and WindowsBase
  2. Add using statements to System.IO, System.Xml, System.Xml.Linq, and DocumentFormat.OpenXml, DocumentFormat.OpenXml.Packaging and DocumentFormat.OpenXml.Wordprocessing
     
    using System;
    using System.IO;
    using System.Xml;
    using System.Xml.Linq;
    using Microsoft.Office.Core;
    using System.EnterpriseServices;
    using System.Runtime.InteropServices;

    using OpenXml = DocumentFormat.OpenXml;
    using Packaging = DocumentFormat.OpenXml.Packaging;
    using Wordprocessing = DocumentFormat.OpenXml.Wordprocessing;

  3. Update the HrImport() method with the following code, which reads through the SPLX document format and creates a Word Table with the data.
     
    public void HrImport(
        string bstrSourcePath,
        string bstrDestPath,
        IConverterApplicationPreferences pcap,
        out IConverterPreferences ppcp,
        IConverterUICallback pcuic)
    {
        ppcp = new SalesPipelineConverterPreferences();

        int tempIndex = 0;
        bool foundFile = false;
        string tempDocPath = "";
        string tempDir = (new FileInfo(bstrDestPath)).Directory.FullName;
        while (!foundFile && tempIndex < 999)
        {
            tempDocPath = String.Format("{0}\\~SalesPipeline{1:0000}.docx", tempDir, tempIndex++);
            if (!File.Exists(tempDocPath))
                foundFile = true;
        }
        if (!foundFile)
            throw new FileNotFoundException("Unable to create temp file");

        using (Packaging.WordprocessingDocument tempDoc = Packaging.WordprocessingDocument.Create(tempDocPath, OpenXml.WordprocessingDocumentType.Document))
        {
            // create the table, table properties, and header row
            Wordprocessing.Table salesPipelineTable = new DocumentFormat.OpenXml.Wordprocessing.Table(
                new Wordprocessing.TableProperties(
                    new Wordprocessing.TableStyle() { Val = "TableGrid" },
                    new Wordprocessing.TableWidth() { Width = 0, Type = Wordprocessing.TableWidthUnitValues.Auto },
                    new Wordprocessing.TableBorders(
                        new Wordprocessing.TopBorder() { Val = Wordprocessing.BorderValues.Single, Size = 4, Space = 0, Color = "auto" },
                        new Wordprocessing.LeftBorder() { Val = Wordprocessing.BorderValues.Single, Size = 4, Space = 0, Color = "auto" },
                        new Wordprocessing.BottomBorder() { Val = Wordprocessing.BorderValues.Single, Size = 4, Space = 0, Color = "auto" },
                        new Wordprocessing.RightBorder() { Val = Wordprocessing.BorderValues.Single, Size = 4, Space = 0, Color = "auto" },
                        new Wordprocessing.InsideHorizontalBorder() { Val = Wordprocessing.BorderValues.Single, Size = 4, Space = 0, Color = "auto" },
                        new Wordprocessing.InsideVerticalBorder() { Val = Wordprocessing.BorderValues.Single, Size = 4, Space = 0, Color = "auto" }),
                    new Wordprocessing.TableCellMargin(
                        new Wordprocessing.TopMargin() { Width = 10, Type = Wordprocessing.TableWidthUnitValues.Dxa },
                        new Wordprocessing.LeftMargin() { Width = 10, Type = Wordprocessing.TableWidthUnitValues.Dxa },
                        new Wordprocessing.BottomMargin() { Width = 10, Type = Wordprocessing.TableWidthUnitValues.Dxa },
                        new Wordprocessing.RightMargin() { Width = 10, Type = Wordprocessing.TableWidthUnitValues.Dxa }),
                    new Wordprocessing.TableLook() { Val = "04A0" }),
                new Wordprocessing.TableGrid(
                    new Wordprocessing.GridColumn() { Width = 3192 },
                    new Wordprocessing.GridColumn() { Width = 3192 },
                    new Wordprocessing.GridColumn() { Width = 3192 }),
                new Wordprocessing.TableRow(
                    new Wordprocessing.TableCell(
                        new Wordprocessing.TableCellProperties(
                            new Wordprocessing.TableCellWidth() { Width = 3192, Type = Wordprocessing.TableWidthUnitValues.Dxa }),
                        new Wordprocessing.Paragraph(
                            new Wordprocessing.Run(
                                new Wordprocessing.Text("Customer Name")))),
                    new Wordprocessing.TableCell(
                        new Wordprocessing.TableCellProperties(
                            new Wordprocessing.TableCellWidth() { Width = 3192, Type = Wordprocessing.TableWidthUnitValues.Dxa }),
                        new Wordprocessing.Paragraph(
                            new Wordprocessing.ParagraphProperties(
                                new Wordprocessing.Justification() { Val = Wordprocessing.JustificationValues.Center }),
                            new Wordprocessing.Run(
                                new Wordprocessing.Text("Deal Value")))),
                    new Wordprocessing.TableCell(
                        new Wordprocessing.TableCellProperties(
                            new Wordprocessing.TableCellWidth() { Width = 3192, Type = Wordprocessing.TableWidthUnitValues.Dxa }),
                        new Wordprocessing.Paragraph(
                            new Wordprocessing.ParagraphProperties(
                                new Wordprocessing.Justification() { Val = Wordprocessing.JustificationValues.Center }),
                            new Wordprocessing.Run(
                                new Wordprocessing.Text("Confidence %"))))));

            // loop through each sales item and add a row to the table
            XDocument salesPipelineDoc = XDocument.Load(bstrSourcePath);
            foreach (XElement salesItem in salesPipelineDoc.Root.Descendants("SalesItem"))
            {
                salesPipelineTable.Append(new Wordprocessing.TableRow(
                    new Wordprocessing.TableCell(
                        new Wordprocessing.TableCellProperties(
                            new Wordprocessing.TableCellWidth() { Width = 3192, Type = Wordprocessing.TableWidthUnitValues.Dxa }),
                        new Wordprocessing.Paragraph(
                            new Wordprocessing.Run(
                                new Wordprocessing.Text(salesItem.Element("CustomerName").Value)))),
                    new Wordprocessing.TableCell(
                        new Wordprocessing.TableCellProperties(
                            new Wordprocessing.TableCellWidth() { Width = 3192, Type = Wordprocessing.TableWidthUnitValues.Dxa }),
                        new Wordprocessing.Paragraph(
                            new Wordprocessing.ParagraphProperties(
                                new Wordprocessing.Justification() { Val = Wordprocessing.JustificationValues.Center }),
                            new Wordprocessing.Run(
                                new Wordprocessing.Text(String.Format("${0:#,#}", Convert.ToInt32(salesItem.Element("DealValue").Value)))))),
                    new Wordprocessing.TableCell(
                        new Wordprocessing.TableCellProperties(
                            new Wordprocessing.TableCellWidth() { Width = 3192, Type = Wordprocessing.TableWidthUnitValues.Dxa }),
                        new Wordprocessing.Paragraph(
                            new Wordprocessing.ParagraphProperties(
                                new Wordprocessing.Justification() { Val = Wordprocessing.JustificationValues.Center }),
                            new Wordprocessing.Run(
                                new Wordprocessing.Text(String.Format("{0:#}%", (Convert.ToDecimal(salesItem.Element("ConfidencePercent").Value) * 100))))))));
            }

            // create a document part and markup, inserting the table we created
            tempDoc.AddMainDocumentPart();
            tempDoc.MainDocumentPart.Document =
                new Wordprocessing.Document(
                    new Wordprocessing.Body(
                        salesPipelineTable));
            tempDoc.MainDocumentPart.Document.Save();
            tempDoc.Close();
        }
        File.Copy(tempDocPath, bstrDestPath, true);
        File.Delete(tempDocPath);
    }


  4. Update the HrExport() method with the following code, which reads through the Word Table and exports the values to the SPLX document format.
     
    public void HrExport(
        string bstrSourcePath,
        string bstrDestPath,
        string bstrClass,
        IConverterApplicationPreferences pcap,
        out IConverterPreferences ppcp,
        IConverterUICallback pcuic)
    {
        ppcp = new SalesPipelineConverterPreferences();

        XDocument salesPipelineDoc = new XDocument(new XElement("SalesPipeline"));

        // open the source document
        using (Packaging.WordprocessingDocument tempDoc = Packaging.WordprocessingDocument.Open(bstrSourcePath, false))
        {
            int rowIndex = 0;
            foreach (Wordprocessing.TableRow tableRow in tempDoc.MainDocumentPart.Document.Descendants<Wordprocessing.TableRow>())
            {
                // skip the header row
                if (rowIndex == 0)
                {
                    rowIndex++;
                    continue;
                }

                int cellIndex = 1;
                string customerName = "";
                string dealValue = "";
                string confidencePercent = "";
                foreach (Wordprocessing.TableCell cell in tableRow.Descendants<Wordprocessing.TableCell>())
                {
                    if (cellIndex == 1)
                        customerName = cell.InnerText;
                    else if (cellIndex == 2)
                        dealValue = cell.InnerText.Replace("$", "").Replace(",", "");
                    else if (cellIndex == 3)
                        confidencePercent = (Convert.ToDecimal(cell.InnerText.Replace("%", "")) / 100).ToString();
                    cellIndex++;
                }

                salesPipelineDoc.Root.Add(
                    new XElement("SalesItem",
                        new XAttribute("id", rowIndex),
                        new XElement("CustomerName", customerName),
                        new XElement("DealValue", dealValue),
                        new XElement("ConfidencePercent", confidencePercent)));

                rowIndex++;
            }
        }

        // save it to XML
        salesPipelineDoc.Save(bstrDestPath);
    }


Test the Sales Pipeline External File Converter

You are now ready to test your custom External File Converter.

  1. Open Word 2007 SP2, and select Open from the Office Menu
  2. Select Sales Pipeline (*.splx) from the file type drop-down
  3. Select the Sales Pipeline Data Jan 2009.splx file that you created earlier


  4. Word should open the file and display the data in a table


  5. You can add a row, fill in the appropriate values, and Save the document
  6. The Sales Pipeline Data Jan 2009.splx file should now contain more XML with the data from the newly added row.

You can also edit your sales pipeline information in the ODT format by doing the following:

  1. Use the Save As feature of Word to save the document as an Open Document Text (*.odt) format
  2. Continue editing the ODT file in Word or close Word and open the ODT file using your favorite ODT editor. For example, you could open the file using Open Office Writer or Symphony.
  3. Add a row of data and save the file
  4. If using an application other than Word, open the file using Word 2007, then use the Save As feature to save the document as our Sales Pipeline (*.splx) format
  5. The row(s) that you added should now be saved in our custom document format. You can open the *.splx file using an XML editor to see the added XML record(s).

You can also save your custom format in any other format that Office 2007 SP2 supports. For example, you can use the Save As feature of Word to save the document as HTML.

External File Converter Resources

If you want to create your own Open XML External File Converter, you can read more about and download the API on MSDN. The MSDN article has a link to a code sample that you can download.

As always, let me know if you have any questions or comments.J