|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Introduction (ramble)Another boring day in my life. I often wonder why it has to be like this – I either have no obligations at all, or I'm packed with them. Most people tell me that it has to do with planning. They wear their favorite serious face and say: you, my son, just don't know how to properly make your schedule. Time is the essence, do not use it improperly! You must divide your time in million little pieces, label each one of them, implement some kind of sorting, import everything into Microsoft Outlook, and stick to that plan. Then, and only then, you'll be a man who is not a campaigner, but an organized, equally time pressured My answer is way simpler, I often reply with just – hey, screw you! ;) Because, really, I've always believed it's not about planning, but about the way universe has been built. Think about it – everything important was created in just those few seconds after the Big Bang. All that followed was just plain simple boring processes of waiting for the fruits of planted seeds to grow; the stage setting for another big moment. So, I hope you'll agree that it is – as Derek Ager once wrote – like the life of a soldier... long periods of boredom, and short periods of terror. One can just hope that those "long periods of boredom" can be filled with small joys of doing something you like; something that'll ease your wait for those important "short periods". This article is just that, my way of getting along with boredom... if it helps someone else, or fills his spare time, my joy will only be greater. Index
ProblemI don't know if you are in a club, but I've met numerous .NET developers who had much trouble with choosing the right tool to build reports. Apart from praise for the Access report building capabilities, you won't hear many compliments for the reporting tools. I guess we have all tried Crystal Reports embedded into Visual Studio .NET - they are OK, but are demanding. And often, small bugs, along with ridiculous option placements, will drive you nuts. SQL Reporting Services are somewhat a new option that is praised all over the web by Microsoft evangelists. In practice, however, I've often stumbled on projects where the team is paralyzed with problems concerning configuration and specific aspects of report writing. Finally, there are numerous custom reporting frameworks such as ActiveReports or DevExpress' (I love these guys) Reporting Tools. Specific maladies aside, the common problem with all the previously laid options is that they have a modest learning curve. I'm not talking about the time needed to acquire the knowledge for generating a list of employees from an "It's easy to use our report suite"™ example. I'm talking about the time needed to acquire the knowledge for developing real-life reports which have three tables that properly expand and contract (along with its columns and rows) over pages. Also, none of these options provide you with the solution for frequent user requirements – when a report is rendered, it should be possible to modify it a bit. The workaround is to use report exporting to popular formats that are known to most users, like Word. As I've experienced, this is the point when the bulb shines above the head of the developer and the idea comes - why not generate reports in Word in the first place. In the majority of projects, clients are provided with the needed output reports in Word format, which they print and fill by hand. And if not... well, you have one of the best "report designers" in the world, as it was tweaked and improved over numerous versions. So, how to do it? Brief solution descriptionOne big, big problem with Word documents before the 2003 version was their binary format. Word's file format was not publicly available, and all utilities that could parse it were mostly developed by reverse-engineering, or by However, in 2003, Microsoft introduced XML formats for storing Office documents. Those formats were succeeded by Office Open XML formats in Office 2007 (which are default, instead of their binary counterparts), so you can safely bet that they are here to stay. So, in order to generate a Word file now, you basically need to apply the appropriate XSLT (XSL Transform) onto the XML data used in a report. This process can be divided into several operational steps:
The biggest problem is to produce valid XSLT; from five steps, three are taken to do that. The generation of XML is far easier, while the transformation is completely trivial. Producing the XSL transformationDefining the XML schema based on the reportIn order to start making the report, it is required to define the necessary data. A picture talks more than a thousand words, an example talks almost an equal amount... so let's look at the picture of the report that we'll use as an example:
Figure 1 – Report that should be generated
It is obvious that we first have the buyer's name, the document date follows. Then we have, from the developer point of view, an interesting table of invoice items... and so on. The structure of the XML which will hold this data is described using an XML schema. Visual Studio 2005 has nice support for visual design of schemas, which we will utilize – after starting the IDE, take option File –> New –> File (CTRL+N): this gives a list of possible document types from which we choose XML Schema. An
Figure 2 – Schema that defines the structure of data for the report
In order to be properly mapped, items on the invoice need to be described as child elements of the
Figure 3 – Adding a child to the Invoice entity
Adding the rest of the elements, assigning types to variables, and setting the Assigning types to variables is optional in most cases – if you use special formats for printing out documents (like dd.MM.yyyy) or monetary values ($10.99), it's easier to leave everything in the schema in string type, and do the formatting and validation during the generation of XML with the data. On the other hand, setting the
Figure 4 – Resulting XML schema
Binding data from the Word document to the appropriate fields in the XML schemaSchema importing is performed by using the XML Structure dialog. In the 2003 version of Office Word, this dialog is accessible through Task Pane (CTRL+F1); it should be chosen from the list shown when clicked on the triangle in the header (left from the small x). If schemas aren't previously imported, and the Template and Add-Ins option is chosen, the picture that follows will faithfully resemble the resulting state of the screen.
Figure 5 – Adding the new XML schema in the Word document
In the dialog shown after clicking on the Add Schema button, it is needed to point to the location of the defined XML schema. Its fields will be then shown in the XML Structure dialog, from where they are further bound to the document data. Before starting that sweet job, some additional options should be set:
This is present to force valid entry of data in the Word document (another application of the technique that is being described). However, our current goal is diametrically opposite – we are not marking fields for entry, but for space in which data from the XML will be inserted, so it's not needed to force a unique appearance and order.
Figure 6 – Dialog for setting XML data
After the schema is imported in to the document and the options set, it's time to move onto binding the schema and the data. Initially, only the root element (in our case,
Figure 7 – Options for applying the schema on the appropriate range in the document
In this example, applying the schema to the entire document is a needed option (possible multi-schematic Word files aren't interesting from the reporting point of view). Now, what is left is to mark the data – the selected text is bound to the schema either by choosing the field from the Task Pane, or by using the option Apply XML Element shown after a right click.
Figure 8 – Binding data from a Word document to fields of the XML schema
Two things are interesting here. First, to define child items, you need to select and map the whole row in the table to the Second, Word, for previously explained reasons, signals error for double usage of the Saving into WordML and the generation of XSLTThe marked document contains all the data needed for the generation of valid XSLT. The WML2XSLT tool accepts WordML as input, so it's required to save the Word document in this format. You can do this by using the Save As option from the File menu – when the dialog is shown in Save as Type, choose XML document (*.xml). The option Apply transform is used in the opposite direction, Data only when XML data is fetched from the document, so both fields should be left unchecked. The prepared WML file is processed using this statement in the Command Prompt (the following is valid assuming that everything is in the same directory): WML2XSLT.exe "WordGeneratedInvoice.xml" –o "WordGeneratedInvoice.xslt"
In case you run into problems ( Solving problems with multiple used elementsThe generated XSL transform will almost always be satisfying. One exception is when an element from the XML with data is used multiple times. In the example we are developing, the <w:r> <w:t><xsl:text>(Buyer: </xsl:text></w:t></w:r>
<xsl:apply-templates select="ns1:Buyer[position() >= 2]" />
<w:r> <w:t><xsl:text>)</xsl:text></w:t></w:r>
It's obvious we aren't interested in the element <w:r> <w:t><xsl:text>(Buyer: </xsl:text></w:t></w:r>
<xsl:apply-templates select="ns1:Buyer" />
<w:r> <w:t><xsl:text>)</xsl:text></w:t></w:r>
Inserting images into the documentNaturally, WordML has good support for images, but it is very poorly documented. So, in order to see how images are represented in WML format, we'll perform a little experiment and save the marked Word document displayed below as XML:
Figure 9 – Document with image
After processing the saved document using the WML2XML tool (with the WML2XML ExampleImage.xml -o ExampleImage.xslt command), and opening the generated XSLT file, we can scroll to the <ns0:SomeImage>
<xsl:for-each select="@ns0:*|@*[namespace-uri()='']">
<xsl:attribute name="{name()}" namespace="{namespace-uri()}">
<xsl:value-of select="." />
</xsl:attribute>
</xsl:for-each>
<w:r>
<w:pict>
<v:shapetype id="_x0000_t75" coordsize="21600,21600" o:spt="75"
o:preferrelative="t" path="m@4@5l@4@11@9@11@9@5xe" filled="f" stroked="f">
<v:stroke joinstyle="miter" />
<v:formulas>
<v:f eqn="if lineDrawn pixelLineWidth 0" />
<v:f eqn="sum @0 1 0" />
<v:f eqn="sum 0 0 @1" />
<v:f eqn="prod @2 1 2" />
<v:f eqn="prod @3 21600 pixelWidth" />
<v:f eqn="prod @3 21600 pixelHeight" />
<v:f eqn="sum @0 0 1" />
<v:f eqn="prod @6 1 2" />
<v:f eqn="prod @7 21600 pixelWidth" />
<v:f eqn="sum @8 21600 0" />
<v:f eqn="prod @7 21600 pixelHeight" />
<v:f eqn="sum @10 21600 0" />
</v:formulas>
<v:path o:extrusionok="f" gradientshapeok="t" o:connecttype="rect" />
<o:lock v:ext="edit" aspectratio="t" />
</v:shapetype>
<w:binData w:name="wordml://01000001.gif">R0lGODlhEAAQAPIGAAAAAAAAsACwALAAALD/sP+wsP
///////yH5BAEAAAcALAAAAAAQABAAAAOW
eHd3h3d3d3h3d4d3cHd4d3eHd3cHWHAXgXF3d3gHVYNwZxZ4d3eAVTUDeHdhh3d3UFgDdocRcXd4
d1CAdncXaHZ3h3dgd3h3Z4d3d3d4d3eHB3d3eHd3h3d3QAh3d4d3d3d4QCSAd3d3eHcHhEQicHh3
d4d3B0QoYHeHd3d3eAcEhnd3d3h3d4cHdnd4d3eHd3d3eHeXADu=
</w:binData>
<v:shape id="_x0000_i1025" type="#_x0000_t75" style="width:12pt;height:12pt">
<v:imagedata src="wordml://01000001.gif" o:title="convert" />
</v:shape>
</w:pict>
</w:r>
<w:p>
<w:r>
<w:t>
<xsl:value-of select="." />
</w:t>
</w:r>
</w:p>
</ns0:SomeImage>
Obviously, the image is Base64 encoded into the XML file between the <ns0:SomeImage>
<xsl:for-each select="@ns0:*|@*[namespace-uri()='']">
<xsl:attribute name="{name()}" namespace="{namespace-uri()}">
<xsl:value-of select="." />
</xsl:attribute>
</xsl:for-each>
<w:r>
<w:pict>
<w:binData w:name="wordml://01000001.gif"><xsl:value-of select="." /></w:binData>
<v:shape id="_x0000_i1025" type="#_x0000_t75" style="width:12pt;height:12pt">
<v:imagedata src="wordml://01000001.gif" o:title="convert" />
</v:shape>
</w:pict>
</w:r>
</ns0:SomeImage>
It looks better, doesn't it? All that is left is to supply the XML data in the proper format: <?xml version="1.0" encoding="utf-8" ?>
<Something xmlns="http://schemas.microsoft.com/GeneratingWordDocuments/ImageExample.xsd">
<SomeText>Small image below</SomeText>
<SomeImage>R0lGODlhE[-- binary data truncated --]3d3eHeXADu=</SomeImage>
</Something>
and we'll have the document from Figure 9 in no time. One final word of warning - if your images aren't always the same size, you'll want to check the <w:pict>
<w:binData w:name="wordml://01000001.gif">
<xsl:value-of select="." />
</w:binData>
<v:shape id="_x0000_i1025" type="#_x0000_t75">
<xsl:attribute name="style">
<xsl:value-of select="@style"/>
</xsl:attribute>
<v:imagedata src="wordml://01000001.gif" o:title="convert" />
</v:shape>
</w:pict>
<?xml version="1.0" encoding="utf-8" ?>
<Something xmlns="http://schemas.microsoft.com/GeneratingWordDocuments/ImageExample.xsd">
<SomeText>Small image below</SomeText>
<SomeImage style="width:24pt;height:24pt">R0lGOD[-- binary data truncated --]3d3eADu=
</SomeImage>
</Something>
Opening the document in read-only modeTo force opening the report in read-only mode when the report is displayed to the user, it's needed to use the Tools -> Options -> Security -> Protect Document option during the document creation. Under Editing Restrictions, 'No changes (Read only)' should be chosen... after that, the only thing left to do is click onto 'Yes, Start Enforcing Protection' and enter the password for protection. Of course, further steps remain the same - the document is saved as WordML, processed through the WML2XSLT tool...
Figure 10 – Settings for the read-only mode
Do not expect too much from this "protection". In WordML format, it's enforced by one line in the <w:docPr>
<w:view w:val="print" />
<w:zoom w:percent="85" />
<w:doNotEmbedSystemFonts />
<w:proofState w:spelling="clean" w:grammar="clean" />
<w:attachedTemplate w:val="" />
<u><w:documentProtection w:edit="read-only" w:enforcement="on"
w:unprotectPassword="4560CA9C" /></u>
<w:defaultTabStop w:val="720" />
<w:punctuationKerning />
<w:characterSpacingControl w:val="DontCompress" />
<w:optimizeForBrowser />
<w:validateAgainstSchema />
<w:saveInvalidXML />
<w:ignoreMixedContent />
<w:alwaysShowPlaceholderText w:val="off" />
<w:compat>
<w:breakWrappedTables />
<w:snapToGridInCell />
<w:wrapTextWithPunct />
<w:useAsianBreakRules />
<w:dontGrowAutofit />
</w:compat>
<w:showXMLTags w:val="off" />
</w:docPr>
This means that the read-only mode can be easily incorporated into XSLT for reports you've already done... but, it also means that anyone knowing WML format can easily workaround your "protection". So, use it wisely :) Preparing data and applying the transformationT-SQL and XMLXML data that satisfies the previously defined schema and which we'll use in the report can be generated in many ways. The most commonly used is the one that utilizes the
For example, if there is a c_City table with columns SELECT CityId, CityName FROM c_City AS City
FOR XML AUTO
<City CityId="43" CityName="100 Mile House" />
<City CityId="53" CityName="Abbotsford" />
If it's needed to write out data in elements, the SELECT CityId, CityName FROM c_City AS City
FOR XML AUTO, ELEMENTS
<City>
<CityId>43</CityId>
<CityName>100 Mile House</CityName>
</City>
<City>
<CityId>53</CityId>
<CityName>Abbotsford</CityName>
</City>
As two elements exist on the first level, SELECT CityId, CityName FROM c_City AS City
FOR XML AUTO, ELEMENTS, ROOT('Root')
<Root>
<City>
<CityId>43</CityId>
<CityName>100 Mile House</CityName>
</City>
<City>
<CityId>53</CityId>
<CityName>Abbotsford</CityName>
</City>
</Root>
Let's assume that there is a c_PostalCode table with postal codes used in cities. If it's required to make XML where postal codes will be child element of cities, the following SQL is in order: SELECT CityId, CityName,
(SELECT PostalCodeId, PostalCodeName FROM c_PostalCode
WHERE CityId = City.CityId
FOR XML AUTO, TYPE)
FROM c_City AS City
FOR XML AUTO, TYPE
<Root>
<City CityId="43" CityName="100 Mile House">
<c_PostalCode PostalCodeId="317701" PostalCodeName="V0K2Z0" />
<c_PostalCode PostalCodeId="317702" PostalCodeName="V0K2E0" />
</City>
<City CityId="53" CityName="Abbotsford">
<c_PostalCode PostalCodeId="317703" PostalCodeName="V3G2J3" />
</City>
</Root>
If more output flexibility is required, it's possible to format the XML in more detail using the SELECT CityId AS '@CityId', CityName,
(SELECT PostalCodeId AS 'NotNeeded/PostalCodeId', PostalCodeName
FROM c_PostalCode
WHERE CityId = City.CityId
FOR XML path('PostalCode'), TYPE)
FROM c_City AS City
FOR XML PATH('CityRow'), type, root('Data')
<Data>
<CityRow CityId="43">
<CityName>100 Mile House</CityName>
<PostalCode PostalCodeName="V0K2Z0">
<NotNeeded>
<PostalCodeId>317701</PostalCodeId>
</NotNeeded>
</PostalCode>
<PostalCode PostalCodeName="V0K2E0">
<NotNeeded>
<PostalCodeId>317702</PostalCodeId>
</NotNeeded>
</PostalCode>
</CityRow>
<CityRow CityId="53">
<CityName>Abbotsford</CityName>
<PostalCode PostalCodeName="V3G2J3">
<NotNeeded>
<PostalCodeId>317703</PostalCodeId>
</NotNeeded>
</PostalCode>
</CityRow>
</Data>
Binding XML to schemaFor the XML data to be shown in Word, it's necessary that the SELECT Buyer, InvoiceDate, ...
FROM Invoice
FOR XML PATH('Invoice'), ELEMENTS
<Invoice>
<Buyer>John Doe</Buyer>
<InvoiceDate>2008-01-01</InvoiceDate>
...
</Invoice>
It's needed to set the WITH XMLNAMESPACES(DEFAULT
'http://schemas.microsoft.com/GeneratingWordDocuments/WordGeneratedInvoice.xsd')
SELECT Buyer, InvoiceDate, ...
FROM Invoice
FOR XML PATH('Invoice'), ELEMENTS
<Data xmlns="http://schemas.microsoft.com/GeneratingWordDocuments/WordGeneratedInvoice.xsd">
<Buyer>John Doe</Buyer>
<InvoiceDate>2008-01-01</InvoiceDate>
...
</Invoice>
A blank Word document is the most common result if the XML data is not bound to the schema over an Applying the transformation on XML datapublic static byte[] GetWord(XmlReader xmlData, XmlReader xslt)
{
XslCompiledTransform xslt = new XslCompiledTransform();
XsltArgumentList args = new XsltArgumentList();
using (MemoryStream swResult = new MemoryStream())
{
xslt.Load(xslt);
xslt.Transform(xmlData, args, swResult);
return swResult.ToArray();
}
}
It's mentioned earlier that this step is trivial. The example justifies that, doesn't it? After the XML data and the XSL transformation are passed as XML->XSLT->HTML->Word, the easy way outIn case you don't need advanced capabilities that Word provides (page numbering, margins, and similar), you have a pretty handy option of hand-writing XSLT that transforms XML data to HTML and then just opens HTML in Word. To illustrate the idea with an example – here is an XSLT that I use for a list report that just shows the contents of a CD <?xml version='1.0' encoding='UTF-8' ?>
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='1.0'
xmlns:fo='http://www.w3.org/1999/XSL/Format'
xmlns:fn='http://www.w3.org/2003/11/xpath-functions'
xmlns:xf='http://www.w3.org/2002/08/xquery-functions'>
<xsl:template match='/'>
<html>
<body>
<h2>Report Header</h2>
<table border='0' width='100%'>
<tr bgcolor='Gray'>
<th align='left'>Title</th>
<th align='left'>Price</th>
</tr>
<xsl:for-each select='DocumentElement/Cd'>
<tr>
<td>
<xsl:value-of select='Title'/>
</td>
<td>
<xsl:value-of select='Price'/>
</td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
The XML data which is transformed: <?xml version='1.0' encoding='UTF-8' ?>
<DocumentElement>
<Cd>
<Title>Mike</Title>
<Price>20$</Price>
</Cd>
<Cd>
<Title>Nike</Title>
<Price>30$</Price>
</Cd>
<Cd>
<Title>Reebok</Title>
<Price>40$</Price>
</Cd>
</DocumentElement>
When the Resulting HTML: <html xmlns:fo="http://www.w3.org/1999/XSL/Format"
xmlns:fn="http://www.w3.org/2003/11/xpath-functions"
xmlns:xf="http://www.w3.org/2002/08/xquery-functions">
<body>
<h2>Something</h2>
<table border="0" width="100%">
<tr bgcolor="Gray">
<th align="left">Title</th>
<th align="left">Price</th>
</tr>
<tr>
<td>Mike</td>
<td>20$</td>
</tr>
<tr>
<td>Nike</td>
<td>30$</td>
</tr>
<tr>
<td>Reebok</td>
<td>40$</td>
</tr>
</table>
</body>
</html>
Word, even in versions earlier than 2003, had no any problems with opening HTML; so, just save the result as .doc (instead of .HTML) and you'll be done. In case you are sending the response over the Web, you can specify the type with: Response.AddHeader("content-type", "application/msword");
Response.AddHeader("Content-Disposition", "attachment; filename=report.doc");
The true value of this option comes into light when you start thinking about generic reports. In the source code that accompanies this article, you'll find a generic version of this example, the one that works with any Organization of resources used for the generation in the Visual Studio projectThe source code I have attached to this article demonstrates one possible way of organizing the needed resources for the Word reports generation. Here is the project structure:
Figure 11 - XSL transform as part of the VS.NET project for generating Word reports
It is of utmost importance that Embedded Resource is set on the Build Action for all the resources that are used in the generation of the Word document (XML, XSD, XSLT). This enables their later fetching from the resource collection of the compiled DLL. Reports are generated through a static public class Report
{
/// <summary>
/// Generates Demonstration Report
/// </summary>
/// <returns>Resulting Word document as Byte array</returns>
public static byte[] WordGeneratedInvoice()
{
// Get data as XML... for demonstration prepared XML is used,
// in real implementation scenario this data would be
// fetched from SQL Server
string xmlData = Getters.GetTestXml("WordGeneratedInvoice");
return Getters.GetWord(xmlData, "WordGeneratedInvoice");
}
// ... //
/* Add Report methods here */
}
Adding new reports in this structure is easy:
FAQCan I convert the generated WordML to PDF? How do I do it?Check out my article Generate PDF using C#. I applied the schema to the Word document and ended my work on it. After some time, I reopened the document, but in the XML structure dialog, the list of elements available for applying onto the document (the lower listbox) is empty.This occurred because the path to the XSD file is changed. The location of the schema can be refreshed by using XML Options -> Schema Library -> choose the schema used in the document -> Schema Settings -> Browse...
Figure 12 – Dialogs (ordered from left to right) that visually show the path to the Browse... option
I've changed the XML schema (XSD) after the changed request for the new report fields arrived. However, Word 2003 does not show new fields in the XML structure dialog, so I can't bind them to the data in the new version of the report. Must I build the report from scratch?This problem can be solved by installing the Office 2003 Service Pack 2. When SP2 is installed, Word 2003 will refresh the attached schema if the following steps are satisfied:
In some situations, the better way to solve this problem is to install the XML Toolbox for Microsoft Office Word 2003 – it adds the command Refresh Schema. The solution isn't universal because the XML Toolbox doesn't install properly always (the most common problems are security polices, the existence of .NET Framework 1.1...). So, my suggestion is to close all Office applications, download the .msi from the link, run it – if everything goes smoothly, you'll see the Word XML toolbar (View -> Toolbars -> Word XML Toolbar); if not, you always have the first suggestion for schema refreshing.
Figure 13 - XML Toolbox in Word 2003, with the Reload Schema option
I made the XSD, bound it to the Word document, made the XSLT, prepared the XML data, performed the transformation, and got – empty documentThe most common cause of this problem is that XML doesn't contain the schema binding (as a value of the The easiest way to see the type of XML you should prepare is to get the properly schema and fields bounded Word document to be saved on the some temporary location as the XML (File -> Save as, Save as type: XML document), by checking the option Save data only. You can view the saved XML by opening it in Visual Studio .NET or Notepad...
Figure 14 – Saving the XML data only from properly mapped Word document
Where are the XML options located in Office Word 2007? How different is report making between Word 2003 and 2007?Honestly, I haven't worked much in the 2007 version of Word, but still - I couldn't find big differences. The only problem I've had is in finding the XML Structure dialog, as it was not accessible with the Task Pane. It seems that the XML Toolbox is installed by default with Office 2007, so you can solve this by its adding by using the toolbar's (Ribbon's) option, Customize...
Figure 15 – Dialog shown after choosing the option Customize... in Word 2007
Figure 16 - XML Toolbox in Word 2007
ConclusionIt is worthy to note that the solution I recommended doesn't use Visual Studio Tools for Office. I tried them out for document generation, and was very disappointed as they required a nasty deal of configuring both to develop and run. Also, using XSLT to produce Word documents is far easier than juggling with the Microsoft Word Object Library COM DLL and its Well, that's it folks. You know the drill - please take your time to rate this article, and if you are (un)happy with it or just need some aid, post comments and I'll be glad to respond/help in no time :). ReferencesIn no particular order… Books:
History
| ||||||||||||||||||||||