Thursday 23 May 2013

Using ReportViewer control in MVC 4 with Razor

The ReportViewer control for Browser apps is an ASP.NET webforms control.  There are quite a lot of posts on various forums about using this in MVC of varying levels of usefulness.  I wanted to see how to use it with an MVC 4 app in which I was using Razor as the view engine.  The starting point for the following was this post on stackoverflow, which pointed me in the right direction but it still required some faffing to get a working solution.  The following works for me.

The general approach is to create an ASP.NET webforms user control (reportViewerControl.ascx) and embed this in a Razor page using the @Html.RenderPartial helper.

To keep things tidy in my MVC project, I have created a folder called Reports under the project root, where I plan to keep my rdlc files and a folder under the Views folder, which I've also called Reports (i.e. Views\Reports) where I create the user control, which I plan to use to render the rdlc files.  I'm hoping with some cunning I may be able to use the control for all reports.

Step 1: Create an ASP.NET user control called ReportViewerControl.ascx in Views\Reports

ReportViewerControl.ascx looks like this

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ReportViewerControl.ascx.cs" Inherits="ReportViewerTest.Views.Reports.ReportViewerControl" %>

<%@ Register TagPrefix="rsweb" Namespace="Microsoft.Reporting.WebForms" Assembly="Microsoft.ReportViewer.WebForms, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" %>
<form id="form1" runat="server">

<div>
    <asp:ScriptManager ID="scriptManager" runat="server" EnablePartialRendering="false" />
   
    <asp:Panel ID="pnlReport" ClientIDMode="Static" runat="server">
    <rsweb:ReportViewer Width="100%" Height="100%" ZoomMode="PageWidth" SizeToReportContent ="True" ID="reportViewer" runat="server" >
                  <LocalReport>
                  </LocalReport>

    </rsweb:ReportViewer>
    </asp:Panel>
</div>
</form>

reportViewer is the ID of the control, referred to in the code behind lower down.

The script manager is needed to make things work

The control is embedded in a panel, to help fix some CSS issues I'll describe later.

Step 2: Design your report (rdlc file).  My test report is called PatientsList.rdlc.  I'm pulling my data from an entity model linked to a SQL Server database.  It looks like this


The dataset is called DataSet1 and I've created a parameter called Title to push in the report title (mainly as a way to demonstrate parameter passing)

Step 3: Add a reference (under references) to Microsoft.ReportViewer.Webforms.  You may see different versions (e.g. 9 or 10) under the .NET tab.  I used version 10.  Set the CopyLocal property to True, so that the dll gets included in the Publish operation.

Step 4: Back in the user control, make the code behind in ReportViewerControl.ascx.cs look like

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
//using System.Web.UI.WebControls;
namespace ReportViewerTest.Views.Reports
{
    //public partial class ReportViewerControl : System.Web.UI.UserControl   
    public partial class ReportViewerControl : System.Web.Mvc.ViewUserControl    
   {
        protected void Page_Load(object sender, EventArgs e)
        {

        }
        protected void Page_Init(object sender, EventArgs e)
        {
           // Required for report events to be handled properly.
            Context.Handler = Page;
            ShowReport();
        }

        protected void ShowReport()
        {
            My_DevEntities entities = new My_DevEntities();
            IQueryable<Patients> pdata = from p in entities.Patients
                                         select p;
            List<Patients> plist = pdata.ToList();
            //List<Patients> Model = plist;

            reportViewer.ProcessingMode = Microsoft.Reporting.WebForms.ProcessingMode.Local;
            reportViewer.LocalReport.ReportPath = "Reports\\PatientsList.rdlc";
            reportViewer.LocalReport.DataSources.Clear();
            reportViewer.LocalReport.DataSources.Add(new Microsoft.Reporting.WebForms.ReportDataSource("DataSet1", plist));


            //Set Parameters
            Microsoft.Reporting.WebForms.ReportParameter param = new Microsoft.Reporting.WebForms.ReportParameter("Title", "Patients Lost");
            reportViewer.LocalReport.SetParameters(param);

            reportViewer.LocalReport.Refresh();       
       
           }

    }
}
Some important points here:

a) The user control needs to inherit from System.Web.Mvc.ViewUserControl instead of
System.Web.UI.UserControl    
b) In Page_Init you need to set Context_Handler = Page; for the report events to work
c) Page_Init is also the place to refresh the report from.  If you do this stuff in Page_Load, the page keeps refreshing itself
d) I pass the data to the report as a List<>. 
e) I set the ProcessingMode to Local, the ReportPath to my rdlc file, clear out any data source defined in the rdlc and pass in DataSet1.  I can pass in multiple datasets, with repeated called to add new data sources to the local report.
f) I'm setting the Title parameter to "Patient List" but you can skip those two lines if you don't need to pass any parameters
g) Finally I refresh the local report

Step 5:  The Razor view

My Razor view is simply this

<h3>My Patient List</h3>
@{Html.RenderPartial("~/Views/Reports/ReportViewerControl.ascx");}

Some posts suggested using the @Html.Partial helper but when I did this, the page wouldn't load in the browser (IE10 or Chrome)

Navigating to this view gives


 which is a good start but the ReportViewer toolbar is a mess.  The problem is that MVC4 Internet projects out of the box, define some CSS for tables.  The following CSS, added to the Content\Site.css file and applied specifically to the panel, fixes this in Chrome and IE10 (haven't tested this for other browsers yet)

/*****************************
*   Extras for ReportViewer  *
******************************/
#pnlReport table, #pnlReport td, #pnlReport span, #pnlReport a, #pnlReport input
{
    padding: 0 !important;
    margin: 0 !important;
}
#pnlReport input
{
    background-color: Transparent;
}
#pnlReport input[type=text]
{
    background-color: #FFF;
    width:200px;
}
/*****************************
*   End of Extras            *
******************************/

The View now renders as


which is just what I wanted.  All of the ReportViewer toolbar functions work as expected.

One final note.  When you make changes to any of the code and rerun the app, you will probably need to do a  Ctrl F5 before you see the effects of the changes.  I mention it here because I always forget this!

Tuesday 12 March 2013

Fitting a quart into a pint pot

I received an interesting support email from a client yesterday. We'd migrated a very large and complex Access database to SQL Server and done some fairly extensive work on the Access front end application, moving slow running queries to sprocs.

The support request was relating to an Access query written by the client, running against the SQL database, that was returning the message "Scaling of decimal value resulted in data truncation". The query contained a calculated column which was the product of 2 decimal fields, a floor area multiplied by a percentage. This column was being summed as part of a GROUP By on the recordset.

She reported that the query didn't always return the error and using the Round function to limit the result of the calculation to 2 decimal places fixed the problem.

When I investigated a little further, I found that the original Numeric Single field in Access had been converted to a decimal(18,4) in SQL Server by the 'Microsoft SQL Server Migration Assistant for Access' tool, which has replaced the old Upsizing Wizard built into older versions of Access. I must own up to not having checked what every field had been mapped to by the tool.

The clue to the error is the 18 decimal digits precision (fixed with 4 decimal places) of the decimal field compared to the highest precision numeric data type is Access, which is a (floating point) Double with 15 decimal digits precision. At some point Access has to shrink (truncate) 18 digits coming from SQL Server down to 15. The query looks simple enough to be passed through to SQL Server for processing. Access decides to either lopp off the precision at the front end ('leading zeros') or at the back end (decimal places). My guess is that where the the result of the calculation is a similar scale across all records in the returning recordset, Access can chose to do one or the other across all records, and maintain all the decimal places without a problem. Where the recordset contains either very small decimal numbers or ones with a lot of decimal places mixed with some much larger numbers, a conflict arises and Access can't decide on a truncation scheme to apply across the board. By using the Round function to limit all results to 2 decimal places, it effectively frees up the precision the the left pf the decimal point. It is unlikely that the sum of any set of floor areas will be be greater than say 10^7, so there is easily sufficient scope to truncate from the left end of the number.

I now need to find time to reproduce the error and inspect the individual values in the recordset to confirm this.

Thursday 14 February 2013

Installing SQL Server 2012

I've just spent a frustrating few hours trying to install SQL Server 2012 Business Intelligence as a new instance.  I already have SQL Server 2005 Express and SQL Server 2008 Standard installed. 

I also have Visual Studio 2010 installed (but not 2012)

The problems come when it gets to installing the development tools  - what used to be called BI Dev Studio or BIDS.

The installation fails if it cannot find vs_setup.msi.  You are given the opportunity to browse to it.  If you point it at the file on the VS 2010 installation DVD, it still grumbles.

One solution (there may be others) which worked for me, is to copy vs.setup.msi to a folder on your local hard drive.  You may then be prompted for cab13.cab and most of the subsequent cabs.  I ended up copying cabs 13 to 52 inclusive on to the local drive.

The installation proceeds ok after that, although it takes an age to complete.

Some forum posts on this subject, suggest downloading VS 2010 Ultimate from the Microsoft site as a work round.  The files for VS 2010 Pro worked ok for me.

I'd be interested to know if you have to go through all this faff,  if you have VS 2012 installed.

Sunday 18 December 2011

Fixing MVC in a virtual directory under IIS6

MVC sites gets broken when you run them in a virtual directory in IIS6. 

Specifically, the routing stops working and the paths to any images, css and javascript files gets broken

Here's what you need to know to fix them:

1) Routing
In IIS6, open the properties dialog and from the Virtual Directory tab, setup wildcard mappings to handle MVC's dimensionless URLS:
• click Configuration
• click Insert
• enter path to the aspnet_isapi.dll file for version of aspnet you are using, e.g. for .net 4 aspnet_isapi.dll (in C:\Windows\Microsoft.NET\Framework\v4.0.30319)
• untick Verify that file exists (this is important!)


You can read more about this on Stephen Sanderson's blog.

2) Image and js paths

To deploy under a Virtual Directory:
In Site.Master
css hrefs need ~ prefix in path eg
<link href="~/Content/Site.css" rel="stylesheet" type="text/css" />

script source refs fixed with the following

<% var siteroot = Url.Content("~/"); %>
<script src="<%: siteroot %>Scripts/jquery-1.4.4.min.js" type="text/javascript"></script>

image paths fixed by rewriting value for src in the image tag as, eg

src="/Content/myimage.png"

rewrite as

src="<%=Url.Content("~/Content/myimage.png") %>"

and note ~ prefix in path