Introduction
Ever tried to print a data grid in ASP.NET?
If you have you would have come up with the problem of the data filtering over
onto the next page. The result of which are cut-off words and a grid on
the next page without any column headers.
When I came up with this problem I had to find a solution that would give me
the flexibility to determine exactly what went on each printed page.
I haven't provided the source code to this article simply because the code is
too tailored to my particular application and would be confusing. But
bear with the article because what I have done is try provide you with the
basic framework for implementing your own solution to this problem.
Background
The solution uses Microsoft's print templates and therefore can only work with
IE 5.5 or above. As far as I know this is the only supported
environment. If this satisfies your requirements then read on...
Detail
The key to the solution is an Active-X control that lives on the ASP.NET
page. At the time of writing this article the Active-X is written in C++
but I would like to know if anyone manages to get it to work in C#. The
Active-X is instantiated on the ASP.NET page and the parameters are set.
The parameters that are needed are as follows:
-
ServerAddress - the name of the computer hosting the ASP.NET pages e.g.
server1;
-
XML - the data file e.g. data.xml;
-
Template - the print template file e.g. template.html;
Also on the ASP.NET page are two server variables and a div for the Active-X
<div id="printTemplateObject"></div>
<div id="svServer" style="VISIBILITY: hidden">
<%= Request.ServerVariables["SERVER_NAME"] %></div>
<div id="svURL" style="VISIBILITY: hidden">
<%= Request.ServerVariables["URL"] %></div>
In the onload
event we create our Active-X object and pass in the relevant parameters
function CreateObject()
{
var server = document.all("svServer");
var url = document.all("svURL");
var oText = "";
var position = url.innerText.lastIndexOf("webApplication");
var location = url.innerText.substr(0, position + 14);
location = "http://" + server.innerText + location + "PrintTemplate.dll";
var printTO = document.all("printTemplateObject");
if(printTO != null)
{
oText += "<OBJECT id=\"printTemplateWindow\" ";
oText += "style=\"Z-INDEX: 120; LEFT: 0px; WIDTH: 100%;" +
" POSITION: absolute; TOP: 800px; HEIGHT: 102px\" ";
oText += "codebase=\"";
oText += location;
oText += "\"";
oText += "classid=\"clsid:CDF583D3-041E-4D1A-AB51-19CE1D3A56A3\" ";
oText += "name=\"printTemplateWindow\" ";
oText += "VIEWASTEXT>";
oText += "<PARAM NAME=\"ServerAddress\" VALUE=\"//";
oText += server.innerText;
oText += "/";
oText += url.innerText;
oText += "\">";
oText += "<PARAM NAME=\"XML\" VALUE=\"";
oText += "data.xml";
oText += "\">";
oText += "<PARAM NAME=\"Template\" VALUE=\"";
oText += "template.html";
oText += "\">";
oText += "</OBJECT>";
printTO.innerHTML = oText;
}
}
The Active-X control has its own user interface and this is why I have set the
width and height of the control on the ASP.NET form. The user interface
of the Active-X has 2 buttons, where one button directly prints and the
other button brings up a print preview window. Example code for the
user interface is shown below:
<HTML>
<BODY id=theBody>
<TABLE width="90%" align="center" ID="Table1">
<TR>
<TD align="center">
<BUTTON style="width: 50mm; height: 10mm;"
onclick='navigate("##print##")' ID="Button1">Print</BUTTON>
</TD>
<TD align="center">
<BUTTON style="width: 50mm; height: 10mm;"
onclick='navigate("##preview##")' ID="Button2">Print Preview</BUTTON>
</TD>
</TR>
</TABLE>
</BODY>
</HTML>
Each button has a special onclick
event that navigates to a bogus address. The Active-X then captures the
OnBeforeNavigate2 message and does its magic.
void CPrintTemplates::OnBeforeNavigate2(
IDispatch* pDisp, VARIANT* URL, VARIANT* Flags,
VARIANT* TargetFrameName,
VARIANT* PostData, VARIANT* Headers, VARIANT_BOOL* Cancel )
{
CString strURL(URL->bstrVal);
IOleCommandTarget *pCmdTarg;
m_spBrowser->QueryInterface(IID_IOleCommandTarget, (void **)&pCmdTarg);
VARIANT vTemplatePath;
V_VT(&vTemplatePath) = VT_BSTR;
CString path = "http:";
path += m_bstrServerAddress.Copy();
path += m_bstrTemplate.Copy();
path += "?";
path += m_bstrXML.Copy();
V_BSTR(&vTemplatePath) = path.AllocSysString();
if(strURL.Left(9) == "##print##")
{
HRESULT hr = pCmdTarg->Exec(&CGID_MSHTML, IDM_PRINT,
OLECMDEXECOPT_PROMPTUSER, &vTemplatePath, NULL);
*Cancel = true;
return;
}
else if(strURL.Left(11) == "##preview##")
{
HRESULT hr = pCmdTarg->Exec(&CGID_MSHTML, IDM_PRINTPREVIEW,
OLECMDEXECOPT_PROMPTUSER, &vTemplatePath, NULL);
*Cancel = true;
return;
}
What this essentially does is build up the path to the template file (that
lives on the server) and adds the data file as a parameter
E.g. http:\\server1\webAplication1\template.html?data.xml
It then calls back to the browser to either print or print preview the template.
Print Templates
The Microsoft Print Templates example application that I linked too at the top
of this article does much the same but in a stand-alone application
sense. The example allows you to specify a template file and press the
'Print Preview' button. Try selecting the Template7.htm - Building
a User Interface' option and press the 'Print Preview' button.
Take a look at the source of template7 and you'll see the main body of the HTML
page at the bottom. Above it is the JavaScript that handles the printing
side of things. The great thing about this is that as it runs client side
we have access to the current printer page size, margins, etc...
Now this doesn't solve my problem of printing data grids as the grid will still
spill over onto the next page. If you look at the file template.html you
should see that the main difference is in the Init()
function.
function Init()
{
printingStarted = false;
if(zoomcontainer != null)
{
zoomcontainer.innerText = "";
}
xmlObj = new ActiveXObject('MSXML2.DOMDocument');
xmlObj.async = false;
var templateLocation = document.URL.substr(
0, document.URL.indexOf("?"));
var position = templateLocation.lastIndexOf("webApplication1");
var location = templateLocation.substr(0, position + 8);
reportFile = location + "/" +
document.URL.substr(document.URL.indexOf("?") + 1,
document.URL.length);
if(xmlObj.load(reportFile) == false)
{
alert("Failed to load document: " +
document.URL.substr(document.URL.indexOf("?") + 1,
document.URL.length));
return;
}
zoomcontainer.style.zoom = "50%";
ui.style.width = document.body.clientWidth;
ui.style.height = "50px";
pagecontainer.style.height =
document.body.clientHeight - ui.style.pixelHeight;
InitClasses();
file = GenerateHTMLPages(xmlObj,
(printer.pageWidth - (printer.marginLeft + printer.marginRight))/100,
(printer.pageHeight - (printer.marginTop + printer.marginBottom))/100);
AddFirstPage(file);
pages = Pages();
}
Firstly, the data.xml file is loaded (based upon the URL). Then the
function GenerateHTMLPages
(stored in a seperate JavaScript file)
is called which compiles the data.xml file into separate HTML files for each
printed page (more on this later).
The final stage adds the first page and then relies on the OnRectComplete
message coming in. When it does we call AddNewPage
to add in
the new page. This occurs for each added page until all of the pages
have been added.
If you look at the Microsoft example template file you'll see that the OnRectComplete
messages checks to see if the event is due to contentOverflow
.
It is this that causes the overflowing of the data grid onto the next page and
so on. As we have already generated our HTML pages we don't need to
handle this case.
The function GenerateHTMLPages
is passed the data file and the
available printable area. At this point it really up to you how to
implement this.
What I did is navigate through my data file and determine how many rows I
wanted to fit on a page based upon a nominal figure of 20mm height for each
row. With this I can now generate a totally new HTML page for
each printed page and put what I want on it and then save it as a temporary
file (shown below). The AddNewPage
function handles the
picking up of each page automatically based on the page number E.g.
output1.HTML, output2.HTML, etc...
function Save(file, pageNumber, htmlText)
{
file = file + pageNumber;
file = file + ".html"
var fso = new ActiveXObject("Scripting.FileSystemObject");
var folder = fso.GetSpecialFolder(2);
file = folder + "/" + file;
var a = fso.CreateTextFile(file, true);
a.WriteLine(htmlText);
a.Close();
return file;
}
When it came to Headers and footers I did much the same. I simply wrote a
method that returns a HTML string that contains the header and footer
information. The print template takes care of the rest of it
function AddHeaderAndFooterToPage(pageNum)
{
newHeader = "<DIV CLASS='headerstyle'>" +
GenerateHeaderFooterHTML(pageNum, GetHeaderFields(xmlObj)) + "</DIV>";
newFooter = "<DIV CLASS='footerstyle'>" +
GenerateHeaderFooterHTML(pageNum, GetFooterFields(xmlObj)) + "</DIV>";
if(document != null)
{
if(document.all("page" + pageNum) != null)
{
document.all("page" +
pageNum).insertAdjacentHTML("afterBegin", newHeader);
document.all("page" +
pageNum).insertAdjacentHTML("beforeEnd", newFooter);
}
}
}
And that's it. Hopefully I haven't missed anything out in terms of the flow
of information but as you have probably realised by now there is still alot
more for you; the developer; to do in order to get this working on you own
system. From my point of view I hope that this serves as a first step for
you.
This article talks mainly about printing data grids but this is not the end of
it. With control of each printed page it is up to you what gets printed.
For reference, a great tutorial on Print Templates is available
here.
Points of Interest
Whilst this (in my opinion) is an incredible convoluted route to do in
essence something which seems very simple, it is the only way that I have
found that gives me the flexibility of client-side customized page-by-page
printing. I have certainly learnt a great deal with this code and I hope
more that I didn't completely miss the boat on a simpler solution.
History
- Version 1 : 27th July 2004 - Original