CodeBetter.Com
CodeBetter.Com
RSS 2.0 via Feedburner
           Do you Twitter? Follow us @CodeBetter

Brendan Tompkins [MVP]

Blog First. Ask Questions Later.

May 2006 - Posts

  • WYSIWYG Web Printing with PDF & Response Filters

    By far the #1 complaint I hear from users of various web applications is that printing a page is not WYSIWYG.  It’s really a simple problem that the industry has sort of swept under the rug for most of the history of the Internet.  Browser printing in the current versions of IE and Firefox is simply horrible, and while server technologies like ASP.NET and PHP have gotten better and better at screen presentation with every release, printing simply not kept up pace.  As web developers we spend our time figuring out all the intricacies of browser page rendering, and often are asked to do the same thing for printing.  It’s frankly a frustrating, difficult problem to solve.  IE7 should finally fix many of these problems when it’s released, but what are we supposed to do until then?  And even when it’s released, how many years will we be designing to support the current crop of web browsers?

    Adobe PDF Printing Rocks

    Adobe’s PDF format does have it’s issues, but it also has huge advantages that make it a great solution for creating printable website documents. 1) Nearly all your users already have the free reader software and 2) Printing is nearly 100% WYSIWYG.  So what’s the solution to your website printing problems at least until you can guarantee that the majority of your users will be using a print-friendly browser?  Simple, when a user makes a print request on your site, dynamically create PDF documents on the fly making sure to include all of your users page state, and output that PDF document to the browser.  Simple right? Well, it’s actually easier said than done.  You have two big issues when attempting to solve this problem: Finding a decent PDF writing component to deploy on your webservers, and being able to accurately render a html page to this component while at the same time preserving your user’s page state.

    Finding a Decent PDF Component

    In order to create a PDF document on the fly, I wanted a PDF component that would take a URL and create a document from the resulting page.  There are plenty of resources out there for you to use that will help you find the right PDF component, so I won’t list them here.  I settled on webSuperGoo’s ABCPdf component.  I liked it’s simplicity – you can grab any page and turn it into a PDF, including JavaScript rendered elements using the following code:

    Doc theDoc = new Doc();

    theDoc.HtmlOptions.PageCacheEnabled = false;

    theDoc.HtmlOptions.PageCacheClear();

     

    theDoc.Page = theDoc.AddPage();

     

    int theID;

    theDoc.HtmlOptions.UseScript = true;

    theID = theDoc.AddImageUrl("http://codebetter.com");

     

    while (true)

    {

        theDoc.FrameRect(); // add a black border

        if (!theDoc.Chainable(theID))

            break;

        theDoc.Page = theDoc.AddPage();

        theID = theDoc.AddImageToChain(theID);

    }

     

    for (int i = 1; i <= theDoc.PageCount; i++)

    {

        theDoc.PageNumber = i;

        theDoc.Flatten();

    }

    You can then take the resulting document and stream it back to the browser.  Here’s what the PDF output looks like rendered via the Acrobat reader in IE

     

    Preserving Page State

    A problem you’ll find with this method of generating PDF documents is that while it works well for static HTML pages and other content, it quickly breaks down for anything but the simplest web applications.  The reason?  This PDF component won’t carry the user’s state information such as login status, session and viewstate cache, through the request, therefore, the resulting PDF output may not show things like secure content, grid sorting, or other page content that is dependent on Page State.  You’ll often bump up against this limitation when you’re trying to stream the content of a web response to some other output location, such as a file, database, cache, email or something else entirely.  The solution to this problem is to create a custom ASP.NET Response.Filter.  This will allow you to intercept the response that would normally be sent to the user’s browser, and do all kinds of neat things with it, like turn it into a PDF document.  Here’s a great article from a great site that talks all about these Response.Filter thingeys.

    Putting it all Together

    In order to get this all to work properly, you’ve got to wire it all up.  I’ve found that the following method works best:

    First, create a linkbutton on your master page to allow the user to create a “Printable PDF View” Handle the button click event, and hand off the response to your custom PDF response filter.  Here’s the code Snippet which switches the filter:

        protected void LinkButton1_Click(object sender, EventArgs e)

        {

            // HOW THIS WORKS:

            //    Add a new response filter.  This saves the page output in the database, and redirects to

            //    Pdf/PageToPdf.ashx.  This page creates a PDF object and adds the saved output page via

            //    Pdf/PreRenderedPdfPage.ashx.  This allows the current page state and view to be requestsed

            //    via the PDF object, which normally wouldn't be able to share session, etc.

            Response.Filter = new PdfResponseFilter(Response.Filter);

        }

    In your custom filter, write the output stream temporarially to the cache (or wherever you want to temporarially store it).  It has to be stored somewhere that any web request can access it.  Good candidates are the file system, cache, or the database.  NOTE: This is what is called “Security by Obscurity” and is generally considered not secure enough for any highly sensitive data, such as credit card information.  The issue is that while it may be extremely difficult to guess the GUID and you’d have to make the page request during the milliseconds or so that passes while this rendered HTML waits to be picked up, it is possible that it could be intercepted.  So, don’t do this if you work for a bank or the government . I don’t and find it secure enough for most applications I work with.

        public override void Write(byte[] buffer, int offset, int count)

        {

            string strBuffer = UTF8Encoding.UTF8.GetString(buffer, offset, count);

     

            // ---------------------------------

            // Wait for the closing </html> tag

            // ---------------------------------

            Regex eof = new Regex("</html>", RegexOptions.IgnoreCase);

     

            if (!eof.IsMatch(strBuffer))

            {

                responseHtml.Append(strBuffer);

            }

            else

            {

                responseHtml.Append(strBuffer);

                string finalHtml = responseHtml.ToString();

     

                Guid g = Guid.NewGuid();

     

                HttpContext.Current.Cache[g.ToString()] = finalHtml;

     

                HttpContext.Current.Response.Redirect("PageToPdf.ashx?DocId=" + g.ToString(), false);           

            }

        }

    The PDF component has be issued a one-time ticket (GUID) to pickup the resulting HTML (which may contain secure information) and the Http response is re-directed to an ASHX handler that will create the PDF document.  This handler creates a SuperGoo pdf document, and requests the stored html, passing in the ticket (GUID). 

    public void ProcessRequest(HttpContext context)

        {

     

            Doc theDoc = new Doc();

            theDoc.HtmlOptions.PageCacheEnabled = false;

            theDoc.HtmlOptions.PageCacheClear();

     

            theDoc.Page = theDoc.AddPage();

     

            int theID;

            theDoc.HtmlOptions.UseScript = true;

            theID = theDoc.AddImageUrl("http://localhost/" + context.Request.ApplicationPath + "/PreRenderedPdfPage.ashx?DocId=" + context.Request["DocId"]);

     

            while (true)

            {

                theDoc.FrameRect(); // add a black border

                if (!theDoc.Chainable(theID))

                    break;

                theDoc.Page = theDoc.AddPage();

                theID = theDoc.AddImageToChain(theID);

            }

     

            for (int i = 1; i <= theDoc.PageCount; i++)

            {

                theDoc.PageNumber = i;

                theDoc.Flatten();

            }

     

     

            context.Response.ClearHeaders();

            context.Response.Expires = 0;

            context.Response.Cache.SetCacheability(HttpCacheability.NoCache);

            context.Response.Cache.SetNoServerCaching();

            context.Response.Cache.SetNoStore();

            context.Response.Cache.SetMaxAge(TimeSpan.Zero);

     

            context.Response.AddHeader("content-disposition", "attachement; filename=Doc" + context.Request["DocId"] + ".pdf");

            context.Response.ContentType = "application/pdf";

     

            theDoc.Save(context.Response.OutputStream);

            theDoc.Clear();

     

            context.Response.End();

        }

    A final ASHX handler writes out the stored HTML and removes it from cache.

        public void ProcessRequest (HttpContext context)

        {

            context.Response.Expires = 0;

            context.Response.Cache.SetCacheability(HttpCacheability.NoCache);

            context.Response.Cache.SetNoServerCaching();

            context.Response.Cache.SetNoStore();

            context.Response.Cache.SetMaxAge(TimeSpan.Zero);

     

            if (context.Request["DocId"] != null && context.Cache[context.Request["DocId"]] != null)

            {

                String preRenderedHtml = context.Cache[context.Request["DocId"]].ToString();

     

                // Write out this document.               

                context.Response.Write(preRenderedHtml);

     

                // Delete this so no one can pick up this page by guessing an ID later on.

                context.Cache.Remove(context.Request["DocId"]);

            }

            else

            {             

               context.Response.Write("<HTML><BODY><H1>THIS PAGE HAS EXPIRED.</H1></BODY></HTML>");

            }                   

     

     

            context.Response.End();

        }

    This all allows the exact HTML that would normally be sent back to the browser, to be routed through the PDF component and sent back to the user in PDF resulting in a true WYSIWYG printable page.

    I’ve thrown together a quick sample app that you can download here that demonstrates this entire process.  You’ll have to visit webSuperGoo and download the trial PDF component to get this to work.   It shows you the following page, allows you to set some session and view state information, then allows you to generate a WYSIWYG PDF document.

    Clicking the Printable PDF View link will generate a pdf, with the calendar and other page state and login information saved.

    Good luck, and I hope this can help solve your web printing headaches, it certainly did for me!

    Technorati Tags: , ,
  • Agile HR?

    I was just poking around some of my old blog posts, and was reminded of a series of cartoons I made back in 2004.  I’m not sure if they’re funny or not, it’s one of those blog things that I probably should have just kept inside my head, instead of putting it out there for the world to see.

    There was one cartoon that I think deserves a repost, especially in light of all of the recent talk about Agile here on CodeBetter. I do happen to be a proponent of Agile, but there’s something funny about Agile for Agile’s sake.  I dunno, I guess I just thought it was a funny idea – what if other departments within an organization adopted Agile?



    Technorati Tags: ,

  • An ObjectDataSource/GridView Adapter for Business Entity Collections

    Using ASP.NET 2.0’s new GridView control with the new ObjectDataSource allows you to properly layer your applications.  You can create strongly-typed collections of your business objects, and quickly bind them to the new Grid.  The hype sounds good.  Real good, in fact:

    From Move Over DataGrid, There's a New Grid in Town!

    The GridView control is the successor to the DataGrid and extends it in a number of ways. First, it fully supports data source components and can automatically handle data operations, such as paging, sorting, and editing, provided its bound data source object supports these capabilities.

    The key phrase here is “provided its bound data source object supports these capabilities” When you bind your GridView to an ObjectDataSource wired to a DataSet, DataView or DataTable, you do indeed get automatic sorting, and paging, and you can also filter your data through the ObjectDataSource’s FilterExpression property.  All this adds up to a really useful control, one that you can base your entire web applications around. 

    Business Entities and SOA.

    Okay, great. Well, say you’ve been using Business Entities and Strongly-Typed collections in lieu of DataSets to build your application framework and business layers.  If you’ve been developing this way, you’re probably a convert.  Having a nice, clean framework of entities can be a great foundation for application development.  You also may be using a service oriented pattern to return these entity objects, instead of filling your objects with an instance method of the business entity itself. 

    Both of these patterns are good to follow, but neither work out very well using the ObjectDataSource.  If you’ve done this and you try wiring up these collections to an ObjectDataSource and GridView, you’ll find that a Strongly-Typed Collection doesn’t work with the filtering (setting the ObjectDataSource’s FilterExpression property) and automatic sorting capabilities of the GridView.  The GridView’s current implementation requires that you use a DataTable, DataSet or DataView, unless you want to handle these events yourself and do your sorting on your own.  It will automatically do paging, but other than that you’re on your own.

    It also doesn’t like the service oriented/entity pattern much.  It actually works best when you have smart business objects that are DataTables, DataSets or DataViews and also implement methods which populate the entities.  In Fact, the ObjectDataSource’s control designer won’t complete until you specify which method on your business object is the “Fill” method. This is too bad, because often what we need is to be able to use our entity objects, our service tier methods, and a nice GridView that does the sorting/paging/filtering stuff easily. 

    Since this doesn’t exist currently, the next best thing is to create a DataTable, fill it with your entity collection’s data, and hand this off to the GridView for presentation.  It’s a little strange having to go back to a DataTable after having built a nice clean framework of entities.  Whenever I find myself having to revert back to using DataSets or DataTables, just for the sake of using one control or another, I feel Michael Corleone in Godfather III -  “Just when I thought that I was out they pull me back in.” 

    This is where I really can jibe with Jeffrey’s RAD Kills message.  We have this great RAD grid, but to use it you have to revert to a sometimes poor design choice (DataSets). 

    The EntityDataTable – Best of Both Worlds?

    To solve this problem of allowing my business entity collections to be quickly bind-able to a GridView I whipped together a quick abstract adapter class which I can sub-class that is actually a DataTable. This way, when I bind it to a GridView, it will support all this auto-sorting/filtering stuff.   This class has two methods, GetDataTable which uses reflection and returns a DataTable containing a column for each public property in my entity class.  It also contains an abstract method Fill which can be overridden to connect to your service tier, get your collection, and call the GetDataTable method with your collection and your collection’s contained type.

    using System;

    using System.ComponentModel;

    using System.Data;

    using System.Reflection;

     

    public abstract class EntityDataTable : DataTable

    {

     

        [DataObjectMethod(DataObjectMethodType.Select, true)]

        public abstract DataTable Fill(object parameter);

     

        /// <summary>

        /// Gets the data table.

        /// </summary>

        /// <param name="list">The list.</param>

        /// <returns></returns>

        protected DataTable GetDataTable(System.Collections.IList list, Type typ)

        {

            DataTable dt = new DataTable();

     

            PropertyInfo[] pi = typ.GetProperties();

            foreach (PropertyInfo p in pi)

            {

                dt.Columns.Add(new DataColumn(p.Name, p.PropertyType));

            }

     

            foreach (object obj in list)

            {

                object[] row = new object[pi.Length];

                int i = 0;

     

                foreach (PropertyInfo p in pi)

                {

                    row[i++] = p.GetValue(obj, null);

                }

     

                dt.Rows.Add(row);

            }

     

            return dt;

        }

    }

    EntityDataTable in Action

    Here’s a quick example of a derived class, that adpts my SomethingCollection to easily work with the GridView and ObjectDataSource.

        public class BindableSomethingCollection : EntityDataTable

        {

            public override DataTable Fill(object parameter)

            {

                SomeService somesrv = new SomeService();

                return GetDataTable(somesrv.GetSomeCollection(parameter as SomeType), typeof(SomeContainedType));

            }       

        }

    And here’s a quick ASCX control snippet that shows it all tied together:

    <asp:GridView ID ="GridView1" runat="server" AutoGenerateColumns="True" EmptyDataText="No Records" ShowFooter ="True" AllowPaging ="True"  AllowSorting="True" DataSourceID="ObjectDataSource1" ></asp:GridView>

    <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="Fill" TypeName="BindableSomethingCollection">
      <SelectParameters>
        <asp:SessionParameter DefaultValue="null" Name="parameter" SessionField="PARAMETER" Type="Object" />
      </SelectParameters>
    </
    asp:ObjectDataSource>

    All that's left for me to do is pop my parameter in the session, and call GridView1.DataBind().. Voila!  My grid is bound to my entity collection, sorting and paging work like a charm, and I can filter my data buy calling by setting the Filter parameter of the ObjectDataSource.

    [tags: ASP.NET, .NET, C#]

  • Welcome Rod Paddock

    Today we added Rod Paddock to the CodeBetter.Com crew.
    Rod Paddock is president and founder of Dash Point Software, Inc. DPSI is an award winning software company based in Seattle, WA. Dash Point Software specializes in application architecture, development and software training. Clients include Six Flags, First Premier Bank, Microsoft, The US Coast Guard and US Navy.

    Dash Point specializes in Visual Studio .NET (C# and VB.NET), Visual Basic, Visual FoxPro, and SQL Server development. Rod has been a very popular speaker at a wide variety of developer conferences in North America and Europe since 1995. His most recent speaking appearance was at the DevTeach 2005 conference in Montreal Canada. Rod was made an MSDN Canada speaker in 2004 and is currently a Microsoft VB.NET MVP.
     
    Rod is also editor in chief for Code Magazine and has written numerous articles and books on software development.

    You can find his blog at http://codebetter.com/blogs/rodpaddock

    Welcome Rod!

More Posts