Welcome to MSDN Blogs Sign in | Join | Help

Correcting the printed margin calculation of the Windows Forms ReportViewer control

[Update 02/04/2008 - It has recently been brought to my attention that this issue has been corrected with the ReportViewer control shipping with Visual Studio 2008.] 

 

In certain situations when printing a report from the Windows Forms ReportViewer control, the printed report's margins are different from the margins specified in the report.  This seems to vary from printer to printer, but as a general rule of thumb, if the top margin or left margin are specified to be less than 0.5" then the printed output will appear to have larger-than-specified margins.  Until such time as this issue is corrected, there are two supported workarounds: either export the report to an intermediate format (such as PDF) and print that, or override the print functionality of the Windows Forms ReportViewer control.  Yet another possibility would be to host a web browser control and use the Web Forms ReportViewer control.

 

The first workaround is obvious, but the second is a little less straightforward, so I will walk through all of the code necessary to provide your own printing logic for the control.  Note that the code differs slightly depending on whether you are using a ServerReport or a LocalReport in your ReportViewer.  I will first present the common elements and then explain the differences between LocalReport processing and ServerReport processing.

 

To begin with, start with the MSDN Walkthrough: Printing a Local Report without Preview article.  This is a console application, so you will most likely want to modify it to a class that you can use with your application.  Your first step is to add a handler for the ReportViewer control's Print event, similar to the following:

 

        public void HandlePrint(object sender, CancelEventArgs e)

        {

            Run();

            e.Cancel = true;

        }

 

The Run method invokes the custom printing code from the article, and by setting e.Cancel to true we prevent the internal ReportViewer printing mechanism from firing.

 

Because we're replacing the print functionality of the ReportViewer, we probably don't want the print to occur silently.  As a result, you can make changes similar to the following to display a print dialog:

 

        private void Print(PrinterSettings printerSettings)

        {

            if (m_streams == null || m_streams.Count == 0)

                return;

 

            PrintDocument printDoc = new PrintDocument();

            printDoc.PrinterSettings = printerSettings;

            printDoc.PrintPage += new PrintPageEventHandler(PrintPage);

            printDoc.Print();

        }

 

        private void Run()

        {

            PrintDialog printDialog = new PrintDialog();

            if (printDialog.ShowDialog() == DialogResult.OK)

            {

                Export(m_viewer.ServerReport);

 

                m_currentPageIndex = 0;

                Print(printDialog.PrinterSettings);

            }

        }

 

This brings us to the crux of the issue, correcting the margin offset:

 

        private void PrintPage(object sender, PrintPageEventArgs ev)

        {

            Metafile pageImage = new

               Metafile(m_streams[m_currentPageIndex]);

 

            // Note: Coordinate (0,0) does not coincide with the top left corner of

            // the page; it coincides with the top left corner of the printable area

            // of the page. To account for this we have to subtract the hard margin.

            RectangleF adjustedRect = new RectangleF(

                ev.PageBounds.Left - ev.PageSettings.HardMarginX,

                ev.PageBounds.Top - ev.PageSettings.HardMarginY,

                ev.PageBounds.Width, ev.PageBounds.Height);

 

            ev.Graphics.DrawImage(pageImage, adjustedRect);                       

            m_currentPageIndex++;

            ev.HasMorePages = (m_currentPageIndex < m_streams.Count);

        }

 

So ends the common functionality to both the ServerReport and LocalReport cases.  In the LocalReport case, you are actually done here; the rendering code in the walkthrough sample is all that you need.  However, in the case of the ServerReport, the Render method has no overload that accepts a CreateStreamCallback delegate, so you have to do the heavy lifting yourself.  There are two basic approaches: call the Render method, get the array of stream identifiers and process each with a call to the RenderStream method.  This works, but it is much slower than the other option, which is to utilize the rs:PersistStreams option of the ReportServer web service by accessing the ReportServer web service via URL access.  As an example, the updated Export method might look like the following:

 

        private void Export(ServerReport report)

        {

            string deviceInfo =

              "<DeviceInfo>" +

              "  <OutputFormat>EMF</OutputFormat>" +

              "  <PageWidth>8.5in</PageWidth>" +

              "  <PageHeight>11in</PageHeight>" +

              "  <MarginTop>0.25in</MarginTop>" +

              "  <MarginLeft>0.25in</MarginLeft>" +

              "  <MarginRight>0.25in</MarginRight>" +

              "  <MarginBottom>0.25in</MarginBottom>" +

              "</DeviceInfo>";

 

            m_streams = new List<Stream>();

 

            string mimeType;

            string extension;

 

            System.Collections.Specialized.NameValueCollection urlAccessParameters = new System.Collections.Specialized.NameValueCollection();

            urlAccessParameters.Add("rs:PersistStreams", "True");

 

            Stream reportStream =

                report.Render("Image", deviceInfo, urlAccessParameters, out mimeType, out extension);

 

            m_streams.Add(reportStream);

            urlAccessParameters.Remove("rs:PersistStreams");

            urlAccessParameters.Add("rs:GetNextStream", "True");

 

            while (reportStream.Length != 0)

            {

                reportStream =

                    report.Render("Image", deviceInfo, urlAccessParameters, out mimeType, out extension);             

                m_streams.Add(reportStream);

            }

 

            m_streams.RemoveAt(m_streams.Count - 1);

 

            foreach (Stream stream in m_streams)

                stream.Position = 0;

        }

 

Note that this code has hard coded the page dimensions and margin information; you would likely want to parameterize this in the real world, and undoubtedly there are other changes you would want to consider to overcome some of the naive aspects of the sample code displayed here.

 

Posted by mattweber | 1 Comments
Filed under:

MSXML does not adhere to the W3C Recommendation for attribute value normalization

The W3C Recommendation for XML 1.0 and XML 1.1 contain the following language with regard to attribute value normalization:

"If the attribute type is not CDATA, then the XML processor MUST further process the normalized attribute value by discarding any leading and trailing space (#x20) characters, and by replacing sequences of space (#x20) characters by a single space (#x20) character. ... All attributes for which no declaration has been read SHOULD be treated by a non-validating processor as if declared CDATA."

What this means is that the following element should be treated by the parser as having an attribute with a value containing a leading space:

<foo bar=" baz">

However, by default, the MSXML parser treats this element as if it were so declared:

<foo bar="baz">

Obviously, this causes problems when " baz" and "baz" should be treated as different values in XPATH queries and elsewhere. The following code fails to locate our element, assuming our element is declared within a document named document.xml:

MSXML2::IXMLDOMDocumentPtr pDocument;
pDocument.CreateInstance(__uuidof(MSXML2::DOMDocument));
pDocument->load(_T("document.xml"));
// This query will fail:
BSTR bstrQuery = SysAllocString(OLESTR("//foo[@bar=\" baz\"]"));
// While this one would succeed:
// BSTR bstrQuery = SysAllocString(OLESTR("//foo[@bar=\"baz\"]"));
MSXML2::IXMLDOMNodePtr pNode = pDocument->selectSingleNode(bstrQuery); // pNode = NULL
SysFreeString(bstrQuery);

Proper handling of attribute value normalization, among other non-W3C-Recommendation-compliant issues, was addressed beginning with MSXML4. In order to prevent breaking legacy code that relied on the old parser's incorrect behavior, a new parser was introduced. This parser has to be explicitly turned on via the setProperty() function that was added with MSXML4. The following code demonstrates utilizing the new parser to obtain the behavior we expect:

// Use IXMLDOMDocument2Ptr to get the setProperty/getProperty functionality
MSXML2::IXMLDOMDocumentPtr2 pDocument;
// IXMLDOMDocument2Ptr only operates on DOMDocument40 (or later)
pDocument.CreateInstance(__uuidof(MSXML2::DOMDocument40));

// Turn on the new parser in MSXML6; this must be done prior to loading the document
_variant_t varNewParser;
BSTR propName = SysAllocString(OLESTR("NewParser"));
varNewParser.vt = VT_BOOL;
varNewParser.boolVal = VARIANT_TRUE;
pDebugDoc->setProperty(propName, true);
SysFreeString(propName);

pDocument->load(_T("document.xml"));
BSTR bstrQuery = SysAllocString(OLESTR("//foo[@bar=\" baz\"]"));
MSXML2::IXMLDOMNodePtr pNode = pDocument->selectSingleNode(bstrQuery); // pNode != NULL
SysFreeString(bstrQuery);
Posted by mattweber | 1 Comments
Filed under:
 
Page view tracker