maandag 29 oktober 2007

Generate docx files

Recently I was asked to make a feature who would open a .docx file filled with the Active Directroy data from the currently logged user. I had to build a feature who would redirect me to an .aspx page (in fact an .ashx page but it'll become more clear by then end this post), from that page I had to retrieve the information of the
currently logged in user, get his data from Acitve Directory with an LDAP query and create a .docx file with Open XML which I have to fill with the data retrieved from Active Directory.
Right....let's start with the easy part, create a feature which redirects me to my .ashx page.
For our feature we need two files, a feature.xml and a elements.xml.
The feature.xml is a standard feature which points to our elemnts.xml where we will build our redirection CustomAction.
The code for the feature.xml file should look like this:

<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="724BB38A-051C-4ce5-844B-2F91FC51B66B"
Title="Create document from template"
Description="This creates a document from a template in this libarary"
Version="1.0.0.0"
Scope="Web"
Hidden="FALSE"
xmlns="http://schemas.microsoft.com/sharepoint">
<ElementManifests>
<ElementManifest Location="elements.xml" />
</ElementManifests>
</Feature>

The elements.xml file like this:

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction Id="CreateDocFromTemplate"
RegistrationType="List"
GroupId="ActionsMenu"
Location="Microsoft.SharePoint.StandardMenu"
Title="Create document from template"
Description="This creates a document from a template in this libarary" >
<UrlAction Url="~site/_layouts/docgeneration/generatedoc.ashx" />
</CustomAction>
</Elements>

We see that in the elements.xml file we define a CustomAction which defines the Location - Title - Description. Like you see it here we are placing our link in the Actions menu dropdownlist for any document library.
The most important part is the UrlAction, this indicates to which page we want to go once we click on the link.
Now that we have our feature we can start with the real work. What we will do is create a .docx file filled with Active Directory data from the user currently logged on our SharePoint site. But the important thing to know here is, that we are not going to create a file which is stored on a physical drive but we will create the file inside a MemoryStream and return that stream in the Httphandler of our .ashx file. What we will obtain is a dialog box which appear once
we click the feature asking us if we want to save or open the file. Once we click Open the file will then be opened in a Word editor.

Create a new website in VS2005 and select the Empty Web Site template. We are adding a new Web Form to our web site, we will name it gendocx.ashx and delete the whole content of the file. Also delete the gendocx.ashx.cs file created with the gendocx.ashx file as we will put all our code in the first .ashx file.
The .ashx file consists of three code-part and the surrounding class declaration.
To be able to send our docx file to the client we need to implement the IHttphandler interface and add a @WebHandler.
To make it easy we’ll also add an @Assembly directive so we can program against the
Windows SharePoint Services object model.

<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ WebHandler Language="C#" Class="gendocx" %>


using System;
using System.IO;
using System.Xml;
using System.Web;
using System.Data;
using System.IO.Packaging;
using Microsoft.SharePoint;

public class gendocx : IHttpHandler {
public bool IsReusable {
get { return false; }
}

public void ProcessRequest(HttpContext context) {
}
}

Add a reference to the WindowsBase.dll and the Microsoft Office SharePoint Server Components assembly.
The first of code we will add now contains the basics command that will be processed when we activate the link.
Some function or code may seem unclear but they'll be explained later on.
Paste the following code inside the ProcessRequest method.

MemoryStream stream = new MemoryStream();
Package pack = Package.Open(stream, FileMode.Create, FileAccess.ReadWrite);
this.BuildDoc(pack);
pack.Close();
context.Response.ClearHeaders();
context.Response.AddHeader("content-disposition", "attachment; filename="+this.getNameFromAD()+".docx");
context.Response.ClearContent();
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
context.Response.ContentType = "application/vnd.ms-word.document.12";
stream.Position = 0;
BinaryWriter writer = new BinaryWriter(context.Response.OutputStream);
BinaryReader reader = new BinaryReader(stream);
writer.Write(reader.ReadBytes((int)stream.Length));
reader.Close();
writer.Close();
stream.Close();
context.Response.Flush();
context.Response.Close();

What is happening here? Well basicly we are creating a MemoryStream that we are filling with a Package, the Package on his turn is filled with all the xml data needed to create a docx file.
Once those two have been made we clear the header of our response which we are filling with our attachement file (read docx file). Every Open statment should be ended by a Close statement. We are also declaring writer and a reader who basicly help us to read the MemoryStream and write in our response header.
You have noticed that I have added two undeclared function beeing BuildDoc and getNameFromAD.

Let me first explain what the BuildDoc method does. We have to create our docx file. How do we do this? We create it build piece after piece with Open XML. If you are not familiar with Open XML I advice you to read this article (Building Server-Side document generation using the Open XML Object Model (MSDN - Erika Ehrli )) or the e-book by Wouter van Vugt (Open XML:The Markup Explained). A docx file is made of several smaller xml files which
combined together make a complete docx file. Take a look at the next chunk of code and you will understand what I'm talking about:

public void BuildDoc(Package pack)
{
Uri uri = new Uri("/word/document.xml", UriKind.Relative);
string partContentType;
partContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
PackagePart part = pack.CreatePart(uri, partContentType);
StreamWriter streamPart = new StreamWriter(part.GetStream(FileMode.Create, FileAccess.Write));
string nameSpace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
XmlDocument xmlPart = new XmlDocument();
XmlElement document;
document = xmlPart.CreateElement("w:document", nameSpace);
xmlPart.AppendChild(document);
XmlElement body;
body = xmlPart.CreateElement("w:body", nameSpace);
document.AppendChild(body);
XmlElement paragraph;
paragraph = xmlPart.CreateElement("w:p", nameSpace);
body.AppendChild(paragraph);
XmlElement row;
row = xmlPart.CreateElement("w:r", nameSpace);
paragraph.AppendChild(row);
XmlElement text;
text = xmlPart.CreateElement("w:t", nameSpace);
row.AppendChild(text);
XmlNode nodeText;
nodeText = xmlPart.CreateNode(XmlNodeType.Text, "w:t", nameSpace);
nodeText.Value = this.getNameFromAD();
text.AppendChild(nodeText);
xmlPart.Save(streamPart);
streamPart.Close();
pack.Flush();
string relation;
relation = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
pack.CreateRelationship(uri, TargetMode.Internal, relation, "relation");
pack.Flush();
}

The last part is getting the data from Active Directory. For this example we will only retrieve the name of the user currently logged in. We compare the mail of the user with the mail stored in Active Directory with an LDAP query.

public string getNameFromAD()
{
string userMail = SPContext.Current.Web.CurrentUser.Email.ToString();
DirectorySearcher objsearch = new DirectorySearcher();
string strrootdse = objsearch.SearchRoot.Path;
DirectoryEntry objdirentry = new DirectoryEntry(strrootdse);
objsearch.Filter = "((mail=" + userMail + "))";
objsearch.SearchScope = System.DirectoryServices.SearchScope.Subtree;
objsearch.PropertiesToLoad.Add("cn");
objsearch.PropertyNamesOnly = false;
objsearch.Sort.Direction = System.DirectoryServices.SortDirection.Ascending;
objsearch.Sort.PropertyName = "cn";
objsearch.PageSize = 1;
SearchResult result = objsearch.FindOne();
objsearch.Dispose();
return result.GetDirectoryEntry().Properties["cn"].Value.ToString();
}

Save your file. Go to your site and click on your feature, a dialog box should pop up and ask you if you want to open or save your file. Click open and there it is, a docx file filled with Active Directory data from our currently logged user.

Links I used:
Retrieving data from Active Directory with System.DirectoryServices – the right way
Generating Office 2007 documents in C#
Server-Side Generation of Word 2007 Docs"

Geen opmerkingen: