Chapter 10: Word Automation Services (SharePoint 2010 Web App The Complete Reference)

Summary:  This chapter discusses building Microsoft Word web-based applications in Microsoft SharePoint 2010 by using OpenXML.

Applies to: Business Connectivity Services | Office 2010 | SharePoint Foundation 2010 | SharePoint Server 2010 | Visual Studio | Word

In this article
Introduction
Word Automation Services
OpenXML
Demonstration Scenario
Summary
Recommended Reading
About the Author

This article is an excerpt from Microsoft SharePoint 2010 Web Applications The Complete Reference by Charlie Holland from McGraw-Hill (ISBN <<0071744568 / 9780071744560>>, copyright McGraw-Hill 2011, all rights reserved). No part of these chapters may be reproduced, stored in a retrieval system, or transmitted in any form or by any means—electronic, electrostatic, mechanical, photocopying, recording, or otherwise—without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.

Contents

  • Introduction

  • Word Automation Services

  • OpenXML

  • Demonstration Scenario

  • Summary

  • Recommended Reading

  • About the Author

Introduction

In the 2007 Microsoft Office system, Microsoft introduced a new file format called OpenXML. As an ECMA (European Computer Manufacturers Association) standard, OpenXML is well documented and open to all developers to use as they see fit on all platforms.

One of the immediate benefits of this innovation is that it allows the programmatic creation and modification of Microsoft Office files such as Word documents and Microsoft Excel spreadsheets. Before OpenXML, the only way to create or modify these documents was to automate the appropriate client application. Although this works well in a client scenario, performing such automation on a server is prone to problems. Using OpenXML, particularly with SharePoint, allows developers to create composite Office documents easily using server-side code and return the finished results to users in a format that can be used by most common Office applications. Having said that, however, certain actions can’t be performed using OpenXML. Because OpenXML is simply an XML-based file format, none of the capabilities of the client application are available. Performing actions such as repagination or updating dynamic content isn’t possible via OpenXML. As a result, developers requiring this type of functionality are required to go down the client application automation route with its attendant issues.

With SharePoint 2010, Microsoft picked up on this shortfall and introduced a new application service known as Word Automation Services. This chapter takes a look at this new service and discusses how you can leverage OpenXML to create custom line-of-business applications.

Word Automation Services

Microsoft Word Automation Services is available as a service application in Microsoft SharePoint Server 2010. Three components make up the overall architecture:

  • Front-end object model In Chapter 9, you saw how the architecture of the framework allows for the generation of proxy classes that can be used on the front end to access the underlying service. The Word Automation Services object model is an example of such proxy classes.

  • Document queue The Word Automation Services application makes use of a central database to maintain a queue of jobs being processed. Each job may contain one or more documents. The primary function of the service application is to write items to the queue and to retrieve details of the status of current jobs.

  • Word Automation Services engine The real work behind Word Automation Services occurs in a separate rendering engine that is capable of rendering Word documents and providing a range of features such as file format conversions, dynamic data updates, and repagination. The output of the rendering engine is then stored in the SharePoint content database as a new document.

Creating Conversion Jobs

From a front-end object model perspective, the primary interface to Word Automation Services is the [ConversionJob] class. The following code snippet shows how to create a job to convert a document to a PDF format:

SPFile temp = folder.Files.Add("temp.docx", mem, true);
SPServiceApplicationProxy proxy=SPServiceContext.Current.GetDefaultProxy( 
    typeof(WordServiceApplicationProxy));
ConversionJob convJob = new ConversionJob(proxy.Id); 
convJob.Name = "Document Assembly";
convJob.UserToken = SPContext.Current.Web.CurrentUser.UserToken;
convJob.Settings.UpdateFields = true;
convJob.Settings.OutputFormat = SaveFormat.PDF;
convJob.Settings.OutputSaveBehavior = SaveBehavior.AlwaysOverwrite;
string siteUrl = SPContext.Current.Site.Url + "/";
string outputUrl = siteUrl + temp.Url.Replace(".docx", ".pdf");
convJob.AddFile(siteUrl + temp.Url, outputUrl);
convJob.Start();

Determining which action should be performed on all documents within a job is a job for the ConversionJobSettings class. The following class view shows the main properties:

Figure 1. ConversionJobSettings Class

ConversionJobSettings class

Checking Status of Conversion Jobs

In Word Automation Services, document processing actually occurs in a separate process, and all jobs are queued for processing. As a result of this, determining the status of a submitted job is an important requirement when it comes to designing an application that makes use of the service.

We can retrieve the status of a submitted job by querying the ConversionJob object that we used to create the job. Effectively, a ConversionJob object is passed as a Windows Communication Foundation (WCF) message from client to server; on the server side, after the job has been written to the document queue database, a job identifier is returned to the client. The identifier can be obtained by querying the ConversionJob.JobId property.

Because conversion jobs can take a long time to complete, common practice is to store the job identifier for later use. A separate process can then periodically check on the status of the job, as the following snippet shows:

string ConversionJobId = SPContext.Current.ListItem.GetFormattedValue(
"ConversionJobId");
if (!string.IsNullOrEmpty(ConversionJobId))
{
    WordServiceApplicationProxy proxy = SPServiceContext.Current.GetDefaultProxy(
    typeof(WordServiceApplicationProxy)) as WordServiceApplicationProxy;
    ConversionJobStatus status = new ConversionJobStatus(
        proxy, new Guid(ConversionJobId), null);
    if (status.Count == status.Succeeded + status.Failed)
    {
        //Job Completed
    }
    else
    {
        //Job in progress
    }
}

OpenXML

As described at the beginning of this chapter, OpenXML is an XML-based file format that’s supported by applications in the 2007 Microsoft Office system and later. Although an in-depth discussion on all aspects of OpenXML is beyond the scope of this chapter, we’ll take a look at a few of the key objects and work through an example showing how the technology can be used when building SharePoint applications.

Note

Although OpenXML files are XML-based and can therefore be created and modified by using the various XML manipulation classes that are present in the Microsoft .NET Framework, Microsoft provides an OpenXML SDK to make it easier to deal with the complex document structure in managed code. It can be downloaded from Open XML SDK 2.0 for Microsoft Office.

Getting Started with OpenXML

OpenXML documents are in fact ZIP files. Each ZIP archive contains a collection of XML files and other elements. Relationships exist among the different XML files stored within the archive, and these relationships are defined in XML format in files with a .rels file extension.

Tip

To see how an OpenXML archive is structured, try renaming a .docx file to .zip and then open it with Windows Explorer.

Many of the files in the archive serve a particular function. For example, the fontTable.xml file contains details of the fonts that are in use within a document. Word documents store an XML file named document.xml; if you examine this file, you’ll see most of the user-generated content.

Dealing with each of the individual files and maintaining references between them by using standard XML processing components such as XmlDocument and XPathNavigator is certainly possible. However, it should be apparent from looking at the number of files involved that such an approach is no trivial undertaking. With that in mind, we’ll continue our discussion with a focus on the object model provided by the OpenXML SDK.

Within the OpenXML object model, a document is represented by a class derived from the OpenXmlPackage class. A document is actually a ZIP archive containing many files, and each of these files is defined as an object derived from OpenXmlPart. For example, the main document element in a Word file can be referenced by examining the MainDocumentPart property of a WordProcessingDocument object. WordProcessingDocument inherits from OpenXmlPackage and MainDocumentPart inherits from OpenXmlPart.

Each OpenXmlPart is made up of one or more OpenXmlElement objects, which in turn can contain OpenXmlAttribute objects. Naturally, these are abstract objects and specific implementations will often be used when processing a document. For example, when adding a Caption to a WordProcessingDocument object, an instance of the WordProcessing.Caption class will be used.

Demonstration Scenario

To give you an idea of how OpenXML and Word Automation Services can be used together to build useful line-of-business applications, consider the following demonstration scenario:

You’ve been engaged by AdventureWorks to design and build a document creation and collaboration tool. The tool will be used by the company’s sales department for producing sales proposals. Each proposal is made up of a number of different documents contributed by various users from different departments. The tool to be built should combine these documents into a single read-only document that can be sent to the customer for consideration.
The input documents will be saved in Microsoft Word OpenXML format. The output document should be in Adobe Acrobat (PDF) format.

Architecture

You need to consider the following points to create an architecture that fits this scenario:

  • Multiple documents will logically make up a single set. Bearing this in mind, we can use the Document Set functionality discussed in Chapter 6.

  • By using OpenXML, we can combine a number of different types of documents into a single OpenXML document.

  • Word Automation Services can be used to convert the output OpenXML document into an Adobe Acrobat–compatible file.

  • Because the process of combining documents is likely to be long-running, we have two possibilities: we could use the SPLongOperation object, which will present the user with the familiar spinning disc image while the process runs. Or we could use a custom job on the server, which will free up the user to perform other activities while the process completes. For the purposes of our demonstration, we’ll use the custom job approach since it illustrates functionality that is useful in manydevelopment situations.

Bearing these points in mind, we can create a custom content type that derives from the Document Set content type. We’ll then develop a custom web part control that will provide a user interface for combining the contents of our custom content set. To do the actual combination, we’ll create a custom job that uses OpenXML and Word Automation Services to put the finished document together and convert the output to PDF.

Creating a Custom Content Type

First we’ll create a new bank site and then provision the Document Set content type before we add a custom content type and define our user interface.

  1. From the Site Actions menu, create a new Blank Site named Chapter 10, as shown:

    Figure 2. Create a Blank Site

    Create a new blank site

  2. As described in detail in Chapter 6, enable the Document Sets feature. From the Site Actions menu, select Site Settings | Go To Top Level Site Settings | Site Collection Features. Activate the Document Sets feature.

  3. We’ll next add a custom content type for our Sales Proposal. Navigate back to the blank site that we created earlier (http://<ServerName>/Chapter10). From the Site Actions menu, select Site Settings. In the Galleries section, select Site Columns, as shown.

    Figure 3. Galleries Section

    Galleries section

  4. Create a new column named JobId of type Single Line Of Text. Save the column in the Custom Columns group.

  5. Create a new column named TemplateUrl of type Single Line Of Text. Save the column in the Custom Columns group.

  6. Navigate back to the Site Settings page, and then select Site Content Types from the Galleries section.

  7. Create a new content type named Sales Proposal. Set the Parent Content Type field to Document Set and save it within the Custom Content Types group, as shown:

    Figure 4. New Content Type

    New content type

  8. With our new content type created, we can add in the site columns that we created earlier. In the Columns section, click Add From Existing Site Columns. From the Custom Columns group, add the JobId and the TemplateUrl columns. Click OK to commit the changes.

Note

We’ve largely skipped over content types and site columns here. For a more in-depth look, see Chapter 13.

Customizing the DocumentSetProperties Web Part

Before we can customize the welcome page for our custom document set, we need to build a web part with the following additional features:

  • A Build Sales Proposal button that creates and starts the compilation job

  • A status indicator that shows the progress of the compilation job

  • A link to the compiled output file

Although we could create a separate web part that could be used in conjunction with the built-in DocumentPropertiesWebPart, it wouldn’t be overly useful as a stand-alone component elsewhere. Instead, we’ll create a web part that inherits from the DocumentPropertiesWebPart and adds our required additional functionality.

  1. Using Visual Studio 2010, create a new Empty SharePoint Project named SalesProposalApplication, as shown:

    Figure 5. New SharePoint Project

    New SharePoint project

  2. Set the debugging site to be the blank site that we created in the preceding section, and select the Deploy As Farm Solution option. Click Finish to create the project.

  3. After the project has been created, add a new Visual Web Part named SalesProposalPropertiesWebPart.

As you saw in Chapter 7, Visual Web Parts provide a design surface when we’re creating web parts. However, since we’re planning to override a built-in web part that already has its own rendering logic, we need to change some of the generated code for the Visual Web Part.

  1. Add a reference to the Microsoft.Office.DocumentManagement assembly, located in the %SPROOT%isapi folder, to the project.

  2. In the SalesProposalPropertiesWebPart.cs file, add the following code:

    using System.ComponentModel;
    using System.Web.UI;
    using Microsoft.Office.Server.WebControls;
    
    namespace SalesProposalApplication.SalesProposalPropertiesWebPart
    {
        [ToolboxItemAttribute(false)]
        public class SalesProposalPropertiesWebPart : DocumentSetPropertiesWebPart
        {
            // Visual Studio might automatically update
            //this path when you change the Visual Web Part project item.
            private const string _ascxPath = @"~/_CONTROLTEMPLATES/SalesProposalApplication/SalesProposalPropertiesWebPart/􀁊
    SalesProposalPropertiesWebPartUserControl.ascx";
            protected override void CreateChildControls()
            {
                Control control = this.Page.LoadControl(_ascxPath);
                this.Controls.Add(control);
                base.CreateChildControls();
            }
            protected override void RenderWebPart(HtmlTextWriter writer)
            {
                base.RenderWebPart(writer);
                this.Controls[0].RenderControl(writer);
            }
        }
    }
    

Tip

When overriding any built-in SharePoint classes, it can be challenging to work out exactly what you need to do to get the behavior that you expect. In the code snippet, to get our web part to render properly, we explicitly need to render our custom user control by overriding RenderWebPart method. Uncovering details such as this from the documentation is often impossible, and this is where Reflector Pro, discussed in Chapter 2, is invaluable.

With our custom user control properly hooked up to our web part, we can implement the rest of our custom logic via the user control.

  1. We’ll make use of Asynchronous JavaScript and XML (AJAX) so that the web part can periodically check on the status of the timer job and redraw the controls. Add an UpdatePanel control to the SalesProposalPropertiesWebPartUserControl.ascx file.

  2. We’ll use an AJAX Timer so that we can automatically refresh the status indicator on our control. Drag the Timer control from the toolbox onto the user control design surface. Name the TimerRefreshTimer and set its Enabled property to False.

  3. From the toolbox, add a Label control, a Hyperlink control, and a Button control to the SalesProposalPropertiesWebPartUserControl.ascx file. Within the UpdatePanel control markup, lay out and rename the controls as follows:

    <asp:UpdatePanel runat="server">
        <ContentTemplate>
            <div width="100%">
            <br />
            <asp:Label ID="StatusLabel" runat="server" Text=""></asp:Label>
            <br />
            <asp:HyperLink ID="OutputHyperlink" runat="server">
                Click here to download a compiled copy</asp:HyperLink>
                <br />
                <asp:Button ID="StartCompilation" OnClick="StartCompilation_Click"
                runat="server" Text="Start Compilation" />
            </div>
        </ContentTemplate>
        <Triggers>
            <asp:AsyncPostBackTrigger ControlID="RefreshTimer" EventName="Tick" />
        </Triggers>
    </asp:UpdatePanel>
    <asp:Timer runat="server" ID="RefreshTimer" Enabled="False">
    </asp:Timer>
    
  4. In the code-behind file (SalesProposalPropertiesWebPartUserControl.aspx.cs), add the following code:

    public partial class SalesProposalPropertiesWebPartUserControl : UserControl
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            RedrawUI();
        }
    
        private void RedrawUI()
        {
            if (SPContext.Current.ListItem != null)
            {
                string ConversionJobId =
                SPContext.Current.ListItem.GetFormattedValue("JobId");
                if (!string.IsNullOrEmpty(ConversionJobId))
                {
                    OutputHyperlink.href =
                    SPContext.Current.RootFolderUrl + "/temp.pdf";
                    SPJobHistory history = (from j in
                    SPFarm.Local.TimerService.JobHistoryEntries
                    where j.JobDefinitionId.ToString() == ConversionJobId
                    orderby j.StartTime descending
                    select j
    
                    if (history != null)
                    {
                        StatusLabel.Text = history.Status.ToString();
                        if (history.Status == SPRunningJobStatus.Succeeded)
                        {
                            OutputHyperlink.Visible = true;
                            StartCompilation.Enabled = true;
                            RefreshTimer.Enabled = false;
                        }
                        else if (history.Status == SPRunningJobStatus.Failed |
                            history.Status == SPRunningJobStatus.Aborted)
                        {
                            OutputHyperlink.Visible = false;
                            StartCompilation.Enabled = true;
                            RefreshTimer.Enabled = false;
                        }
                        else
                        {
                            OutputHyperlink.Visible = false;
                            StartCompilation.Enabled = false;
                            RefreshTimer.Enabled = true;
                        }
                    }
                    else
                    {
                        StatusLabel.Text = "Processing";
                        OutputHyperlink.Visible = false;
                        StartCompilation.Enabled = false;
                        RefreshTimer.Enabled = true;
                    }
                }
            }
            else
            {
                OutputHyperlink.href = "#";
                OutputHyperlink.Visible = true;
                StatusLabel.Text = "My Status";
                StartCompilation.Enabled = false;
            }
        }
    
        protected void StartCompilation_Click(object sender, EventArgs e)
        {
            throw new NotImplementedException();
        }
    }
    

Before our customized web part can be deployed, we need to make a few changes to the solution. The default packaging mechanisms that are set up in Visual Studio work well for creating web parts that are derived directly from System.Web.UI.WebControls.Webparts. Webpart. However, when creating a web part that’s derived from another base class, we’ll occasionally see an "Incompatible Web Part Markup" error message when we’re trying to use the deployed web part on a page. To resolve this error, we need to use an alternative packaging format.

  1. Rename the SalesProposalPropertiesWebPart.webpart file to SalesProposalPropertiesWebPart.dwp.

  2. Replace the contents with the following XML:

    <WebPart xmlns="https://schemas.microsoft.com/WebPart/v2">
        <Assembly>
            $SharePoint.Project.AssemblyFullName$
        </Assembly>
        <TypeName>
        SalesProposalApplication.SalesProposalPropertiesWebPart.SalesProposalPropertiesWebPart􀁊
        </TypeName>
        <Title>SalesProposalPropertiesWebPart</Title>
        <Description>Web Part Description</Description>
    </WebPart>
    
  3. So that the renamed file is installed properly, edit the Elements.xml file in the SalesProposalPropertiesWebPart folder as follows:

    <?xml version="1.0" encoding="utf-8"?>
        <Elements xmlns="https://schemas.microsoft.com/sharepoint/" >
        <Module Name="SalesProposalPropertiesWebPart" List="113" 
            Url="_catalogs/wp">
            <File Path="SalesProposalPropertiesWebPart\
                SalesProposalPropertiesWebPart.dwp"
                Url="SalesProposalPropertiesWebPart.dwp"
                Type="GhostableInLibrary" >
                <Property Name="Group" Value="Custom" />
            </File>
        </Module>
    </Elements>
    

Creating a Custom Job Definition

With our user interface largely complete, our next step is to define a custom job that will compile all documents in our document set and send the compiled output to Word Automation Services for conversion to PDF.

In Visual Studio, add a new class named DocumentCombinerJob.cs. Add the following code to the file:

public class DocumentCombinerJob : SPJobDefinition
{
     [Persisted]
    private Guid _siteId;
     [Persisted]
    private Guid _webId;
     [Persisted]
    private Guid _folderId;
     [Persisted]
    private Guid _proxyId;
    public DocumentCombinerJob() : base()
    {
    }
    public DocumentCombinerJob(SPListItem documentSet)
        : base("Combine Documents" + Guid.NewGuid().ToString(),
    SPFarm.Local.TimerService, null, SPJobLockType.None)
    {
        _siteId = documentSet.Web.Site.ID;
        _webId = documentSet.Web.ID;
        _folderId = documentSet.Folder.UniqueId;
        _proxyId = SPServiceContext.Current.GetDefaultProxy(
        typeof(WordServiceApplicationProxy)).Id;
        Title = "Combine Documents - " + documentSet.Folder.Url;
    }
    protected override bool HasAdditionalUpdateAccess()
    {
        return true;
    }
}

Developers familiar with SharePoint 2007 should notice a few interesting elements in this code snippet. First, check out the HasAdditionalUpdateAccess override. In previous versions of SharePoint, only farm administrators could create jobs. This greatly restricted their usefulness for offloading ad hoc tasks. With SharePoint 2010, where the HasAdditionalUpdateAccess method returns true, any user can create a job.

Also notice that when we’re creating a job, the job can be associated with either a service or an application pool. These associations are primarily for administrative purposes since most jobs run via the SPTimerV4 service. In our example, we’re associating our custom job with the TimerService.

The final thing to notice is that job definitions are serialized when a job is created. As a result, not all types of objects can be defined as properties. For example, the SPListItem isn’t serializable and therefore can’t be stored as a property. To get around this problem, we’re storing a number of identifiers that can be used to recover a reference to the appropriate SPListItem object when the job is deserialized.

Combine Documents Using OpenXML

Before we can make use of OpenXML, we need to add a reference to the OpenXML SDK binaries:

  1. Download and install the OpenXML SDK; then, in Visual Studio, add a reference to the DocumentFormat.OpenXML assembly.

  2. Add a reference to the WindowsBase assembly.

  3. To prevent any confusion between similarly named objects within the OpenXML SDK, add the following Using statement to the DocumentCombinerJob.cs file:

    using Word = DocumentFormat.OpenXml.Wordprocessing;
    
  4. In the DocumentCombinerJob.cs file, add the following code:

    public override void Execute(Guid targetInstanceId)
    {
        using (SPSite site = new SPSite(_siteId))
        {
            using (SPWeb web = site.OpenWeb(_webId))
            {
                SPFolder folder = web.GetFolder(_folderId);
                SPListItem documentSet = folder.Item;
                SPFile output = CombineDocuments(web, folder, documentSet);
                ConvertOutput(site, web, output);
            }
        }
    }
    
    private SPFile CombineDocuments(SPWeb web, SPFolder folder, SPListItem documentSet)
    {
        char[] splitter = { '/' };
        string[] folderName = folder.Name.Split(splitter);
        string templateUrl = documentSet.GetFormattedValue("TemplateUrl1");
        SPFile template = web.GetFile(templateUrl);
        byte[] byteArray = template.OpenBinary();
        using (MemoryStream mem = new MemoryStream())
        {
            mem.Write(byteArray, 0, (int)byteArray.Length);
            using (WordprocessingDocument myDoc = WordprocessingDocument.Open(mem, true))
            {
                MainDocumentPart mainPart = myDoc.MainDocumentPart;
                foreach (Word.SdtElement sdt in mainPart.Document.Descendants<Word.SdtElement>().ToList())
                {
                    Word.SdtAlias alias = sdt.Descendants<Word.SdtAlias>().FirstOrDefault();
                    if (alias != null)
                    {
                        string sdtTitle = alias.Val.Value;
                        if (sdtTitle == "MergePlaceholder")
                        {
                            foreach (SPFile docFile in folder.Files)
                            {
                                if (docFile.Name.EndsWith(".docx"))
                                {
                                    if (docFile.Name != "temp.docx")
                                    {
                                        InsertDocument(mainPart, sdt, docFile);
                                        Word.PageBreakBefore pb = 
                                            new Word.PageBreakBefore();
                                        sdt.Parent.InsertAfter(pb, sdt);
                                    }
                                }
                            }
                            sdt.Remove();
                        }
                    }
                }
            }
            SPFile temp = folder.Files.Add("temp.docx", mem, true);
            return temp;
        }
    }
    
    protected int id = 1;
    
    void InsertDocument(MainDocumentPart mainPart, Word.SdtElement sdt,
    SPFile filename)
    {
        string altChunkId = "AIFId" + id;
        id++;
        byte[] byteArray = filename.OpenBinary();
        AlternativeFormatImportPart chunk = 
            mainPart.AddAlternativeFormatImportPart(AlternativeFormatImportPartType.WordprocessingML, altChunkId);
        using (MemoryStream mem = new MemoryStream())
        {
            mem.Write(byteArray, 0, (int)byteArray.Length);
            mem.Seek(0, SeekOrigin.Begin);
            chunk.FeedData(mem);
        }
        Word.AltChunk altChunk = new Word.AltChunk();
        altChunk.Id = altChunkId;
        OpenXmlElement parent = sdt.Parent.Parent;
        parent.InsertAfter(altChunk, sdt.Parent);
    }
    
    private void ConvertOutput(SPSite site, SPWeb web, SPFile output)
    {
        throw new NotImplementedException();
    }
    

In this code snippet, the CombineDocuments method loads a Microsoft Word format template. The code then searches for all content controls within the document, and where the content control has a title of MergePlaceholder, the contents of all files with a .docx extension within the document set are merged into the template. The merge process makes use of the AlternativeFormatImportPart control to merge contents. This control inserts a binary copy of data into the template at a specific position. When the completed document is rendered in a client application, the merge is performed dynamically each time the document is opened.

Converting an OpenXML Document to an Alternative Format

Before we can make use of Word Automation Services in our application, we need to add a reference to the appropriate assembly:

  1. In Visual Studio, add a reference to Microsoft.Office.Word.Server.dll. At the time of writing, this appears in the Add Reference dialog as one of two components named Microsoft Office 2010 component; this problem may be resolved in the final release.

  2. Update the ConvertOutput method in DocumentTimerJob.cs as follows:

    private void ConvertOutput(SPSite site, SPWeb web, SPFile output)
    {
        ConversionJob convJob = new ConversionJob(_proxyId);
        convJob.Name = "Document Assembly";
        convJob.UserToken = web.CurrentUser.UserToken;
        convJob.Settings.UpdateFields = true;
        convJob.Settings.OutputFormat = SaveFormat.PDF;
        convJob.Settings.OutputSaveBehavior = SaveBehavior.AlwaysOverwrite;
        string webUrl = web.Url + "/";
        convJob.AddFile(webUrl + output.Url, webUrl + output.Url.Replace(
            ".docx", ".pdf"));
        convJob.Start();
        Guid jobId = convJob.JobId;
        ConversionJobStatus status = new ConversionJobStatus(_proxyId, jobId, null);
        while (status.Count != (status.Succeeded + status.Failed))
        {
            Thread.Sleep(3000);
            status.Refresh();
        }
        if (status.Failed == status.Count)
        {
            throw new InvalidOperationException();
        }
    

    With our custom job definition completed, we can change the implementation in our user interface to create a new instance of the job.

  3. In SalesProposalWebPartUserControl.ascx.cs, change the StartCompilation_Click method as follows:

    protected void StartCompilation_Click(object sender, EventArgs e)
    {
        SPListItem current = SPContext.Current.ListItem;
        current["JobId"] = string.Empty;
        current.Update();
        DocumentCombinerJob job = new DocumentCombinerJob(current);
        job.Update();
        job.RunNow();
        current["JobId"] = job.Id;
        current.Update();
        RedrawUI();
    }
    

We’ve now completed the code required to implement our demonstration scenario. Deploy the project by selecting Deploy SalesProposalApplication from the Build menu.

Customizing Document Set Welcome Page

As you saw in Chapter 6, each document set has a welcome page that contains a list of the documents within the set as well as information about the set itself. The web part that we created earlier will be used to customize the welcome page for our Sales Proposal document set so that whenever the content type is used, our custom control will be displayed instead of the built-in DocumentSetProperties control.

  1. Navigate to the Chapter 10 site that we created earlier. Select Site Settings from the Site Actions menu.

  2. Select Site Content Types from the Galleries section and then click the Sales Proposal content type.

  3. Select the Document Set settings link in the Settings section and then, in the Welcome Page section, click the Customize the Welcome Page link, as shown here:

    Figure 6. Customize the Welcome Page

    Customize the Welcome page

  4. From the Page tab in the ribbon, select Edit Page.

  5. Delete the Document Set Properties web part, and then click the Add a Web Part link in Zone 2 to show the web part selector.

  6. Add the SalesProposalPropertiesWebPart from the Custom category, as shown:

    Figure 7. Add a Web Part

    Add a Web Part

  7. Click Stop Editing to commit the changes to the welcome page.

Create a Document Library

Before we can begin creating sales proposals, we need to create a new document library that is bound to our Sales Proposal content type.

  1. From the Site Actions menu, select New Document Library. Name the new library Sales Proposals.

  2. After the new library has been created, select Library Settings from the Library tab of the ribbon.

  3. In the Document Library Settings page, select Advanced Settings, and then select the Allow Management Of Content Types option. Click OK to save the changes.

  4. From the Content Types section, click Add From Existing Site Content Types, and then select the Sales Proposal content type, as shown. Click OK to save the changes.

    Figure 8. Select Content Types

    Select content types

Create a Document Template

Our final step before we can see our document set functionality in action is to create a template for our compilation process. Since we need to add Content Controls to our template document, we can create the document using Visual Studio.

  1. To our SalesProposalApplication solution, add a new project of type Word 2010 Document, as shown. Name the project SampleTemplate.

    Figure 9. Add a New Project

    Add new project

  2. Drag a RichTextContentControl onto the SampleTemplate.docx file. Type the Title property as MergePlaceholder, as shown:

    Figure 10. MergePlaceholder

    MergePlaceholder

  3. Close the SampleTemplate.docx pane in Visual Studio, and then save the project.

  4. Right-click the project node and select Open Folder in Windows Explorer.

  5. Create a new document library named Document Templates and upload the SampleTemplate.docx file.

Tip

When you select the Upload Document | Upload Multiple Documents option from the Documents tab, the file can be uploaded by dragging and dropping it onto the dialog box.

We can now make use of our Sales Proposals document set to create a composite document.

  1. Navigate to the Sales Proposals document library, and then select New Document | Sales Proposal from the Documents tab of the ribbon.

  2. In the New Document Set: Sales Proposal dialog, enter the URL of the sample template in the TemplateUrl box.

  3. Upload a few Word documents to the document set, and then click the Start Compilation button. If all is well, after a few minutes a link will appear as shown, allowing us to download a PDF copy of the compiled sales proposal:

    Figure 11. Sales Proposal Dialog Box

    Sales proposal dialog box

Summary

This chapter demonstrated how we can create custom solutions by combining the capabilities of Microsoft Office 2010 and SharePoint Server 2010. By leveraging tools such as OpenXML and application services such as Word Automation Services, we can perform extensive processing on documents that have been created using tools with which users are already familiar.

For more information and further reading, see the Word Automation Services Developer Resource Center.

About the Author

Community Contributor  Charlie Holland, Freelance SharePoint Consultant, Author and Trainer, is a long term contributor to Microsoft Office. For more information, visit his Web site at http://www.chaholl.com.