Let me begin by stating that it is very important to understand that my solution is running only in IE7 and is inside an company intranet. What this means is that I have to have the users trust the site and setup IE to allow Unsigned Active X controls because we are going to be automating Excel. However, in this environment it is okay and all the users will share the same experience because they all have Excel 2007. [I would like to sign my controls but we have no way to do that just yet.]
Excel 2007 supports XML using the Excel XML format. The simplest way to see this in action is to open Excel, put some stuff in it, and format it the way you want. You can even change the page layout, margins, header/footer, and setup conditional formatting. Save the file as an XML file. Open this file in your favorite (or not so favorite) XML editor. I have VS 2008 and it works fine for this. You will notice that this XML is actually very easy (scary I know) to read (or am I just a geek!). You can also see all of the formatting you added and all of that is well formed XML. Now comes the trick! You can convert this XML to XSL very easily and this XSL can then be used to format data to Excel readable XML that will open in Excel formatted the way you want. Cool huh?
ON WITH THE GOOD STUFF!!
So the first report that I was asked to create was similar to a list view. However, we have need for a dynamic header and footer option along with some custom color coding. We also needed to span 3 rows for each item. Without getting too detailed, here are some of the steps that were taken:
Step 1: Open Excel and design the look and feel. Repeat the formatting for the 3 rows below row 5. Remember to format the cells according to the data that will be in them.
Step 2: Save as XML! (File-Save As-Other Formats-->XML Spreadsheet)
Step 3: Convert this XML to XSL using your editor of choice (Or whatever you have!). I named my file PrintList2.xsl and loaded it to a document library called PrintTemplates.
First You have to Remove the XML declaration and change it to XSL stylesheet.
- <xsl:stylesheet
- xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
- xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882"
- xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
- xmlns:rs="urn:schemas-microsoft-com:rowset"
- xmlns:z="#RowsetSchema" version="1.0">
- <xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />
- <xsl:template match="/">
Then you can add/remove the needed XSL inside the xsl:template tag!
[I have removed all of the styles for brevity]
The DocumentProperties and ExcelWorkbook sections are fairly straightforward and are added automatically based on your selections.
- <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
- xmlns:o="urn:schemas-microsoft-com:office:office"
- xmlns:x="urn:schemas-microsoft-com:office:excel"
- xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
- xmlns:html="http://www.w3.org/TR/REC-html40">
- <DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">
- <Author>Daniel Walker</Author>
- <LastAuthor>Dan</LastAuthor>
- <LastPrinted>2010-06-18T10:19:06Z</LastPrinted>
- <Created>2010-06-18T09:26:14Z</Created>
- <LastSaved>2010-12-28T16:32:41Z</LastSaved>
- <Company>My Company</Company>
- <Version>12.00</Version>
- </DocumentProperties>
- <ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">
- <WindowHeight>11640</WindowHeight>
- <WindowWidth>17400</WindowWidth>
- <WindowTopX>120</WindowTopX>
- <WindowTopY>45</WindowTopY>
- <ProtectStructure>False</ProtectStructure>
- <ProtectWindows>False</ProtectWindows>
- </ExcelWorkbook>
The next section is the WorkSheet section and it contains most of the meat of the xml and xsl code
- <Worksheet ss:Name="Sheet1">
- <Names>
- <NamedRange ss:Name="Print_Titles" ss:RefersTo="=Sheet1!R1:R5"/>
- </Names>
- <Table ss:ExpandedRowCount="60000" x:FullColumns="1" x:FullRows="1">
- <Column ss:StyleID="s62" ss:AutoFitWidth="0" ss:Width="61.5" ss:Span="3"></Column>
- <Column ss:Index="5" ss:StyleID="s62" ss:AutoFitWidth="0" ss:Width="56.25"></Column>
- <Column ss:StyleID="s62" ss:Width="49.5" ss:Span="1"></Column>
- <Column ss:Index="8" ss:StyleID="s62" ss:AutoFitWidth="0" ss:Width="66.75"></Column>
- <Column ss:StyleID="s62" ss:AutoFitWidth="0" ss:Width="57"></Column>
- <Column ss:StyleID="s62" ss:Width="40.5"></Column>
- <Column ss:AutoFitWidth="0" ss:Width="9"></Column>
- <Column ss:AutoFitWidth="0" ss:Width="187.5" ss:Span="242"></Column>
- <Row ss:AutoFitHeight="0" ss:Height="8.25">
- <Cell ss:MergeAcross="9" ss:MergeDown="1" ss:StyleID="m65201024">
- <Data ss:Type="String">Commitments - All Tasks</Data>
- <NamedCell ss:Name="Print_Titles"/>
- </Cell>
- </Row>
- <Row ss:AutoFitHeight="0" ss:Height="8.25"></Row>
- <Row ss:AutoFitHeight="0" ss:Height="13.5">
- <Cell ss:MergeAcross="1" ss:StyleID="s266">
- <Data ss:Type="String">Priority</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- <Cell ss:StyleID="s268">
- <Data ss:Type="String">Division</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- <Cell ss:StyleID="s268">
- <Data ss:Type="String">Branch</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- <Cell ss:MergeAcross="2" ss:StyleID="s268">
- <Data ss:Type="String">Status</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- <Cell ss:StyleID="s268">
- <Data ss:Type="String">Serial #</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- <Cell ss:StyleID="s268">
- <Data ss:Type="String">ECD</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- <Cell ss:MergeDown="2" ss:StyleID="m54658428">
- <Data ss:Type="String">Wkg Days</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- </Row>
- <Row>
- <Cell ss:MergeAcross="6" ss:StyleID="m57148000">
- <Data ss:Type="String">Title</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- <Cell ss:StyleID="s195">
- <Data ss:Type="String">Fnd #</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- <Cell ss:StyleID="s195">
- <Data ss:Type="String">Rev ECD</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- </Row>
- <Row ss:Height="13.5">
- <Cell ss:MergeAcross="6" ss:StyleID="m57148020">
- <Data ss:Type="String">Description</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- <Cell ss:StyleID="s234">
- <Data ss:Type="String">Cmt #</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- <Cell ss:StyleID="s235">
- <Data ss:Type="String">Created</Data>
- <NamedCell ss:Name="Print_Titles"></NamedCell>
- </Cell>
- </Row>
This is the main header formatted the way we need. Most of the formatting options are declared later. This will produce just the first part of the spreadsheet as shown in the following image:
Now we need to setup the data portion of the template. When the page is called, it is going to be used to format XML data. We will need to loop through this data with the xsl:for-each command. As this report carries one list item over 3 rows we have to set that up correctly. Also, note that due to the way the data is returned from a webservice query, it might need to be tweaked to look correct. An example would be the need for the substring commands for the ows_Body. This data has opening and closing div tags because it is a note field.
- <xsl:for-each select="listitems/rs:data/z:row">
- <Row>
- <Cell ss:MergeAcross="1" ss:StyleID="s271">
- <Data ss:Type="String">
- <xsl:value-of select="substring-after(@ows_Priority, '#')"/>
- </Data>
- </Cell>
- <Cell ss:StyleID="s272">
- <Data ss:Type="String">
- <xsl:value-of select="@ows_AssignedDivision"/>
- </Data>
- </Cell>
- <Cell ss:StyleID="s272">
- <Data ss:Type="String">
- <xsl:value-of select="@ows_AssignedBranch"/>
- </Data>
- </Cell>
- <Cell ss:MergeAcross="2" ss:StyleID="s272">
- <Data ss:Type="String">
- <xsl:value-of select="@ows_Status"/>
- </Data>
- </Cell>
- <Cell ss:StyleID="s272">
- <Data ss:Type="String">
- <xsl:value-of select="@ows_Serial"/>
- </Data>
- </Cell>
- <Cell ss:StyleID="s139">
- <Data ss:Type="DateTime">
- <xsl:value-of select="substring-before(@ows_DueDate, ' ')"/>
- </Data>
- </Cell>
- <Cell ss:MergeDown="2" ss:StyleID="m65201084">
- <Data ss:Type="Number">
- <xsl:value-of select="@ows_WorkingDays"/>
- </Data>
- </Cell>
- </Row>
- <Row>
- <Cell ss:MergeAcross="6" ss:StyleID="s276">
- <Data ss:Type="String">
- <xsl:value-of select="@ows_Title"/>
- </Data>
- </Cell>
- <Cell ss:StyleID="s274">
- <Data ss:Type="String">
- <xsl:value-of select="@ows_FindingNum"/>
- </Data>
- </Cell>
- <Cell ss:StyleID="s140">
- <xsl:choose>
- <xsl:when test="@ows_RECD !=''">
- <Data ss:Type="DateTime">
- <xsl:value-of select="substring-before(@ows_RECD, ' ')"/>
- </Data>
- </xsl:when>
- </xsl:choose>
- </Cell>
- </Row>
- <Row ss:Height="13.5">
- <Cell ss:MergeAcross="6" ss:StyleID="s277">
- <Data ss:Type="String">
- <xsl:value-of select="substring-before(substring-after(@ows_Body, '<div>'), '</div>')"/>
- </Data>
- </Cell>
- <Cell ss:StyleID="s275">
- <Data ss:Type="String">
- <xsl:value-of select="@ows_CommitmentNum"/>
- </Data>
- </Cell>
- <Cell ss:StyleID="s141">
- <Data ss:Type="DateTime">
- <xsl:value-of select="substring-before(@ows_Created, ' ')"/>
- </Data>
- </Cell>
- </Row>
- </xsl:for-each>
- </Table>
Now we have looped through the data and output the results. The rest of the code handles the formatting including conditional header and the conditional formatting. The conditional formatting has code to calculate the range of cells it should affect as well!
Note again that this XSL file was uploaded to a document library called PrintTemplates
- <WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
- <PageSetup>
- <Layout x:Orientation="Landscape" x:CenterHorizontal="1"/>
- <xsl:variable name="MarkRequired" select="count(listitems/rs:data/z:row[@ows_HandleAs = 'SPECIAL 1'])" />
- <xsl:choose>
- <xsl:when test="$MarkRequired >= 1">
- <Header x:Margin="0.3" x:Data="&C&"Arial,Bold"&8SPECIAL: This document is special!"/>
- </xsl:when>
- </xsl:choose>
- <Footer x:Margin="0.3" x:Data="&L&"Arial,Bold"CTS Commitments View&C&"Arial,Bold"&D&R&"Arial,Bold"&P of &N"/>
- <PageMargins x:Bottom="0.75" x:Left="0.25" x:Right="0.25" x:Top="0.75"/>
- </PageSetup>
- <Print>
- <ValidPrinterInfo/>
- <Scale>88</Scale>
- <HorizontalResolution>600</HorizontalResolution>
- <VerticalResolution>600</VerticalResolution>
- </Print>
- <PageBreakZoom>60</PageBreakZoom>
- <Selected/>
- <FreezePanes/>
- <FrozenNoSplit/>
- <SplitHorizontal>5</SplitHorizontal>
- <TopRowBottomPane>5</TopRowBottomPane>
- <ActivePane>2</ActivePane>
- <Panes>
- <Pane>
- <Number>3</Number>
- </Pane>
- <Pane>
- <Number>2</Number>
- <ActiveRow>11</ActiveRow>
- <ActiveCol>1</ActiveCol>
- </Pane>
- </Panes>
- <ProtectObjects>False</ProtectObjects>
- <ProtectScenarios>False</ProtectScenarios>
- </WorksheetOptions>
- <ConditionalFormatting xmlns="urn:schemas-microsoft-com:office:excel">
- <xsl:variable name="ItemCount" select="count(listitems/rs:data/z:row)" />
- <xsl:variable name="Range1" select="$ItemCount * 3"/>
- <xsl:variable name="Range2" select="$Range1 + 5" />
- <Range>R6C10:R<xsl:value-of select="$Range2"/>C10</Range>
- <Condition>
- <Qualifier>GreaterOrEqual</Qualifier>
- <Value1>6</Value1>
- <Format Style='color:black;font-weight:700;background:red'/>
- </Condition>
- <Condition>
- <Qualifier>Between</Qualifier>
- <Value1>0</Value1>
- <Value2>5</Value2>
- <Format Style='color:black;font-weight:700;background:#FC6E04'/>
- </Condition>
- <Condition>
- <Qualifier>LessOrEqual</Qualifier>
- <Value1>0</Value1>
- <Format Style='color:black;font-weight:700;background:#00CC00'/>
- </Condition>
- </ConditionalFormatting>
- </Worksheet>
- </Workbook>
- </xsl:template>
- </xsl:stylesheet>
BRING ON THE DATA!!
So the next thing we need to do is to get the data and automate Excel. This is all done in javascript and jQuery and I had placed this code in the master page. I also chose to make it an action from a list view, but you can choose whatever method you want.
Excel Automation Using Javascript
- <script language="javascript" type="text/javascript">
- var viewpage = null;
- var test = null;
- $(document).ready(function() {
- viewname = $("td[id*='onetViewSelector']").text();
- test = new String(window.location);
- createPrintMenuItems()
- });
- function createPrintMenuItems() {
- // Attach to the Actions menu
- if (test.indexOf("Lists/Commitments") > 0) {
- var oNodes = document.getElementsByTagName("a");
- for (var c = 0; c < oNodes.length; c++) {
- if (oNodes[c].id.indexOf("_ListActionsMenu") > -1) {
- var oTokens = MMU_ParseNVAttr(oNodes[c], "menuTokenValues");
- var sMenuID = MHash_Item(oTokens, "TEMPLATECLIENTID");
- var oMenu = document.getElementById(sMenuID);
- oMenu.innerHTML = "<ie:menuitem description='Prints the current view' id='printListMenuItem' type='option' onMenuClick='javascript:printList()' iconSrc='/_layouts/images/WPICON.GIF' text='Print View' menuGroupId='200'></ie:menuitem>" + oMenu.innerHTML;
- return;
- }
- }
- setTimeout("createPrintMenuItems()", 500);
- }
- }
So now let's call the printlist function that does most of the real work here. I tip my hat to Marc Anderson's SPServices library for making this a lot easier. It can be found here
- function printList() {
- var camlQuery;
- var xmldata1 = new ActiveXObject("MSXML.DomDocument");
- xmldata1.async = false;
- var xmldata2 = new ActiveXObject("MSXML.DomDocument");
- xmldata2.async = false;
- var xsldata1 = new ActiveXObject("MSXML.DomDocument");
- xsldata1.async = false;
- xsldata1.load(L_Menu_BaseUrl + "/PrintTemplates/PrintList2.xsl")
- var zpath = "c:\\tmpdata1.xml";
- if (viewname != "All") {
- camlQuery = "<Query><Where><Eq><FieldRef Name='AssignedDepartment'/><Value Type='Text'>" + viewname + "</Value></Eq></Where><OrderBy><FieldRef Name='ID' /></OrderBy></Query>";
- }
- else {
- camlQuery = "<Query><OrderBy><FieldRef Name='ID' /></OrderBy></Query>";
- }
- var camlViewFields = "<ViewFields><FieldRef Name='Priority' />";
- camlViewFields += "<FieldRef Name='Priority' />";
- camlViewFields += "<FieldRef Name='AssignedDivision' />";
- camlViewFields += "<FieldRef Name='AssignedBranch' />";
- camlViewFields += "<FieldRef Name='Title' />";
- camlViewFields += "<FieldRef Name='Serial' />";
- camlViewFields += "<FieldRef Name='FindingNum' />";
- camlViewFields += "<FieldRef Name='CommitmentNum' />";
- camlViewFields += "<FieldRef Name='DueDate' />";
- camlViewFields += "<FieldRef Name='RECD' />";
- camlViewFields += "<FieldRef Name='Status' />";
- camlViewFields += "<FieldRef Name='WorkingDays' />";
- camlViewFields += "</ViewFields>";
- $().SPServices({
- operation: "GetListItems",
- async: false,
- webURL: L_Menu_BaseUrl,
- listName: "Commitments",
- CAMLQuery: camlQuery,
- CAMLViewFields: camlViewFields,
- completefunc: function(xData, Status) {
- alert(xData.responseXML.xml);
- var rtz = elementContents(xData.responseXML.xml, "GetListItemsResult");
- try {
- xmldata1.loadXML(rtz);
- }
- catch (e) { }
- xmldata1.transformNodeToObject(xsldata1, xmldata2);
- var fs = new ActiveXObject("Scripting.FileSystemobject");
- var f = fs.OpenTextFile(zpath, 2, true, 0);
- var output = "<?xml version='1.0'?><?mso-application progid='Excel.Sheet'?>" + xmldata2.xml;
- f.write(output);
- f.close();
- var oxl = new ActiveXObject("Excel.Application");
- oxl.Visible = true; // set to false
- var owb = oxl.Workbooks.Open(zpath);
- oxl.ActiveSheet.Cells.Select;
- oxl.Selection.EntireColumn.Autofit;
- //oxl.ActiveSheet.PrintOut;
- //owb.Close(false);
- //oxl.Quit;
- } // end completefunc
- }); // end spservices
- }
- function elementContents(text, tagName) {
- var sTag = "<" + tagName + ">";
- var eTag = "</" + tagName + ">";
- var sTagIndex = text.indexOf(sTag);
- var eTagIndex = text.indexOf(eTag, sTagIndex + sTag.length);
- if ((sTagIndex >= 0) && (eTagIndex > sTagIndex)) return text.substring(sTagIndex + sTag.length, eTagIndex);
- return null;
- }
- </script>
So, this code does the rest! In this case, I allowed for different CAML query based on the viewname variable which is set based on the current view for this list. In this case, it is based on the department the commitments are assigned to. I commented out the last 3 lines of the function that force the print out just to be sure it looks like what it should. This technique allowed me to create a header based on certain content and I can repeat the header fields on subsequent pages automatically too. This same approach could be used for single item printouts in a special layout as well. To see the results, on the list view I would go to Actions-> Print View (Or whatever you call it in the javascript code)
Resulting in the following Excel output.
There it is folks! Just print the spreadsheet and it will retain all the formatting you did on the original. As I said, it is very versatile for what we are doing and we can do a lot of stuff with it. Feel free to ask whatever questions you may have!!
Hmmm... Working through this and got this error:
ReplyDeleteMessage: Automation server can't create object
The error is happening on line 44 of your printList() code:
xmldata1.transformNodeToObject(xsldata1, xmldata2);
I'm not very familiar with working with files in Javascript. Where should I start looking for the issue?
Blessings,
Jim Bob
So sorry to miss this comment as it appears that I never got an email on it!! (evilgenius at cox dot net) This code works in IE only as far as I know so far, but this can happen if your stylesheet is not formed well. Not sure what else would cause this assuming it is loading the file into the xsldata1 object.
ReplyDeleteI am working on an update to this post that might make this easier!
ReplyDeleteWould love to see the update.
ReplyDeleteAnother issue I ran into with this solution is the need to turn on ActiveX at every client.
Oh, I am testing it with IE. :)
Still working through some options, so I look forward to seeing what you come up with, or even working on a solution together.
Blessings,
Jim Bob
jimbobhoward at that mail service by Google.
One update that I am working on is directly filling in the data and also autosaving to a document library. I am working on it!!!
ReplyDeleteCool. Looking forward to that update!
ReplyDelete