Wednesday, July 25, 2012

RDLC Reports in MVC Web application


Step by Step adding an RDLC report to MVC4 project

For any large project, reports will be needed. I love RDLC so I’m sticking with them. I had some problems in the beginning but with some search here and there and some testing it start working. So I will describe it from the beginning.
Requirements:
·         In my new MVC I need to have a number of RDLC reports
·         I need the reports to read from objects, direct entities or report objects (for large reports I prefer to something like a Report View Model, in the end a report is view for data)
·         A report my require one or more data sets
·         Some of the reports will need parameters that the user will select from the MVC pages
·         I want to export the report by  PDF, but having word and Excel would be an advantage
·         Some report standardization point that is needed:
o   Required reports have a description title
o   Required have right and left main and sub headers
o   Required to have the user name printing the report in the footer
o   Required to have the printing date on the footer
o   Required to have number of page and total pages in the footer
o   All the reports are A4 stander reports ready for printing.
o   Reports will be multi language (currently English and Arabic)
o   Reports may contain charts and bars
o   Reports need to be secure
·         The system contains an alert module and some of the requirement needs the PDF document to be attached to the email.

So lets start clean and from the beginning.
Lunching Visual Studio, selecting new project
Selecting MVC4, intrenet application (all template will work too)

Adding a new project with the name: ASPNET_MVC_RDLC

I will start putting it all in one project; it’s the same if you have business and data access layers that are in separate projects too, only to make it short im putting it all together and skipping testing too.



 So let’s start by adding the report view model that will handle the report parameters, headers and data sets




   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Web;
   5:  using Microsoft.Reporting.WebForms;
   6:  using System.Web;
   7:  using System.Web.Mvc;
   8:   
   9:  namespace ASPNET_MVC_RDLC.Models
  10:  {
  11:      public class ReportViewModel
  12:      {
  13:          public enum ReportFormat         { PDF=1,Word=2,Excel=3}
  14:          public ReportViewModel()
  15:          {
  16:              //initation for the data set holder
  17:              ReportDataSets = new List<ReportDataSet>();
  18:          }
  19:   
  20:          //Name of the report
  21:          public string Name { get; set; }
  22:   
  23:          //Language of the report
  24:          public string ReportLanguage { get; set; }
  25:   
  26:          //Reference to the RDLC file that contain the report definition
  27:          public string FileName { get; set; }
  28:   
  29:          //The main title for the reprt
  30:          public string ReportTitle { get; set; }
  31:   
  32:          //The right and left titles and sub title for the report
  33:          public string RightMainTitle { get; set; }
  34:          public string RightSubTitle { get; set; }
  35:          public string LeftMainTitle { get; set; }
  36:          public string LeftSubTitle { get; set; }
  37:   
  38:          //the url for the logo, 
  39:          public string ReportLogo { get; set; }
  40:   
  41:          //date for printing the report
  42:          public DateTime ReportDate { get; set; }
  43:   
  44:          //the user name that is printing the report
  45:          public string UserNamPrinting { get; set; }
  46:   
  47:          //dataset holder
  48:          public List<ReportDataSet> ReportDataSets { get; set; }
  49:     
  50:          //report format needed
  51:          public ReportFormat Format { get; set; }
  52:          public bool ViewAsAttachment { get; set; }
  53:   
  54:           //an helper class to store the data for each report data set
  55:          public class ReportDataSet
  56:          {
  57:              public string DatasetName { get; set; }
  58:              public List<object> DataSetData { get; set; }
  59:          }
  60:   
  61:          public string ReporExportFileName { get {
  62:              return string.Format("attachment; filename={0}.{1}", this.ReportTitle, ReporExportExtention);
  63:          } }
  64:          public string ReporExportExtention
  65:          {
  66:              get
  67:              {
  68:                  switch (this.Format)
  69:                  {
  70:                      case ReportViewModel.ReportFormat.Word: return  ".doc"; 
  71:                      case ReportViewModel.ReportFormat.Excel: return ".xls"; 
  72:                      default:
  73:                          return ".pdf";
  74:                  }
  75:              }
  76:          }
  77:   
  78:          public string LastmimeType
  79:          {
  80:              get
  81:              {
  82:                  return mimeType;
  83:              }
  84:          }
  85:          private string mimeType;
  86:          public byte[] RenderReport()
  87:          {
  88:              //geting repot data from the business object
  89:   
  90:              //creating a new report and setting its path
  91:              LocalReport localReport = new LocalReport();
  92:              localReport.ReportPath =System.Web.HttpContext.Current.Server.MapPath(this.FileName);
  93:   
  94:              //adding the reort datasets with there names
  95:              foreach (var dataset in this.ReportDataSets)
  96:              {
  97:                  ReportDataSource reportDataSource = new ReportDataSource(dataset.DatasetName, dataset.DataSetData);
  98:                  localReport.DataSources.Add(reportDataSource);
  99:              }
 100:              //enabeling external images
 101:              localReport.EnableExternalImages = true;
 102:   
 103:              //seting the partameters for the report
 104:              localReport.SetParameters(new ReportParameter("RightMainTitle", this.RightMainTitle));
 105:              localReport.SetParameters(new ReportParameter("RightSubTitle", this.RightSubTitle));
 106:              localReport.SetParameters(new ReportParameter("LeftMainTitle", this.LeftMainTitle));
 107:              localReport.SetParameters(new ReportParameter("LeftSubTitle", this.LeftSubTitle));
 108:              localReport.SetParameters(new ReportParameter("ReportTitle", this.ReportTitle));
 109:              localReport.SetParameters(new ReportParameter("ReportLogo", System.Web.HttpContext.Current.Server.MapPath(this.ReportLogo)));
 110:              localReport.SetParameters(new ReportParameter("ReportDate", this.ReportDate.ToShortDateString()));
 111:              localReport.SetParameters(new ReportParameter("UserNamPrinting", this.UserNamPrinting));
 112:   
 113:              //preparing to render the report
 114:   
 115:              string reportType = this.Format.ToString();
 116:             
 117:              string encoding;
 118:              string fileNameExtension;
 119:   
 120:              //The DeviceInfo settings should be changed based on the reportType
 121:              //http://msdn2.microsoft.com/en-us/library/ms155397.aspx
 122:              string deviceInfo =
 123:              "<DeviceInfo>" +
 124:              "  <OutputFormat>" + this.Format.ToString() + "</OutputFormat>" +
 125:              "</DeviceInfo>";
 126:   
 127:              Warning[] warnings;
 128:              string[] streams;
 129:              byte[] renderedBytes;
 130:   
 131:              //Render the report
 132:              renderedBytes = localReport.Render(
 133:                  reportType,
 134:                  deviceInfo,
 135:                  out mimeType,
 136:                  out encoding,
 137:                  out fileNameExtension,
 138:                  out streams,
 139:                  out warnings);
 140:   
 141:              return renderedBytes;
 142:          }
 143:      }
 144:      
 145:  }



lets add a new report called MyMVCReport.rdlc as below:
we will need to add some stub methods to set the report data sources

Set the data source for the report using the stub methods


   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Web;
   5:   
   6:  namespace ASPNET_MVC_RDLC.Models
   7:  {
   8:      public class Business
   9:      {
  10:          public List<Employee> StubForEmployeeDataSet()
  11:          {
  12:              //this is used only to help in adding the dataset of type employee to the report definition
  13:              return null;
  14:          }
  15:          public List<Project> StubForProjectDataSet()
  16:          {
  17:              //this is used only to help in adding the dataset of type project to the report definition
  18:              return null;
  19:          }
Note: in production code, I don't recommend putting this in the business layer, any report stub class will be better.


Set the data source for the report using the stub methods
so we have a problem here when selecting add data set from the report menu, the data source drop down list is empty, and when you click new it open a connection to the database.
So after some search I found a solution from stackoverflow, simply add a web form page to the MVC project and it will start working, I didn't have time to check why is this, but it works.
lets add another one.




lets set the design and layout of the report
the above is only for testing.

coming near the last steps , we need to add a reference to the Microsoft web form reports assembly

now lets add the controller that will produce the report.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Web;
   5:  using System.Web.Mvc;
   6:  using ASPNET_MVC_RDLC.Models;
   7:   
   8:  namespace ASPNET_MVC_RDLC.Controllers
   9:  {
  10:      public class ReportController : Controller
  11:      {
  12:          public ActionResult PrintRepert()
  13:          {
  14:              //geting repot data from the business object
  15:              var Business = new Business();
  16:              var reportViewModel = Business.GetMyRepoertViewModel();
  17:   
  18:              var renderedBytes = reportViewModel.RenderReport();
  19:   
  20:              if(reportViewModel.ViewAsAttachment)
  21:                  Response.AddHeader("content-disposition", reportViewModel.ReporExportFileName);
  22:              return File(renderedBytes, reportViewModel.LastmimeType);
  23:   
  24:          }
  25:   
  26:      }
  27:  }

Add a link to the page to open the report wherever is needed.


@{
    ViewBag.Title = "Home Page";
}
@section featured {
    <section class="featured">
        <div class="content-wrapper">
            <hgroup class="title">
                <h1>@ViewBag.Title.h1>
                <h2>@ViewBag.Messageh2>
            hgroup>
          
        div>
    section>
}
<h3>We suggest the following:h3>
<ol class="round">
    <li class="one">
        <h5>Print an RDLC report in MVCh5>
          @Html.ActionLink("Print Report", "PrintRepert","Report")
    li>
 
ol>

ok lets run and see.

And its working, there still more work to be done as checking the other formats, the email attachment, 
adding some security, the Multilanguage, and uploading this example and linking it here.

I would love to here some comments.

Update: for the business class, it really must come from your original entities. so for this sample this is the one im using
   1:   
   2:  namespace ASPNET_MVC_RDLC.Models
   3:  {
   4:      public class Business
   5:      {
   6:          public List<Employee> StubForEmployeeDataSet()
   7:          {
   8:              //this is used only to help in adding the dataset of type employee to the report definition
   9:              return null;
  10:          }
  11:          public List<Project> StubForProjectDataSet()
  12:          {
  13:              //this is used only to help in adding the dataset of type project to the report definition
  14:              return null;
  15:          }
  16:          //somew
  17:          public ReportViewModel GetMyRepoertViewModel()
  18:          { 
  19:          // I will not go through getting data from ef, i will assume some test data here
  20:   
  21:            //first dataset, employee info
  22:              var EmplyeeDataSet =new List<object>() {
  23:                      new Employee() { Name="Ahmed Khaled", Mobile = "055111111", Email="Ahmed.Khaled@Somewhere.com", BirthDate=new DateTime(1970,10,10)},
  24:                      new Employee() { Name="Jamal Awsom" , Mobile = "055222222", Email="Jamal.Awsom@Somewhere.com", BirthDate=new DateTime(1977,1,10)},
  25:                      new Employee() { Name="James Salem" , Mobile = "055333333", Email="James.Salem@Somewhere.com", BirthDate=new DateTime(1982,12,10)},
  26:                      new Employee() { Name="Tony Read"   , Mobile = "055444444", Email="Tony.Read@Somewhere.com", BirthDate=new DateTime(1982,4,10)}
  27:        
  28:          };
  29:          var ProjectDataSet =new List<object>() ;
  30:              for (int i=1;i<30;i++)
  31:              {
  32:                  EmplyeeDataSet.Add(new Employee() { Name = "James Salem", Mobile = "055333333", 
  33:                      Email = "James.Salem@Somewhere.com", BirthDate = new DateTime(1982, 12, 10) });
  34:              }
  35:              
  36:              //Assuming the person printing the report is me
  37:              var UserPrinting="Ali Taki";
  38:   
  39:              var reportViewModel = new ReportViewModel()
  40:              {
  41:                  FileName = "~/Reprots/MyMVCReport2.rdlc",
  42:                  LeftMainTitle = "ABC Company Name",
  43:                  LeftSubTitle = "DEF Department Name",
  44:                  RightMainTitle = "اسم الشركة",
  45:                  RightSubTitle = "اسم القسم",
  46:                  Name = "Statistical Report",
  47:                  ReportDate = DateTime.Now,
  48:                  ReportLogo = "~/Content/logo.jpg",
  49:                  ReportTitle = "Summary report for top employee's and projects",
  50:                  ReportLanguage = "en-US",
  51:                  UserNamPrinting = UserPrinting,
  52:                  Format=ReportViewModel.ReportFormat.PDF,
  53:                  ViewAsAttachment=false,
  54:   
  55:              };
  56:              //adding the dataset information to the report view model object
  57:              reportViewModel.ReportDataSets.Add(new ReportViewModel.ReportDataSet() { DataSetData = EmplyeeDataSet.ToList(), DatasetName = "Employees" });
  58:              reportViewModel.ReportDataSets.Add(new ReportViewModel.ReportDataSet() { DataSetData = ProjectDataSet.ToList(), DatasetName = "Projects" });
  59:   
  60:   
  61:              return reportViewModel;
  62:          
  63:          }
  64:      }
  65:  }

Monday, July 23, 2012

MVC AutoComplete template


Get the Id/Key of the selected item and highlight the inputed text in the list of selections.
The below works with MVC3 or MVC4 using Razor view engine

I will use JQuery to enhance the UI of the app, so here is my autocomplete that can be used anywhere in your MVC project and more than once in a page.


First configure your scripts to include JQuery and JQuery UI, etc in the layout page, so the autocomplete can work all over the application in any view.
        @Scripts.Render("~/bundles/jquery")
        @Scripts.Render("~/bundles/jqueryui")
        @Scripts.Render("~/bundles/jqueryval")
I added the autocomplete java code in the ("~/bundles/jqueryval") bundle, at the App_Start as below:
  public static void RegisterBundles(BundleCollection bundles)
        {
            //TODO: On production this will be true
            BundleTable.EnableOptimizations = false;

            bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-1.*"));

            bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include(
                        "~/Scripts/jquery-ui*"));

            bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                        "~/Scripts/jquery.unobtrusive*",
                        "~/Scripts/jquery.validate*",
                        "~/Scripts/ControlsJScript.js"));
           .....
           .....

This ControlJscript.js file will be my stander control code for all the templates I need, currently its populated with the autocomplete code that will wire the autocomplete and the highlighted behavior

$(document).ready(function () {

    //set autocomplete with hieghlit text
    $(":input[data-autocomplete]").each(function () {
        $(this).autocomplete({
            source: $(this).attr('data-autocomplete'),
            minLength: 2,
            change: function (event, ui) {
                var TargetId = "#" + $(this).attr('data-target');    //find Target Control Id
                if (ui.item == null) {$(TargetId).attr('value', "  ");} //Set the value to the selected value
                else { $(TargetId).attr('value', ui.item.id);}
            }
        }).data("autocomplete")._renderItem = function (ul, item) {
            var term = $(this)[0].term;
            var index = item.label.toLowerCase().indexOf(term.toLowerCase());

            return $("<li>li>")
                .data("item.autocomplete", item)
                .append("<a>" + item.label.substring(0, index)
                      + "<font class='AutoFont'>" + item.label.substring(index, index + term.length)
                      + "font>" + item.label.substring(index + term.length) + "a>")
                .appendTo(ul);
            //&#8205 script joiner
            //u is the solution
        };
    });
});

 Note: AutoFont is a css class that specify the style for the input text
Second set Autocomplete View Model class in your model folder or anywhere you like.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace ADM.Ntrasal.UI.Web.Browser.Models
{
    public class AutoCompleteViewModule
    {
        public AutoCompleteViewModule()
        {
            //default values
            AutocompleteCount = 10;
            Type = AutoCompleteTypes.EmployeeNames;
        }
        public string Id { get; set; }
        public string Name { get; set; }
        public string Field { get; set; }
        public int AutocompleteCount { get; set; }
        public AutoCompleteTypes Type { get; set; }

        public enum AutoCompleteTypes
        {
            EmployeeNames,
            OrgChart,
            Sources
        }
    }
}

I prefer to add the controller logic to an api controller, but for this example (for backwards compatibility) im adding the managing code to a normal controller as below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using ADM.Ntrasal.Business;
using ADM.Ntrasal.Data;
namespace ADM.Ntrasal.Web.UI.Browser.Controllers
{
    public class AutoCompleteController : Controller
    {
         private readonly IRepositories Repositories;
         public AutoCompleteController()
        {  //Prefare to use Ioc but for simplicity
            this.Repositories = new Repositories();
        }
        //
        // GET: /AutoComplete/

        public ActionResult EmployeeNames(string term)
        {
            var count = 10;
            var UserProfileRepostery = this.Repositories.Create<UserProfile>();


            var result = UserProfileRepostery.GetAll().Where(x => x.Employee_Name.ToLower().Contains(term.ToLower()))
              .Take(count).Select(x =>
                  new
                  {
                      label = x.Employee_Name,
                      id = x.UserID
                  });
            return Json(result, JsonRequestBehavior.AllowGet);
        }

    }
}
Note: the idea is to return the text with its key, so when the user selects something you get the id/key directly.

Add the below as a template for the Autocomplete
@model ADM.Ntrasal.UI.Web.Browser.Models.AutoCompleteViewModule
 <input type="text"  name="AutocompleteTextBox"
     data-target='@Model.Field'
     data-autocomplete="@Url.Action(@Model.Type.ToString(), "AutoComplete", new { count = @Model.AutocompleteCount })"
     value='@Model.Name' />

@Html.Hidden(@Model.Field)

Add it to your view whenever an autocomplete is neededset, for a test view, make the view a strong type with AutoCompleteViewModule as below.
@model ADM.Ntrasal.UI.Web.Browser.Models.AutoCompleteViewModule
  @Html.EditorFor(x=>@Model)

I did’t add a code sample because this is a part of a large project, but if anybody is interested, I can make a quick project and upload it.