Leveraging XtraReports in MVC 2

So I went shopping for some reporting libraries for an ASP.NET MVC 2 application running on Azure.  One of the products I researched is DevExpress’ XtraReports.  After weighing the pros and cons of XtraReports with several other libraries (from open source to other vendors) I chose to give this product a go under the 30 day trial.  I was surprised there was no obvious marketing around support for MVC but after watching a few demos it was clear the tool is comprehensive and exposes plenty of API for the dev to hook into, regardless of the chossen framework – it doesn’t hurt that the product ships with source code for a little more money.

So here is the use case.  The app I’m working on doesn’t really call for heavy reporting of tabular data – though its expected the app will grow into this need in the future.  For now, we simply need a PDF representation of a small set of data.  For demo purposes, let’s assume we’re reporting on some basic data about a customer’s purchases. Another part of the request for this data in PDF format, is to make it accessible via REST – so a given route rendering HTML pages “/CustomerPurhcases” would render a PDF if .pdf was appended to the route – “/CustomerPurchases.pdf”.

In this post, we’re going to see how we can wrap a very thin layer around XtraReports and take a ViewEnginer-like approach to delivering reports in different formats.  You’ll be pleased to know that XtraReports does most of the heavy lifting for us – let’s check it out.

I’m going to skip some set-up here and refer you to Seth Juarez’s “Building Reusable Reports” demo if you need information about how to create a report and bind it to an object – specifically in an n-tier application with a services layer (which I happen to use fairly often).  I’m going to pick up from Seth’s demo around the point where he introduces the “Load” method on the report.

Let’s start with the reporting and work our way into the MVC side of things.  Here is a look at the report.

Definitely not winning any awards with this report’s appearance, but you can see here the object graph of the object I’ve bound this report to, and the report and databound fields.  I chose to use an object here that contains another object to show that XtraReports nicely supports deep object graphs in its Field Lists.  So we have our report, we now want to retrieve it.  My first approach to getting these reports was explicitly asking for the report by name.

public static void GetCustomerPurchaseReport(CustomerPurchases customerPurchases, Stream stream)
{
   var report = new CustomerPurchasesReport();
   report.Load(new List() { customerPurchases});
   report.ExportToPdf(stream, new PdfExportOptions()
   {
      ImageQuality = PdfJpegImageQuality.Highest
   });
}

Well that was easy enough, right?  Notice the nice and easy call to the Load method described in Seth’s demo, whereby we provide the object for the reports DataSource, and then simply call ExportToPdf. Done, we now have a PDF that can float up to the user.  However, we can do better.  Immediately, some forsight kicks in and we can see ourselves repeating this call over and over calling reports by name.

So let’s identify a pattern we already have before us.  Notice that our object we are providing has the same name as our report, except the word “Report” is appended.  If we commit to this convention we can apply a little reflection and be bi-winning.  Let’s take a look at the reflection.

public static void GetPDFReport(T model, Stream stream)
{
   var modelType = model.GetType();
   var assembly = Assembly.GetExecutingAssembly();
   object report;

   if (assembly.GetTypes().ToList().ConvertAll(x => x.Name).Contains(
                                String.Concat(modelType.Name, "Report")))
   {
      report = assembly.CreateInstance(String.Format("XR.Reporting.{0}Report",
                                                      modelType.Name));
      if (report != null)
      {
         var list = (new[] { model }).ToList();
         ((Report)report).Load(list);
         ((Report)report).ExportToPdf(stream, new PdfExportOptions()
               {
                  ImageQuality = PdfJpegImageQuality.Highest
               });
      }
      else
         throw new NullReferenceException();
   }
   else
   {
      throw new FileNotFoundException();
   }
}

With the use of generics we are now positioned to accept any type.  You start to see our tie in to MVC, here.  Much like we provide a view a model, we’re asking our reporting library for a PDF “View” and providing a model.  This GetPDFReport method examines the incoming model discovering its name and then looks in the current library for a class with the same name with Report appended.  From there, its as straight forward as our previous method, load and export.  There are a few things to note here, though.  First, a report’s DataSource accepts an IList object, hence the conversion to a list prior to the call to Load.  Also, you may have noticed a new type introduced, “Report”.  Let’s see that there is no magic here, just more abstraction.

public class Report : XtraReport
{
   public void Load(T t)
   {
      DataSource = t;
   }
}

Here we simply extend the XtraReport type and provide a generic Load method, one level of abstraction away from Seth’s demo – nothing new here at all.

So that takes care of the reporting side.  Of course, there is still room to grow.  GetPDFReport was intentionally left specific for demonstration and readability, but this can be further refactored to generically handle other export types and the string literal assembly name could be further abstracted.  Have a look at XtraReports ExportTo<type> interface, there is XLS, RTF, CSV, etc supported, and the ExportToPdf call in the reflection above could easily fall out to a format of your choice with relative ease.

Let’s see how we bring this back to the MVC 2 App. Here is a look at our very unattractive Purchases page.  We see most of the same data from our report view.  Notice the URL, /Purchases.

As you might imagine, this View is served as expected from a controller, nothing special there.  To access our PDF report, we’ll append .pdf to our URL.

We can see above by simply appending the .pdf desired format to the end of the URL we get our PDF report of Customer Purchases.  This may seem like some voodoo, but its actually pretty straight forward – which is great news!  Let’s take a look at our routes and controller.

routes.MapRoute("PurchasesRoute", "Purchases",
   new { controller = "Customer", action = "Purchases"});
routes.MapRoute("PurchasesFormatRoute", "Purchases.{format}",
   new { controller = "Customer", action = "FormattedPurchases",
         format = UrlParameter.Optional });

 

public ActionResult Purchases()
{
    return View(customerServices.GetCustomerPurchases());
}

public ActionResult FormattedPurchases(string format)
{
    using (var memStream = new MemoryStream())
    {
        switch (format)
        {
           case "pdf":
              //Note: memStream is our output, here
              ReportGenerator.GetPDFReport(customerServices.GetCustomerPurchases(), memStream);
              return new PDFResult(memStream.ToArray());
              break;
           default:
              //If unsupported format, return default page
              return RedirectToAction("Purchases");
         }
     }
}

We see that our routes are pretty standard issue.  The first route, PurchasesRoute, we have our straight forward set-up pointing to the Purhcases action in the Customer controller.  This delivers us into our Purchases() action.  To pull off the .pdf extention to the URL, we add another route and append .{format} to the end.  Sure, we could have handled this all in one Route and one Action, but I would argue that would be a violation of single responsibility.  In my case, the user is expected to navigate to the Html page via the PurchasesRoute more frequently than an alternative format, therefore I chose not to roll formats and standard page requests into one route and action.

That aside, let’s get to the good stuff.  We see here, still, life is good – no crazy complexity.  We require a memory stream to pass through to the reporting library as that’s what ExportToPdf uses to spit out results to.  Here, we’ll use our static factory call to GetPDFReport and a return of a customer PDFResult which inherits straight from ActionResult.  Also, if an unsupported type is entered into the format, we’re pointed back to the html page – note that the choice to seperate the actions isn’t painful as we can simply point the user back with a redirect.

public class PDFResult : System.Web.Mvc.ActionResult
{
    private readonly byte[] pdf;

    public PDFResult(byte[] pdf)
    {
        this.pdf = pdf;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        var response = context.RequestContext.HttpContext.Response;
        response.Clear();
        response.ContentType = "application/pdf";
        response.BinaryWrite(pdf);
        response.End();
    }
}

Above is our customer PDFResult.  Notice here we’re accepting a byte array, providing our PDF application content type, and then writing our binary straight to the response.  This is the final touch on our MVC implementation.  Notice that the majority of this implementation is set-up on the MVC side, and a wrapper around our XtraReports library was pretty thin.  If you want to check out the demo app for yourself, feel free!  Of course, you’ll need your own XtraReports license or trial, but you’re welcome to the code nonetheless.

XtraReportsDemoProject

XtraReportsDemoProjectWithMVC3

Advertisements

No comments yet... Be the first to leave a reply!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: