Downloading InfoPath 2007 Forms in a Specific File Format from a SharePoint Form Library

This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals who are still using these technologies. This page may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist.

Summary: Learn how to use a technique that enables users to convert and download Microsoft Office InfoPath 2007 forms directly from Microsoft Office SharePoint Server 2007 form libraries as files in a format that is not XML. (25 printed pages)

S.Y.M Wong-A-Ton

November 2007

**Applies to:**Microsoft Office InfoPath 2007, Microsoft Office SharePoint Server 2007, Windows SharePoint Services 3.0

Contents

  • Overview of Downloading InfoPath Forms

  • Adding a Menu Item to SharePoint Form Libraries on a Site

  • Creating an ASP.NET Page to Download InfoPath Forms

  • Retrieving an InfoPath Form from a Form Library

  • Identifying InfoPath Forms

  • Converting InfoPath Forms to Another File Format

  • Returning a Converted Form as a File

  • Adding Error Handling to the ASP.NET Page

  • Enhancing the Solution

  • Sample Code in the ASP.NET Page

  • Conclusion

  • Additional Resources

Overview of Downloading InfoPath Forms

You can use the Download a Copy menu item on Microsoft Office SharePoint Server 2007 form libraries to download Microsoft Office InfoPath 2007 forms as XML files. But sometimes you might want to convert a form to another file format before downloading it so that you can either print it or open it in another application. This article presents a technique that you can use to enable users to download InfoPath forms directly from SharePoint 2007 form libraries as files in a format that is not XML. It assumes that you are familiar with InfoPath form template design and publishing, Windows SharePoint Services 3.0, and ASP.NET development.

Figures 1 and 2 show an example of how this technique works if a user wants to download an InfoPath form as a vCalendar file:

  1. A user goes to a SharePoint form library, selects an InfoPath form, and clicks Download As vCalendar from the drop-down menu.

    Figure 1. Downloading an InfoPath form as a vCalendar file

    Downloading a form as a vCalendar file

  2. The data in the InfoPath form is extracted, converted to the vCalendar format, and the user is prompted to Save the vCalendar file. Or, the user can choose to Open the file directly in an application such as Microsoft Office Outlook.

    Figure 2. File Download dialog box for the vCalendar file

    File Download dialog box for the vCalendar file

Note

A vCalendar file is a text file that contains information about an appointment or event. For more information about using vCalendar files in Microsoft Office Outlook, see How to use vCalendar in Outlook.

NoteNote

A vCalendar file is a text file that contains information about an appointment or event. For more information about using vCalendar files in Microsoft Office Outlook, see How to use vCalendar in Outlook.

Although the example that is used throughout this article downloads forms as vCalendar files, you can use this technique to convert and download InfoPath forms as Microsoft Office Word files, Microsoft Office Excel files, Portable Document Format (PDF) files, Comma-Separated Values (CSV) text files, or whichever file formats you choose to implement for your solution.

The technique consists of the following tasks:

  1. Adding a menu item to SharePoint form libraries on a site.

  2. Creating an ASP.NET page that is associated with the menu item.

  3. Retrieving the XML data of the InfoPath form that the user chooses to download.

  4. Identifying the type of form that is retrieved.

  5. Converting the form to the file format that is specified by the ASP.NET page.

  6. Returning the converted form as a file to the user.

  7. Adding error handling to the ASP.NET page.

Prerequisites

To complete the tasks outlined in this article, you must have the following applications installed on your computer:

  • Windows SharePoint Services 3.0 or Microsoft Office SharePoint Server 2007

  • Microsoft Visual Studio 2005

To implement the example in this article, you must also have Office InfoPath 2007 installed so that you can design and publish a form template to a SharePoint server. At a minimum, the form template must contain the fields listed in Table 1.

Table 1. Fields of a sample InfoPath form template that can capture event data

Field label

Control

Field name

Data type

Start date

Date Picker

startDate

Date (date)

End date

Date Picker

endDate

Date (date)

Start time

Text Box

startTime

Time (time)

End time

Text Box

endTime

Time (time)

Location

Text Box

location

Text (string)

Summary

Text Box

summary

Text (string)

Adding a Menu Item to SharePoint Form Libraries on a Site

You can use a Feature to add a custom menu item to the Edit Control Block menu of a form library. The Edit Control Block menu is the drop-down menu that appears when you click the drop-down arrow on an item in a SharePoint list or library (see Figure 1).

To learn more about Features and how to extend the user interface of a SharePoint site, navigate to the page, Welcome to the Windows SharePoint Services 3.0 SDK, and view the following articles:

To add a Download As vCalendar menu item to form libraries on a site

  1. Click Start, and then click Run.

  2. In the Run dialog box, type C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES, and then click OK.

  3. In the FEATURES folder, create a new subfolder named DownloadAsvCalECB.

  4. In the DownloadAsvCalECB folder, create a new XML file named Feature.xml and add the following example code.

    <?xml version="1.0" encoding="UTF-8"?>
    <Feature xmlns="http://schemas.microsoft.com/sharepoint/"
        Id="A9C84DB-7175-4837-B83A-3D7424758C2F"
        Scope="Web"
        Title="InfoPath Form Download As vCalendar Feature"
        Version="1.0.0.0"
        Description="Custom Feature - This Feature adds a link to the 
         EditControlBlock menu of Form Libaries to be able 
         to download InfoPath forms in the vCalendar format." >
        <ElementManifests>
            <ElementManifest Location="elements.xml" />
        </ElementManifests>
    </Feature>
    
    NoteNote

    You can generate a new globally unique identifier (GUID) for the Id attribute by using GuidGen.exe, which is available in Microsoft Visual Studio 2005.

  5. In the DownloadAsvCalECB folder, create a new XML file named elements.xml and add the following example code.

    <?xml version="1.0" encoding="UTF-8"?>
    <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
        <CustomAction Id="A9C84DB-7175-4837-B83A-3D7424758C2F"
            RegistrationType="List"
            RegistrationId="115"
            Location="EditControlBlock"
            Title="Download As vCalendar">
            <UrlAction 
              Url="/DownloadAs/vCalendar.aspx?ItemUrl={ItemUrl}
             &amp;ItemId={ItemId}&amp;ListId=
             {ListId}&amp;SiteUrl={SiteUrl}" />
        </CustomAction>
    </Elements>
    
  6. In the DownloadAsvCalECB folder, create a new file named install.bat and add the following example code.

    cd C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\BIN
    stsadm -o installfeature -filename DownloadAsvCalECB\Feature.xml –force
    stsadm -o activatefeature -name DownloadAsvCalECB -url http://ServerName/SiteName/
    iisreset
    
    NoteNote

    Modify the URL of the site to point to your server running Windows SharePoint Services 3.0 or Office SharePoint Server 2007.

  7. Double-click the install.bat file to install and activate the Feature.

The CustomAction element in the elements.xml file defines the bulk of what the Feature does. The attributes defined on the CustomAction element are described in Table 2.

Table 2. Attributes defined on the CustomAction element

Attribute

Description

RegistrationType

The object that the custom action acts upon; a List, which is a form library in this case.

RegistrationId

The identifier for the list type. A list type identifier of 115 represents an XML Form Library. This means that the custom action acts upon all form libraries within the scope that is specified in the Feature element, Web in this case.

Location

The location of the custom action. EditControlBlock specifies that the action defined in the CustomAction element will be included in the drop-down menu of items in the form library.

Title

The caption that the menu item will have in the Edit Control Block menu.

In addition, the UrlAction element in the CustomAction element specifies the URL of an ASP.NET page that the menu item opens when it is clicked. You can use URL tokens to pass information such as the site a user is on, the library a user is in, and the form a user selects, as query string parameters to the ASP.NET page. The technique in this article requires the four URL tokens described in Table 3 to be passed to the ASP.NET page.

Table 3. URL tokens used in the Url attribute

Token

Description

{ItemUrl}

The URL of the form being downloaded.

{ItemId}

The Identifier (ID) of the form in the form library.

{ListId}

The GUID of the form library from which the form is downloaded.

{SiteUrl}

The URL of the site from which the form is downloaded.

Creating an ASP.NET Page to Download InfoPath Forms

The menu item that you added in the previous section opens an ASP.NET page that will download InfoPath forms in a specific file format. You must create the ASP.NET page on the same location (site and folder) and with the same name that you specified in the Feature.

Follow these steps to add an ASP.NET page named vCalendar.aspx to a folder named DownloadAs under the root of the Windows SharePoint Services site that you specified in the Feature.

To add the ASP.NET page to the SharePoint site

  1. Open Microsoft Visual Studio 2005.

  2. On the File menu, select Open, and then click Web Site.

  3. In the Open Web Site dialog box, in the left toolbar, click File System, select the root folder of the site to which you deployed the Feature (for example C:\Inetpub\wwwroot\wss\VirtualDirectories\80), and then click Open.

  4. In Solution Explorer, select the root folder of the Web site.

  5. On the Web Site menu, click New Folder, and then rename the folder DownloadAs in Solution Explorer.

  6. In Solution Explorer, select the DownloadAs folder.

  7. On the Web Site menu, click Add New Item.

  8. In the Add New Item dialog box, select Web Form, rename the file vCalendar.aspx, select your language of choice from the Language drop-down list box, select the Place code in separate file check box, and then click Add.

Debugging the ASP.NET Page

Before you can debug the ASP.NET page in Visual Studio, you must configure the Web project to start with the URL to the ASP.NET page on the SharePoint site. For the newly added page to be recognized by the SharePoint server, you must also reset Internet Information Services (IIS) after you save and build the Web project.

To configure the Web project and reset IIS

  1. In Solution Explorer, select the root of the Web site.

  2. On the Website menu, click Start Options.

  3. In the Start action section, click Specific page, and then type the URL to the ASP.NET page, for example:

    DownloadAs/vCalendar.aspx

    Where DownloadAs is the name of the folder that you created in Visual Studio, and vCalendar.aspx is the name that you used for the ASP.NET page.

  4. In the Server section, click Use custom server, and then type the URL to your server, for example:

    http://ServerName/

    Where ServerName is the name of your server.

  5. Click OK.

  6. On the File menu, click Save All.

  7. On the Build menu, click Build Web Site (or Build Solution if you created a solution for your project), and resolve any build errors that appear.

  8. Click Start, and then click Run.

  9. In the Run dialog box, type iisreset, and then click OK.

    Now you can press F5 to start debugging the ASP.NET page. If you receive a message that states that debugging is not enabled, ensure that Modify the Web.config file to enable debugging is selected, and then click OK. This automatically adds the debug flag to the web.config file to enable debugging.

The following sections explain the code that you should add to the Page_Load event handler in the code-behind of the ASP.NET page.

NoteNote

For clarity, the code samples in the main body of this article do not contain error-handling code and using or Imports namespace directives. For information about how to write error-handling code for this solution, see Adding Error Handling to the ASP.NET Page. To view the entire listing of the code for the ASP.NET page, including error-handling code and using or Imports namespace directives, see Sample Code in the ASP.NET Page.

Retrieving an InfoPath Form from a Form Library

The ASP.NET page that you created in the previous section runs within the context of a SharePoint site, so you can use classes defined in the Microsoft.SharePoint namespace to retrieve a form from a form library.

The SiteUrl, ListId, ItemUrl, and ItemId query string parameters that are passed to the ASP.NET page through the {SiteUrl}, {ListId}, {ItemUrl}, and {ItemId} URL tokens that were defined in the Feature contain information about the server, site, library, name, and identifier of the form that is downloaded. The following example shows how to use this information together with the SPSite, SPWeb, SPList, and SPListItem classes to retrieve a form from a form library.

// Retrieve the query string parameters.
string siteUrl = Request["SiteUrl"];
string itemUrl = Request["ItemUrl"];
string itemIdAsString = Request["ItemId"];
string listIdAsString = Request["ListId"];


// Convert itemId and listId query string parameters.
int itemId = Int32.Parse(itemIdAsString);
Guid listId = new Guid(listIdAsString);

// Retrieve the form from the form library.
byte[] xmlFormData = null;
SPSite site = new SPSite(siteUrl);
SPWeb web = site.OpenWeb();
SPList lib = web.Lists[listId];
SPListItem listItem = lib.GetItemById(itemId);
SPFile file = listItem.File;
xmlFormData = file.OpenBinary();

// Load form data into an XPathDocument object.
XPathDocument ipForm = null; 

if (xmlFormData != null)
{
    using (MemoryStream ms = new MemoryStream(xmlFormData))
    {
        ipForm = new XPathDocument(ms);
        ms.Close();
    }
}
' Retrieve the query string parameters.
Dim siteUrl As String = Request("SiteUrl")
Dim itemUrl As String = Request("ItemUrl")
Dim itemIdAsString As String = Request("ItemId")
Dim listIdAsString As String = Request("ListId")

' Convert itemId and listId query string parameters.
Dim itemId As Integer = Int32.Parse(itemIdAsString)
Dim listId As Guid = New Guid(listIdAsString)

' Retrieve the form from the form library.
Dim xmlFormData As Byte() = Nothing
Dim site As SPSite = New SPSite(siteUrl)
Dim web As SPWeb = site.OpenWeb()
Dim library As SPList = web.Lists(listId)
Dim listItem As SPListItem = library.GetItemById(itemId)
Dim file As SPFile = listItem.File
xmlFormData = file.OpenBinary()

' Load form data into an XPathDocument object.
Dim ipForm As XPathDocument = Nothing

If xmlFormData IsNot Nothing Then
    Using ms As MemoryStream = New MemoryStream(xmlFormData)
        ipForm = New XPathDocument(ms)
        ms.Close()
    End Using
End If

Identifying InfoPath Forms

Because the ASP.NET page may receive requests to download InfoPath forms from any form library that is within the scope that was defined in the Feature, and because one form library may contain several different types of InfoPath forms if multiple content types are used, the ASP.NET page must be able to uniquely identify each InfoPath form that it retrieves, so that it can apply the appropriate conversion code to the form.

InfoPath forms are XML files that have the following basic structure as shown in Figure 3:

  • An XML declaration.

  • An XML processing instruction, mso-infoPathSolution, which contains information about the form template that is used to create or open the form.

  • An XML processing instruction, mso-application, which indicates that the XML file should be opened with InfoPath.

  • XML elements that represent the contents of the main data source of the form.

Figure 3. Basic structure of an InfoPath form with form identity attributes shown in red

Structure of form showing form identity attributes

By default, InfoPath form templates carry two identities that associate forms with a form template:

  • Form ID

  • Access Path

For more information about these identities, see "Understanding form identity" in Security Levels, E-Mail Deployment, and Remote Form Templates.

Whenever you fill out and save a form, the values of the Form ID and Access Path identities of the form template are stored in attributes of the mso-infoPathSolution processing instruction of the form: The Form ID is stored in the name attribute, and the Access Path is stored in the href attribute. While you could use either one to identify the form template of a form, the Access Path may not always be present in form templates. Therefore, it is recommended that you use the name attribute or check whether the href attribute is present within a form before relying on it to identify the form template that the form is based on.

If you expect to add or remove form template fields as you upgrade form templates, you should also consider using the solutionVersion attribute to help identify forms. The solutionVersion attribute of the mso-infoPathSolution processing instruction contains the version number of the published form template and may differ in forms that are based on the same form template as you upgrade and republish the form template.

In the following example, urn:schemas-microsoft-com:office:infopath:MyTemplate:-myXSD-2007-09-30T00-00-00 is the Form ID and http://ServerName/FormServerTemplates/MyTemplate.xsn is the Access Path in the name and href attributes of the mso-infoPathSolution processing instruction of a form.

<?mso-infoPathSolution 
    name="urn:schemas-microsoft-com:office:infopath:MyTemplate:-myXSD-2007-09-30T00-00-00" 
    solutionVersion="1.0.0.8" 
    productVersion="12.0.0.0" 
    PIVersion="1.0.0.0"
    href="http://ServerName/FormServerTemplates/MyTemplate.xsn"?>

To identify a form that the ASP.NET page retrieves, you must extract the name, href, and solutionVersion attributes of the mso-infoPathSolution processing instruction from the XML data of the form. The following example shows how to do this.

// Create XPathNavigator object to navigate XML of InfoPath form.
XPathNavigator ipFormNav = ipForm.CreateNavigator(); 

// Declare variables to store values of attributes to identify form.
string formID = String.Empty;
string href = String.Empty;
string solutionVersion = String.Empty;

// Retrieve the mso-infoPathSolution processing instruction.
XPathNavigator pi = ipFormNav.SelectSingleNode("/processing-instruction(\"mso-infoPathSolution\")");
if (pi != null)
{
    string piContents = pi.Value;

    if (!String.IsNullOrEmpty(piContents))
    {
        string[] piAttributes = piContents.Split(' ');

        // Loop through the attributes of the processing instruction
        // to look for the name, href, and solutionVersion attributes.
        if (piAttributes != null)
        {
            int attrArrayLength = piAttributes.Length;

            for (int i = 0; i < attrArrayLength; i++)
            {
                string[] piAttributeNameValuePair = 
                    piAttributes[i].Split('=');
                if (piAttributeNameValuePair != 
                    null && piAttributeNameValuePair.Length == 2)
                {
                    string piAttrName = piAttributeNameValuePair[0];
                    string piAttrValue = piAttributeNameValuePair[1];

                    // Get the values for the name, href, and
                    // solutionVersion attributes.
                    switch (piAttrName)
                    {
                        case "name":
                            formID = piAttrValue.Replace("\"", "");
                            break;
                        case "href":
                            href = piAttrValue.Replace("\"", "");
                            break;
                        case "solutionVersion":
                            solutionVersion = 
                                piAttrValue.Replace("\"", "");
                            break;
                        default:
                            break;
                    }
                }
            }
        }
    }
}
' Create XPathNavigator object to navigate XML of InfoPath form.
Dim ipFormNav As XPathNavigator = ipForm.CreateNavigator()

' Declare variables to store values of attributes to identify form.
Dim formID As String = String.Empty
Dim href As String = String.Empty
Dim solutionVersion As String = String.Empty

' Retrieve the mso-infoPathSolution processing instruction.
Dim pi As XPathNavigator = ipFormNav.SelectSingleNode("/processing-instruction(""mso-infoPathSolution"")")
If pi IsNot Nothing Then

    Dim piContents As String = pi.Value

    If Not String.IsNullOrEmpty(piContents) Then

        Dim piAttributes As String() = piContents.Split(" ")

        ' Loop through the attributes of the 
        ' processing instruction to look for the name,
        ' href, and solutionVersion attributes.
        If piAttributes IsNot Nothing Then

            Dim attrArrayLength As Integer = piAttributes.Length

            Dim i As Integer
            For i = 0 To attrArrayLength - 1

                Dim piAttributeNameValuePair As String() = piAttributes(i).Split("=")
                If piAttributeNameValuePair IsNot Nothing And piAttributeNameValuePair.Length = 2 Then

                    Dim piAttrName As String = piAttributeNameValuePair(0)
                    Dim piAttrValue As String = piAttributeNameValuePair(1)

                    ' Get the values for the name, href, 
                    ' and solutionVersion attributes.
                    Select Case piAttrName
                        Case "name"
                            formID = piAttrValue.Replace("""", "")
                        Case "href"
                            href = piAttrValue.Replace("""", "")
                        Case "solutionVersion"
                            solutionVersion = piAttrValue.Replace("""", "")
                        Case Else
                            ' Do nothing.
                    End Select

                End If
            Next
        End If
    End If
End If

Converting InfoPath Forms to Another File Format

To convert an InfoPath form to another file format, you must:

  1. Extract the data from the form.

  2. Use the extracted data to construct a file in a new format.

To extract data from an InfoPath form, you can do one of the following:

  • Use XML Path Language (XPath) to retrieve data from the form’s XML data.

  • Use an XSLT transformation to extract and convert data from the form’s XML data.

  • Generate a Microsoft .NET Framework class from the form template’s XML Schema Definition (XSD) file, deserialize the form’s XML data to an object, and use this object to extract data.

After the data is extracted, you can use it to construct a file in a new format. Formats that conform to open standards, such as XML, HTML, Open XML, CSV, and plain text, are fairly straightforward to convert to. For proprietary formats such as RTF (Rich Text Format), PDF, and Microsoft Office Word, it is best to use a suitable (third-party) library to construct files in these formats.

To convert an InfoPath form to the vCalendar format, you must construct a text file with a structure similar to the following; the data that may come from the InfoPath form is shown here in italic text.

BEGIN:VCALENDAR

VERSION:1.0

BEGIN:VEVENT

DTSTART:20070930T080000Z

DTEND:20070930T100000Z

LOCATION:The location name goes here.

DESCRIPTION;ENCODING=QUOTED-PRINTABLE:=

This description will go in the body of the appointment.=0D=0A

SUMMARY:This summary will go in the Subject field.

END:VEVENT

END:VCALENDAR

In the following example, the name attribute of the mso-InfoPathSolution processing instruction is used to identify the form templates that forms are based on. For each form template, you must manually retrieve the value of the name attribute from a form that is based on that form template and use this value in a case statement. To manually retrieve the value of the name attribute, follow these steps.

To retrieve the value of the name attribute

  1. Go to a form library that contains a form that you want to convert.

  2. Click the drop-down arrow on the form, select Send To, and then click Download a Copy.

  3. Save the form locally.

  4. Locate the saved form and open it in a text editor such as Notepad.

  5. Search for the mso-infoPathSolution processing instruction.

  6. Copy the value of the name attribute for use in your code.

In each case statement, you must write code to convert forms that are based on the same form template. After a form is converted, a byte array of the converted form is stored in the convertedFormData variable.

// Declare variable to hold converted form data.
byte[] convertedFormData = null;

// Convert forms to the vCalendar format.
switch (formID)
{
    case "urn:schemas-microsoft-com:office:infopath:DownloadAs:-myXSD-2007-09-27T08-19-13":
        // Create XmlNamespaceManager object to declare
        // namespaces used in form template.
        XmlNamespaceManager nsManager =
            new XmlNamespaceManager(
            new NameTable());
        nsManager.AddNamespace("my",
          "http://schemas.microsoft.com/office/infopath/2003/myXSD/2007-09-27T08:19:13");

        // Extract the data from the form using XPath.
        string startDate = getNodeValue(
            ipFormNav, nsManager, "/my:myFields/my:startDate");
        string endDate = getNodeValue(
            ipFormNav, nsManager, "/my:myFields/my:endDate");
        string startTime = getNodeValue(
            ipFormNav, nsManager, "/my:myFields/my:startTime");
        string endTime = getNodeValue(
            ipFormNav, nsManager, "/my:myFields/my:endTime");
        string location = getNodeValue(
            ipFormNav, nsManager, "/my:myFields/my:location");
        string summary = getNodeValue(
            ipFormNav, nsManager, "/my:myFields/my:summary");

        // vcs uses UTC, so we need to adjust the time.
        TimeZone tz = TimeZone.CurrentTimeZone;
        int timeZoneOffset = 0;
        if (tz != null)
        {
            timeZoneOffset = tz.GetUtcOffset(DateTime.Now).Hours;
        }
        DateTime startDateTime =
            new DateTime(Int32.Parse(startDate.Substring(0, 4)),
            Int32.Parse(startDate.Substring(5, 2)),
            Int32.Parse(startDate.Substring(8, 2)),
            Int32.Parse(startTime.Substring(0, 2)),
            Int32.Parse(startTime.Substring(3, 2)),
            0);
        DateTime startDateTimeOffset =
            startDateTime.AddHours(-timeZoneOffset);
        DateTime endDateTime =
            new DateTime(Int32.Parse(endDate.Substring(0, 4)),
            Int32.Parse(endDate.Substring(5, 2)),
            Int32.Parse(endDate.Substring(8, 2)),
            Int32.Parse(endTime.Substring(0, 2)),
            Int32.Parse(endTime.Substring(3, 2)),
            0);
        DateTime endDateTimeOffset = endDateTime.AddHours(
            -timeZoneOffset);

        // Construct the vCalendar file.
        StringBuilder sb = new StringBuilder();
        sb.Append("BEGIN:VCALENDAR\n");
        sb.Append("VERSION:1.0\n");
        sb.Append("BEGIN:VEVENT\n");
        sb.AppendFormat("DTSTART:{0}{1}{2}T{3}{4}00Z\n",
            startDateTimeOffset.Year.ToString(),
            startDateTimeOffset.Month.ToString().PadLeft(2, '0'),
            startDateTimeOffset.Day.ToString().PadLeft(2, '0'),
            startDateTimeOffset.Hour.ToString().PadLeft(2, '0'),
            startDateTimeOffset.Minute.ToString().PadLeft(2, '0'));
         sb.AppendFormat("DTEND:{0}{1}{2}T{3}{4}00Z\n",
            endDateTimeOffset.Year.ToString(),
            endDateTimeOffset.Month.ToString().PadLeft(2, '0'),
            endDateTimeOffset.Day.ToString().PadLeft(2, '0'),
            endDateTimeOffset.Hour.ToString().PadLeft(2, '0'),
            endDateTimeOffset.Minute.ToString().PadLeft(2, '0'));
         sb.AppendFormat("LOCATION:{0}\n", location);
         sb.Append("DESCRIPTION;ENCODING=QUOTED-PRINTABLE:Appointment generated by InfoPath\n");
         sb.AppendFormat("SUMMARY:{0}\n", summary);
         sb.Append("END:VEVENT\n");
         sb.Append("END:VCALENDAR\n");

         // Convert the vCalendar file to a byte array.
         convertedFormData = Encoding.UTF8.GetBytes(sb.ToString());
        break;

    // Convert forms that are based on other form templates
    // to the vCalendar format.
    //case "urn:...":
    //  ...
    //  break;
    //
    //case "urn:...":
    //  ...
    //  break;

    default:
        break;
}
' Declare variable to hold converted form data.
Dim convertedFormData As Byte() = Nothing

' Convert forms to the vCalendar format.
Select Case formID
    Case "urn:schemas-microsoft-com:office:infopath:DownloadAs:-myXSD-2007-09-27T08-19-13"
        ' Create XmlNamespaceManager object to declare
        ' namespaces used in form template.
        Dim nsManager As XmlNamespaceManager = New XmlNamespaceManager(New NameTable())
        nsManager.AddNamespace("my", _
"http://schemas.microsoft.com/office/infopath/2003/myXSD/2007-09-27T08:19:13")

        ' Extract the data from the form using XPath.
        Dim startDate As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:startDate")
        Dim endDate As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:endDate")
        Dim startTime As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:startTime")
        Dim endTime As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:endTime")
        Dim location As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:location")
        Dim summary As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:summary")

        ' vcs uses UTC, so we need to adjust the time.
        Dim tz As TimeZone = TimeZone.CurrentTimeZone
        Dim timeZoneOffset As Integer = 0
        If tz IsNot Nothing Then 
            timeZoneOffset = tz.GetUtcOffset(DateTime.Now).Hours
        End If
        Dim startDateTime As DateTime = _
            New DateTime(Int32.Parse(startDate.Substring(0, 4)), _
            Int32.Parse(startDate.Substring(5, 2)), _
            Int32.Parse(startDate.Substring(8, 2)), _
            Int32.Parse(startTime.Substring(0, 2)), _
            Int32.Parse(startTime.Substring(3, 2)), _
            0)
        Dim startDateTimeOffset As DateTime = _
            startDateTime.AddHours(-timeZoneOffset)
        Dim endDateTime As DateTime = _
            New DateTime(Int32.Parse(endDate.Substring(0, 4)), _
            Int32.Parse(endDate.Substring(5, 2)), _
            Int32.Parse(endDate.Substring(8, 2)), _
            Int32.Parse(endTime.Substring(0, 2)), _
            Int32.Parse(endTime.Substring(3, 2)), _
            0)
        Dim endDateTimeOffset As DateTime = endDateTime.AddHours(-timeZoneOffset)

        ' Construct the vCalendar file.
        Dim sb As StringBuilder = New StringBuilder()
        sb.Append("BEGIN:VCALENDAR")
        sb.Append(vbCrLf)
        sb.Append("VERSION:1.0")
        sb.Append(vbCrLf)
        sb.Append("BEGIN:VEVENT")
        sb.Append(vbCrLf)
        sb.AppendFormat("DTSTART:{0}{1}{2}T{3}{4}00Z", _
            startDateTimeOffset.Year.ToString(), _
            startDateTimeOffset.Month.ToString().PadLeft(2, "0"), _
            startDateTimeOffset.Day.ToString().PadLeft(2, "0"), _
            startDateTimeOffset.Hour.ToString().PadLeft(2, "0"), _
            startDateTimeOffset.Minute.ToString().PadLeft(2, "0"))
        sb.Append(vbCrLf)
        sb.AppendFormat("DTEND:{0}{1}{2}T{3}{4}00Z", _
            endDateTimeOffset.Year.ToString(), _
            endDateTimeOffset.Month.ToString().PadLeft(2, "0"), _
            endDateTimeOffset.Day.ToString().PadLeft(2, "0"), _
            endDateTimeOffset.Hour.ToString().PadLeft(2, "0"), _
            endDateTimeOffset.Minute.ToString().PadLeft(2, "0"))
        sb.Append(vbCrLf)
        sb.AppendFormat("LOCATION:{0}", location)
        sb.Append(vbCrLf)
        sb.Append("DESCRIPTION;ENCODING=QUOTED-PRINTABLE:Appointment generated by InfoPath")
        sb.Append(vbCrLf)
        sb.AppendFormat("SUMMARY:{0}", summary)
        sb.Append(vbCrLf)
        sb.Append("END:VEVENT")
        sb.Append(vbCrLf)
        sb.Append("END:VCALENDAR")
        sb.Append(vbCrLf)

        ' Convert the vCalendar file to a byte array.
        convertedFormData = Encoding.UTF8.GetBytes(sb.ToString())

    ' Convert forms that are based on other form
    ' templates to the vCalendar format.
    'Case "urn:..."
    '  ...
    '
    'Case "urn:..."
    ' ...

    Case Else

End Select
NoteNote

You must replace the formID value in the case statement and the my namespace value with the appropriate values for your own form template.

In the previous code, getNodeValue is a private function that you can use to retrieve the value of a given XML node. It is defined as shown in the following code.

private string getNodeValue(
    XPathNavigator nav, XmlNamespaceManager nsManager, string xpath)
{
    string nodeValue = string.Empty;
    if (nav != null && nsManager != null &&
        !String.IsNullOrEmpty(xpath))
    {
        XPathNavigator nodeNav = 
            nav.SelectSingleNode(xpath, nsManager);
        if (nodeNav != null)
        {
            nodeValue = nodeNav.Value;
        }
    }
    return nodeValue;
}
Private Function getNodeValue( _
    ByVal nav As XPathNavigator, _
    ByVal nsManager As XmlNamespaceManager, _
    ByVal xpath As String) As String
    
    Dim nodeValue As String = String.Empty

    If nav IsNot Nothing And nsManager IsNot Nothing And _
        Not String.IsNullOrEmpty(xpath) Then

        Dim nodeNav As XPathNavigator = _
            nav.SelectSingleNode(xpath, nsManager)
        If Not nodeNav Is Nothing Then
            nodeValue = nodeNav.Value
        End If
    End If

    Return nodeValue

End Function

Returning a Converted Form as a File

After you convert the form to a byte array, you can use standard ASP.NET practices to return the converted form as a file to the user. The BinaryWrite method of the Response object allows you to write the byte array of the converted form to the HTTP output stream, and the ContentType property of the Response object specifies the HTTP content type to return. You can set the ContentType property to text/x-vCalendar to return the HTTP content in the vCalendar format. Finally, you can add a content-disposition header to the Response object to raise a File Download dialog box and allow the user to download and either Open or Save the converted form as a file.

The following example shows how to return the converted InfoPath form as a vCalendar file to the user. The convertedFormData variable contains the byte array for the vCalendar file and the newFileName variable specifies the name of the file that will be downloaded, which consists of the name of the form and a .vcs file name extension.

// Retrieve the form name and construct a new file name.
string newFileName = itemUrl.Substring(
    itemUrl.LastIndexOf('/') + 1).Replace(".xml", ".vcs");

// Return the converted form as a file to the user.
Response.Clear();
Response.ClearHeaders();
Response.ClearContent();
Response.Buffer = true;
Response.ContentType = "text/x-vCalendar";
Response.AddHeader("content-disposition",
                   "attachment; filename=" + newFileName);
Response.BinaryWrite(convertedFormData);
Response.End();
' Retrieve the form name and construct a new file name.
Dim newFileName As String = itemUrl.Substring( _
    itemUrl.LastIndexOf("/") + 1).Replace(".xml", ".vcs")

' Return the converted form as a file to the user.
Response.Clear()
Response.ClearHeaders()
Response.ClearContent()
Response.Buffer = True
Response.ContentType = "text/x-vCalendar"
Response.AddHeader("content-disposition", _
                   "attachment; filename=" & newFileName)
Response.BinaryWrite(convertedFormData)
Response.End()

Adding Error Handling to the ASP.NET Page

You can implement a mechanism to catch errors that are raised while converting and downloading forms, and then use either a custom error page or the built-in error functionality of Windows SharePoint Services to redirect users to a user-friendly error page. Figure 4 shows an example of an error page that could be displayed when a user chooses to download a form for which there is no conversion code available.

Figure 4. Page that displays the error message when the conversion code for a form is missing

Error message when missing conversion code

This section discusses the basics of an approach to implementing error handling for the ASP.NET page. You can find the full implementation for the error handling throughout the code in the Sample Code in the ASP.NET Page section.

Adding an Error Handler to Catch All Exceptions

You can add one main try-catch block to the Page_Load event handler of the ASP.NET page to handle all exceptions that may occur while downloading forms. You can use the TransferToErrorPage method of the Microsoft.SharePoint.Utilities.SPUtility class within the last catch block of the main try-catch block to display an error page with a custom error message. The following code shows the general outline of the main try-catch block in the Page_Load event handler.

protected void Page_Load(object sender, EventArgs e)
{
    try
    {
        // Write your conversion and download code here.
    }
    catch (ThreadAbortException)
    {
        // Do nothing; the Response object is 'unwinding'.
    }
    catch (Exception ex)
    {
        SPUtility.TransferToErrorPage(ex.Message);
    }
}
Protected Sub Page_Load(ByVal sender As Object, _
                        ByVal e As System.EventArgs) Handles Me.Load   
        
    Try
        ' Write your conversion and download code here.
    Catch ex As ThreadAbortException
        ' Do nothing; the Response object is 'unwinding'
    Catch ex As Exception
        SPUtility.TransferToErrorPage(ex.Message)
    End Try

End Sub

The main try-catch block consists of two catch blocks:

The Response object throws a ThreadAbortException when it stops processing. Because you use the Response object and end its processing to return a file to the user, you must catch the ThreadAbortException that it throws. Failure to do this will result in the ThreadAbortException being caught by the last catch block, which catches all exceptions that were not caught by previous catch blocks. The side effect of allowing a ThreadAbortException to be caught by the Exception catch block is that the error page, which the TransferToErrorPage method transfers to, will be downloaded instead of the requested file for the converted form.

Creating and Storing Error Messages

For ease of maintenance, modification, and data sharing when you are using multiple ASP.NET pages to download forms in different file formats, you can add the error messages that you want to display on the error page to a resource file instead of hard-coding them within the ASP.NET page.

To add a resource file to your project

  1. Open your Web project.

  2. In Solution Explorer, select the App_GlobalResources folder.

  3. On the Website menu, click Add New Item.

  4. In the Add New Item dialog box, select Resource File, rename the file to CustomErrorMessages.resx, and then click Add.

  5. Add error messages to the resource file and give each error message a Name and Value.

    The error messages that are used in this article are listed in Table 4.

Table 4. Strings defined in the CustomErrorMessages.resx resource file

Name

Value

ConversionCodeMissing

The InfoPath form cannot be downloaded in the requested file format, because it cannot be converted.

ConvertedFormDataMissing

The converted data for the InfoPath form is missing.

GetFileFailed

Failed to retrieve file.

GetListFailed

Failed to retrieve library with ID '{0}'.

GetListItemFailed

Failed to retrieve item with ID '{0}'.

FormConversionError

The following error occurred while trying to convert the InfoPath form: '{0}'.

FormIDMissing

The identifier of the InfoPath form is missing.

FormXMLMissing

The XML data for the InfoPath form is missing.

FormXPathNavigatorMissing

Could not create XPathNavigator object for InfoPath form.

LoadFormDataError

The following error occurred while trying to load the InfoPath form: '{0}'.

OpenSiteFailed

Failed to connect to site with URL '{0}'.

OpenWebFailed

Failed to open web belonging to site with URL '{0}'.

QueryStringParameterMissing

The following query string parameter is missing: '{0}'.

RetrieveFormError

The following error occurred while trying to retrieve the InfoPath form from the form library: '{0}'.

For more information about resource files, see ASP.NET Web Page Resources Overview.

Retrieving and Displaying Error Messages

The following example shows how you can retrieve the value of the ConversionCodeMissing error message that is defined in the CustomErrorMessages.resx resource file.

string errorMessage = 
    Resources.CustomErrorMessages.ConversionCodeMissing;
Dim errorMessage As String = _ 
    Resources.CustomErrorMessages.ConversionCodeMissing

The string variable errorMessage contains the following text: "The InfoPath form cannot be downloaded in the requested file format, because it cannot be converted." This is the value that was given to the ConversionCodeMissing error message in the CustomErrorMessages.resx resource file.

Because a main try-catch block is used to catch and handle all exceptions, you can throw an exception from any location within your code where you think it is appropriate to report an error back to the user. For example, the query string parameters are absolutely required to be able to convert and download a form, so it makes sense to throw an exception if any of these parameters is null or Nothing as shown in the following example.

string siteUrl = Request["SiteUrl"];

if (siteUrl == null)
    throw new ApplicationException(
        String.Format(
        CustomErrorMessages.QueryStringParameterMissing, "SiteUrl"));
Dim siteUrl As String = Request("SiteUrl")

If siteUrl Is Nothing Then
    Throw New ApplicationException( _
        String.Format( _
        CustomErrorMessages.QueryStringParameterMissing, "SiteUrl"))
End If

To report errors that are specific to certain sections of your code, for example, when the form is being loaded into an XPathDocument, you can add separate try-catch blocks within the main try-catch block. This helps determine the exact location and stage in the download process at which a particular error occurred. In the following example, using a MemoryStream object could throw an OutOfMemoryException. If you encapsulate the code in a try-catch block, the exception is caught, and then rethrown for the main try-catch block to catch and transfer processing to the error page that would display an error message, such as "The following error occurred while trying to load the InfoPath form: 'Insufficient memory to continue the execution of the program.'"

// Load form data into an XPathDocument object.
XPathDocument ipForm = null;
try
{
    if (xmlFormData != null)
    {
        using (MemoryStream ms = new MemoryStream(xmlFormData))
        {
            ipForm = new XPathDocument(ms);
            ms.Close();
        }
    }
}
catch (Exception ex)
{
    throw new ApplicationException(
        String.Format(CustomErrorMessages.LoadFormDataError,
            ex.Message));
}
' Load form data into an XPathDocument object.
Dim ipForm As XPathDocument = Nothing
Try
    If xmlFormData IsNot Nothing Then
        Using ms As MemoryStream = New MemoryStream(xmlFormData)
            ipForm = New XPathDocument(ms)
            ms.Close()
        End Using
    End If
Catch ex as Exception
    Throw New ApplicationException( _
        String.Format(CustomErrorMessages.LoadFormDataError, _
            ex.Message))
End Try

Handling Missing Conversion Code for Form Templates

You could publish a form template to a form library and not add any code to the ASP.NET page to convert forms that are based on that form template. If the form template of a form cannot be identified by one of the case statements in the switch or Select Case block, code in the default or Case Else statement executes. The following example shows how you can add code to the default or Case Else statement to throw an exception that alerts the user that the form cannot be downloaded because there is no conversion code available for it. Also see Figure 4 for the error page that displays the message.

switch (formID)
{
    case "...":
        try
        {
            // Write code to convert the form.
        }
        catch (Exception ex)
        {
            // Handle exceptions thrown while converting the form.
        }
        break;

    default:
        // There is no conversion code available for the form.
        throw new ApplicationException(
            CustomErrorMessages.ConversionCodeMissing);
}
Select Case formID
    Case "..."
        Try
            ' Write code to convert the form.
        Catch ex As Exception
            ' Handle exceptions thrown while converting the form.
        End Try

    Case Else
        ' There is no conversion code available for the form.
        Throw New ApplicationException( _
            CustomErrorMessages.ConversionCodeMissing)
End Select
Important noteImportant

Always assess how much and what kind of information you should display to users. A security best practice is to tell users only what is necessary for them to resolve issues, and log more detailed information about errors to an event log for diagnostic purposes and problem resolution. Apply the same prudence when you implement error handling for your solution.

Enhancing the Solution

The technique described in this article does not represent a complete solution. Possible ways for enhancing it are:

  • Downloading multiple file formats

    Each ASP.NET page that you associate with a menu item downloads InfoPath forms in only one predefined file format for that ASP.NET page. If you want to have one menu item that lets a user choose from multiple file formats to download an InfoPath form in, you could redirect the ASP.NET page to an intermediate page that presents the user with a list of available file formats, let the user choose a file format from the list, and then continue converting and downloading the form based on the chosen file format.

  • Creating a "plug-in" model

    Currently, you must modify the ASP.NET page whenever you publish a new form template and want to add conversion code for the form template. The Provider pattern enables you to construct a system where you can publish a new form template, create and deploy a new conversion component for the form template, and associate the two through a setting in a Web application configuration file without having to modify the ASP.NET page.

Sample Code in the ASP.NET Page

This section contains the entire listing of the code for the ASP.NET page. It includes error-handling code and using or Imports namespace directives.

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml;
using System.Xml.XPath;
using System.IO;
using System.Text;
using System.Threading;
using Resources;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;

public partial class DownloadAs_vCalendar : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        try
        {
            // Retrieve the query string parameters.
            string siteUrl = Request["SiteUrl"];
            string itemUrl = Request["ItemUrl"];
            string itemIdAsString = Request["ItemId"];
            string listIdAsString = Request["ListId"];

            // Validate query string parameters.
            if (siteUrl == null)
                throw new ApplicationException(String.Format(
                    CustomErrorMessages.QueryStringParameterMissing,
                    "SiteUrl"));
            if (itemUrl == null)
                throw new ApplicationException(String.Format(
                    CustomErrorMessages.QueryStringParameterMissing,
                    "ItemUrl"));
            if (itemIdAsString == null)
                throw new ApplicationException(String.Format(
                    CustomErrorMessages.QueryStringParameterMissing, 
                    "ItemId"));
            if (listIdAsString == null)
                throw new ApplicationException(String.Format(
                    CustomErrorMessages.QueryStringParameterMissing, 
                    "ListId"));
            
            // Convert itemId and listId query string parameters.
            int itemId = Int32.Parse(itemIdAsString);
            Guid listId = new Guid(listIdAsString);

            // Retrieve the form from the form library.
            byte[] xmlFormData = null;
            try
            {
                SPSite site = new SPSite(siteUrl);
                if (site == null)
                    throw new ApplicationException(String.Format(
                        CustomErrorMessages.OpenSiteFailed, siteUrl));

                SPWeb web = site.OpenWeb();
                if (web == null)
                    throw new ApplicationException(String.Format(
                        CustomErrorMessages.OpenWebFailed, siteUrl));

                if (web.Lists == null)
                    throw new ApplicationException(String.Format(
                        CustomErrorMessages.GetListFailed, 
                       listId.ToString()));

                SPList lib = web.Lists[listId];
                if (lib == null)
                    throw new ApplicationException(String.Format(
                        CustomErrorMessages.GetListFailed, 
                        listId.ToString()));

                SPListItem listItem = lib.GetItemById(itemId);
                if (listItem == null)
                    throw new ApplicationException(String.Format(
                        CustomErrorMessages.GetListItemFailed, 
                        itemId.ToString()));

                SPFile file = listItem.File;
                if (file == null)
                    throw new ApplicationException(String.Format(
                        CustomErrorMessages.GetFileFailed));
                
                xmlFormData = file.OpenBinary();
            }
            catch (Exception ex)
            {
                throw new ApplicationException(String.Format(
                    CustomErrorMessages.RetrieveFormError, 
                    ex.Message));
            }

            // Load form data into an XPathDocument object.
            XPathDocument ipForm = null;
            try
            {
                if (xmlFormData != null)
                {
                    using (MemoryStream ms = 
                        new MemoryStream(xmlFormData))
                    {
                        ipForm = new XPathDocument(ms);
                        ms.Close();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new ApplicationException(String.Format(
                    CustomErrorMessages.LoadFormDataError, 
                    ex.Message));
            }

            // Validate InfoPath form.
            if (ipForm == null)
                throw new ApplicationException(
                    CustomErrorMessages.FormXMLMissing);

            // Create XPathNavigator object to navigate XML of 
            // InfoPath form.
            XPathNavigator ipFormNav = ipForm.CreateNavigator();

            // Validate XPathNavigator object.
            if (ipFormNav == null)
                throw new ApplicationException(
                    CustomErrorMessages.FormXPathNavigatorMissing);

            // Declare variables to store values of attributes to
            // identify form.
            string formID = String.Empty;
            string href = String.Empty;
            string solutionVersion = String.Empty;

            // Retrieve the mso-infoPathSolution processing
            // instruction.
            XPathNavigator pi = ipFormNav.SelectSingleNode(
                "/processing-instruction(\"mso-infoPathSolution\")");
            if (pi != null)
            {
                string piContents = pi.Value;

                if (!String.IsNullOrEmpty(piContents))
                {
                    string[] piAttributes = piContents.Split(' ');

                    // Loop through the attributes of the processing
                    // instruction to look for the name, href,
                    // and solutionVersion attributes.
                    if (piAttributes != null)
                    {
                        int attrArrayLength = piAttributes.Length;

                        for (int i = 0; i < attrArrayLength; i++)
                        {
                            string[] piAttributeNameValuePair = 
                                piAttributes[i].Split('=');
                            if (piAttributeNameValuePair != null && 
                                piAttributeNameValuePair.Length == 2)
                            {
                                string piAttrName = 
                                    piAttributeNameValuePair[0];
                                string piAttrValue = 
                                    piAttributeNameValuePair[1];

                                // Get the values for the name, href, 
                                // and solutionVersion attributes.
                                switch (piAttrName)
                                {
                                    case "name":
                                        formID = piAttrValue.Replace(
                                            "\"", "");
                                        break;
                                    case "href":
                                        href = piAttrValue.Replace(
                                            "\"", "");
                                        break;
                                    case "solutionVersion":
                                        solutionVersion = piAttrValue
                                            .Replace("\"", "");
                                        break;
                                    default:
                                        break;
                                }
                            }
                        }
                    }
                }
            }

            // Declare variable to hold converted form data.
            byte[] convertedFormData = null;

            // Validate 'name' attribute ( = InfoPath form identifier = 
            // formID ).
            if (String.IsNullOrEmpty(formID))
                throw new ApplicationException(
                    CustomErrorMessages.FormIDMissing);

            // Convert forms to the vCalendar format.
            switch (formID)
            {
                case "urn:schemas-microsoft-com:office:infopath:DownloadAs:-myXSD-2007-09-27T08-19-13":
                    try
                    {
                        // Create XmlNamespaceManager object to declare
                        // namespaces used in form template.
                        XmlNamespaceManager nsManager =
                            new XmlNamespaceManager(
                            new NameTable());
                        nsManager.AddNamespace("my",
                           "http://schemas.microsoft.com/office/infopath/2003/myXSD/2007-09-27T08:19:13");

                        // Extract the data from the form using XPath.
                        string startDate = getNodeValue(
                            ipFormNav, nsManager, 
                            "/my:myFields/my:startDate");
                        string endDate = getNodeValue(
                            ipFormNav, nsManager, 
                            "/my:myFields/my:endDate");
                        string startTime = getNodeValue(
                            ipFormNav, nsManager, 
                            "/my:myFields/my:startTime");
                        string endTime = getNodeValue(
                            ipFormNav, nsManager, 
                            "/my:myFields/my:endTime");
                        string location = getNodeValue(
                            ipFormNav, nsManager, 
                            "/my:myFields/my:location");
                        string summary = getNodeValue(
                            ipFormNav, nsManager, 
                            "/my:myFields/my:summary");

                        // vcs uses UTC, so we need to adjust the time.
                        TimeZone tz = TimeZone.CurrentTimeZone;
                        int timeZoneOffset = 0;
                        if (tz != null)
                        {
                            timeZoneOffset = 
                                tz.GetUtcOffset(DateTime.Now).Hours;
                        }
                        DateTime startDateTime =
                            new DateTime(
                                Int32.Parse(startDate.Substring(0, 4)),
                                Int32.Parse(startDate.Substring(5, 2)),
                                Int32.Parse(startDate.Substring(8, 2)),
                                Int32.Parse(startTime.Substring(0, 2)),
                                Int32.Parse(startTime.Substring(3, 2)),
                                0);
                        DateTime startDateTimeOffset =
                            startDateTime.AddHours(-timeZoneOffset);
                        DateTime endDateTime =
                            new DateTime(
                                Int32.Parse(endDate.Substring(0, 4)),
                                Int32.Parse(endDate.Substring(5, 2)),
                                Int32.Parse(endDate.Substring(8, 2)),
                                Int32.Parse(endTime.Substring(0, 2)),
                                Int32.Parse(endTime.Substring(3, 2)),
                                0);
                        DateTime endDateTimeOffset = 
                            endDateTime.AddHours(-timeZoneOffset);

                        // Construct the vCalendar file.
                        StringBuilder sb = new StringBuilder();
                        sb.Append("BEGIN:VCALENDAR\n");
                        sb.Append("VERSION:1.0\n");
                        sb.Append("BEGIN:VEVENT\n");
                        sb.AppendFormat(
                            "DTSTART:{0}{1}{2}T{3}{4}00Z\n",
                            startDateTimeOffset.Year.ToString(),
                            startDateTimeOffset.Month.ToString()
                                .PadLeft(2, '0'),
                            startDateTimeOffset.Day.ToString()
                                .PadLeft(2, '0'),
                            startDateTimeOffset.Hour.ToString()
                                .PadLeft(2, '0'),
                            startDateTimeOffset.Minute.ToString()
                                .PadLeft(2, '0'));
                        sb.AppendFormat("DTEND:{0}{1}{2}T{3}{4}00Z\n",
                            endDateTimeOffset.Year.ToString(),
                            endDateTimeOffset.Month.ToString()
                                .PadLeft(2, '0'),
                            endDateTimeOffset.Day.ToString()
                                .PadLeft(2, '0'),
                            endDateTimeOffset.Hour.ToString()
                                .PadLeft(2, '0'),
                            endDateTimeOffset.Minute.ToString()
                                .PadLeft(2, '0'));
                        sb.AppendFormat("LOCATION:{0}\n", location);
                        sb.Append("DESCRIPTION;ENCODING=QUOTED-PRINTABLE:Appointment generated by InfoPath\n");
                        sb.AppendFormat("SUMMARY:{0}\n", summary);
                        sb.Append("END:VEVENT\n");
                        sb.Append("END:VCALENDAR\n");

                        // Convert the vCalendar file to a byte array.
                        convertedFormData = 
                            Encoding.UTF8.GetBytes(sb.ToString());
                    }
                    catch (Exception ex)
                    {
                        throw new ApplicationException(String.Format(
                            CustomErrorMessages.FormConversionError, 
                            ex.Message));
                    }
                    break;

                // Convert forms that are based on other form templates
                // to the vCalendar format.
                //case "urn:...":
                //  ...
                //  break;
                //
                //case "urn:...":
                //  ...
                //  break;

                default:
                    // There is no conversion code available for the 
                    // form, so throw error.
                    throw new ApplicationException(
                        CustomErrorMessages.ConversionCodeMissing);
            }

            // Validate converted form data.
            if (convertedFormData == null)
                throw new ApplicationException(
                    CustomErrorMessages.ConvertedFormDataMissing);

            // Retrieve the form name and construct a new file name.
            string newFileName =
                itemUrl.Substring(itemUrl.LastIndexOf('/') + 1)
               .Replace(".xml", ".vcs");

            // Return the converted form as a file to the user.
            Response.Clear();
            Response.ClearHeaders();
            Response.ClearContent();
            Response.Buffer = true;
            Response.ContentType = "text/x-vCalendar";
            Response.AddHeader("content-disposition", 
                "attachment; filename=" + newFileName);
            Response.BinaryWrite(convertedFormData);
            Response.End();   
        }
        catch (ThreadAbortException)
        {
            // Do nothing; the Response object is 'unwinding'.
        }
        catch (Exception ex)
        {
            SPUtility.TransferToErrorPage(ex.Message);
        }
    }

    private string getNodeValue(XPathNavigator nav, 
        XmlNamespaceManager nsManager, string xpath)
    {
        string nodeValue = string.Empty;
        if (nav != null && nsManager != null &&
            !String.IsNullOrEmpty(xpath))
        {
            XPathNavigator nodeNav = 
                nav.SelectSingleNode(xpath, nsManager);
            if (nodeNav != null)
            {
                nodeValue = nodeNav.Value;
            }
        }
        return nodeValue;
    }
}
Imports System.Xml
Imports System.Xml.XPath
Imports System.IO
Imports System.Text
Imports System.Threading
Imports Resources
Imports Microsoft.SharePoint
Imports Microsoft.SharePoint.Utilities

Partial Class DownloadAs_vCalendar
    Inherits System.Web.UI.Page

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load   
        
    Try
            ' Retrieve the query string parameters.
            Dim siteUrl As String = Request("SiteUrl")
            Dim itemUrl As String = Request("ItemUrl")
            Dim itemIdAsString As String = Request("ItemId")
            Dim listIdAsString As String = Request("ListId")

            ' Validate query string parameters.
            If siteUrl Is Nothing Then
                Throw New ApplicationException(String.Format(CustomErrorMessages.QueryStringParameterMissing, "SiteUrl"))
            End If
            If itemUrl Is Nothing Then
                Throw New ApplicationException(String.Format(CustomErrorMessages.QueryStringParameterMissing, "ItemUrl"))
            End If
            If itemIdAsString Is Nothing Then
                Throw New ApplicationException(String.Format(CustomErrorMessages.QueryStringParameterMissing, "ItemId"))
            End If
            If listIdAsString Is Nothing Then
                Throw New ApplicationException(String.Format(CustomErrorMessages.QueryStringParameterMissing, "ListId"))
            End If

            ' Convert itemId and listId query string parameters.
            Dim itemId As Integer = Int32.Parse(itemIdAsString)
            Dim listId As Guid = New Guid(listIdAsString)

            ' Retrieve the form from the form library.
            Dim xmlFormData As Byte() = Nothing
            Try
                Dim site As SPSite = New SPSite(siteUrl)
                If site Is Nothing Then
                    Throw New ApplicationException(String.Format(CustomErrorMessages.OpenSiteFailed, siteUrl))
                End If

                Dim web As SPWeb = site.OpenWeb()
                If web Is Nothing Then
                    Throw New ApplicationException(String.Format(CustomErrorMessages.OpenWebFailed, siteUrl))
                End If

                If web.Lists Is Nothing Then
                    Throw New ApplicationException(String.Format(CustomErrorMessages.GetListFailed, listId.ToString()))
                End If

                Dim library As SPList = web.Lists(listId)
                If library Is Nothing Then
                    Throw New ApplicationException(String.Format(CustomErrorMessages.GetListFailed, listId.ToString()))
                End If

                Dim listItem As SPListItem = library.GetItemById(itemId)
                If listItem Is Nothing Then
                    Throw New ApplicationException(String.Format(CustomErrorMessages.GetListItemFailed, itemId.ToString()))
                End If

                Dim file As SPFile = listItem.File
                If file Is Nothing Then
                    Throw New ApplicationException(String.Format(CustomErrorMessages.GetFileFailed))
                End If

                xmlFormData = file.OpenBinary()

            Catch ex As Exception
                Throw New ApplicationException(String.Format(CustomErrorMessages.RetrieveFormError, ex.Message))
            End Try

            ' Load form data into an XPathDocument object.
            Dim ipForm As XPathDocument = Nothing
            Try
                If xmlFormData IsNot Nothing Then
                    Using ms As MemoryStream = New MemoryStream(xmlFormData)
                        ipForm = New XPathDocument(ms)
                        ms.Close()
                    End Using
                End If
            Catch ex as Exception
                Throw New ApplicationException(String.Format(CustomErrorMessages.LoadFormDataError, ex.Message))
            End Try

            ' Validate InfoPath form.
            If ipForm Is Nothing Then
                Throw New ApplicationException(CustomErrorMessages.FormXMLMissing)
            End If

            ' Create XPathNavigator object to navigate XML of 
            ' InfoPath form.
            Dim ipFormNav As XPathNavigator = ipForm.CreateNavigator()

            ' Validate XPathNavigator object.
            If ipFormNav Is Nothing Then
                Throw New ApplicationException(CustomErrorMessages.FormXPathNavigatorMissing)
            End If

            ' Declare variables to store values of attributes 
            ' to identify form.
            Dim formID As String = String.Empty
            Dim href As String = String.Empty
            Dim solutionVersion As String = String.Empty

            ' Retrieve the mso-infoPathSolution processing instruction.
            Dim pi As XPathNavigator = ipFormNav.SelectSingleNode("/processing-instruction(""mso-infoPathSolution"")")
            If pi IsNot Nothing Then

                Dim piContents As String = pi.Value

                If Not String.IsNullOrEmpty(piContents) Then

                    Dim piAttributes As String() = piContents.Split(" ")

                    ' Loop through the attributes of the 
                    ' processing instruction to look for the name,
                    ' href, and solutionVersion attributes.
                    If piAttributes IsNot Nothing Then

                        Dim attrArrayLength As Integer = piAttributes.Length

                        Dim i As Integer
                        For i = 0 To attrArrayLength - 1

                            Dim piAttributeNameValuePair As String() = piAttributes(i).Split("=")
                            If piAttributeNameValuePair IsNot Nothing And piAttributeNameValuePair.Length = 2 Then

                                Dim piAttrName As String = piAttributeNameValuePair(0)
                                Dim piAttrValue As String = piAttributeNameValuePair(1)

                                ' Get the values for the name, href, 
                                ' and solutionVersion attributes.
                                Select Case piAttrName
                                    Case "name"
                                        formID = piAttrValue.Replace("""", "")
                                    Case "href"
                                        href = piAttrValue.Replace("""", "")
                                    Case "solutionVersion"
                                        solutionVersion = piAttrValue.Replace("""", "")
                                    Case Else
                                        ' do nothing
                                End Select

                            End If
                        Next
                    End If
                End If
            End If

            ' Declare variable to hold converted form data.
            Dim convertedFormData As Byte() = Nothing

            ' Validate 'name' attribute 
            ' ( = InfoPath form identifier = formID ).
            If String.IsNullOrEmpty(formID) Then
                Throw New ApplicationException(CustomErrorMessages.FormIDMissing)
            End If

            ' Convert forms to the vCalendar format.
            Select Case formID

                Case "urn:schemas-microsoft-com:office:infopath:DownloadAs:-myXSD-2007-09-27T08-19-13"
                    Try
                        ' Create XmlNamespaceManager object to declare
                        ' namespaces used in form template.
                        Dim nsManager As XmlNamespaceManager = New XmlNamespaceManager(New NameTable())
                        nsManager.AddNamespace("my", _
                           "http://schemas.microsoft.com/office/infopath/2003/myXSD/2007-09-27T08:19:13")

                        ' Extract the data from the form using XPath.
                        Dim startDate As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:startDate")
                        Dim endDate As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:endDate")
                        Dim startTime As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:startTime")
                        Dim endTime As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:endTime")
                        Dim location As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:location")
                        Dim summary As String = getNodeValue(ipFormNav, nsManager, "/my:myFields/my:summary")

                        ' vcs uses UTC, so we need to adjust the time.
                        Dim tz As TimeZone = TimeZone.CurrentTimeZone
                        Dim timeZoneOffset As Integer = 0
                        If tz IsNot Nothing Then 
                            timeZoneOffset = tz.GetUtcOffset(DateTime.Now).Hours
                        End If
                        Dim startDateTime As DateTime = _
                            New DateTime(Int32.Parse(startDate.Substring(0, 4)), _
                            Int32.Parse(startDate.Substring(5, 2)), _
                            Int32.Parse(startDate.Substring(8, 2)), _
                            Int32.Parse(startTime.Substring(0, 2)), _
                            Int32.Parse(startTime.Substring(3, 2)), _
                            0)
                        Dim startDateTimeOffset As DateTime = _
                        startDateTime.AddHours(-timeZoneOffset)
                        Dim endDateTime As DateTime = _
                            New DateTime(Int32.Parse(endDate.Substring(0, 4)), _
                            Int32.Parse(endDate.Substring(5, 2)), _
                            Int32.Parse(endDate.Substring(8, 2)), _
                            Int32.Parse(endTime.Substring(0, 2)), _
                            Int32.Parse(endTime.Substring(3, 2)), _
                            0)
                        Dim endDateTimeOffset As DateTime = endDateTime.AddHours(-timeZoneOffset)

                        ' Construct the vCalendar file.
                        Dim sb As StringBuilder = New StringBuilder()
                        sb.Append("BEGIN:VCALENDAR")
                        sb.Append(vbCrLf)
                        sb.Append("VERSION:1.0")
                        sb.Append(vbCrLf)
                        sb.Append("BEGIN:VEVENT")
                        sb.Append(vbCrLf)
                        sb.AppendFormat("DTSTART:{0}{1}{2}T{3}{4}00Z", _
                            startDateTimeOffset.Year.ToString(), _
                            startDateTimeOffset.Month.ToString().PadLeft(2, "0"), _
                            startDateTimeOffset.Day.ToString().PadLeft(2, "0"), _
                            startDateTimeOffset.Hour.ToString().PadLeft(2, "0"), _
                            startDateTimeOffset.Minute.ToString().PadLeft(2, "0"))
                        sb.Append(vbCrLf)
                        sb.AppendFormat("DTEND:{0}{1}{2}T{3}{4}00Z", _
                            endDateTimeOffset.Year.ToString(), _
                            endDateTimeOffset.Month.ToString().PadLeft(2, "0"), _
                            endDateTimeOffset.Day.ToString().PadLeft(2, "0"), _
                            endDateTimeOffset.Hour.ToString().PadLeft(2, "0"), _
                            endDateTimeOffset.Minute.ToString().PadLeft(2, "0"))
                        sb.Append(vbCrLf)
                        sb.AppendFormat("LOCATION:{0}", location)
                        sb.Append(vbCrLf)
                        sb.Append("DESCRIPTION;ENCODING=QUOTED-PRINTABLE:Appointment generated by InfoPath")
                        sb.Append(vbCrLf)
                        sb.AppendFormat("SUMMARY:{0}", summary)
                        sb.Append(vbCrLf)
                        sb.Append("END:VEVENT")
                        sb.Append(vbCrLf)
                        sb.Append("END:VCALENDAR")
                        sb.Append(vbCrLf)

                        ' Convert the vCalendar file to a byte array.
                        convertedFormData = Encoding.UTF8.GetBytes(sb.ToString())
                    Catch ex As Exception
                        Throw New ApplicationException(String.Format(CustomErrorMessages.FormConversionError, ex.Message))
                    End Try

                    ' Convert forms that are based on other form
                    ' templates to the vCalendar format.
                    'Case "urn:..."
                    '  ...
                    '
                    'Case "urn:..."
                    ' ...

                Case Else
                    ' There is no conversion code available for the
                    ' form, so throw error.
                    Throw New ApplicationException(CustomErrorMessages.ConversionCodeMissing)
            End Select

            ' Validate converted form data.
            If convertedFormData Is Nothing Then
                Throw New ApplicationException(CustomErrorMessages.ConvertedFormDataMissing)
            End If

            ' Retrieve the form name and construct a new file name.
            Dim newFileName As String = itemUrl.Substring(itemUrl.LastIndexOf("/") + 1).Replace(".xml", ".vcs")

            ' Return the converted form as a file to the user.
            Response.Clear()
            Response.ClearHeaders()
            Response.ClearContent()
            Response.Buffer = True
            Response.ContentType = "text/x-vCalendar"
            Response.AddHeader("content-disposition", "attachment; filename=" & newFileName)
            Response.BinaryWrite(convertedFormData)
            Response.End()

        Catch ex As ThreadAbortException
            ' Do nothing; the Response object is 'unwinding'
        Catch ex As Exception
            SPUtility.TransferToErrorPage(ex.Message)
        End Try


    End Sub

    Private Function getNodeValue(ByVal nav As XPathNavigator, ByVal nsManager As XmlNamespaceManager, ByVal xpath As String) As String

        Dim nodeValue As String = String.Empty

        If nav IsNot Nothing And nsManager IsNot Nothing And Not String.IsNullOrEmpty(xpath) Then

            Dim nodeNav As XPathNavigator = nav.SelectSingleNode(xpath, nsManager)
            If Not nodeNav Is Nothing Then
                nodeValue = nodeNav.Value
            End If
        End If

        Return nodeValue

    End Function
End Class

Conclusion

You can use a Feature to extend the user interface of SharePoint form libraries with a custom menu item and associate this menu item with an ASP.NET page. You can implement code in the ASP.NET page to retrieve an InfoPath form from a form library, identify the form, extract data from the form, construct a file in a new format, and then return the converted form as a downloadable file to the user. You can apply several enhancements to this technique to make your solution more robust, flexible, and easy to extend and deploy.

About the Author

S.Y.M. Wong-A-Ton, MCSD, MCSD.NET, has been working in the software industry for over a decade. Currently, she is a Technology Consultant for a large global technology solutions provider.

Additional Resources