Lately I have seen a couple of questions about how to make a excel export from a custom report and also if it is possible to make a custom report in MVC.
I had never done any custom reports in MVC so I decided to try this things out and it showed that is was pretty simple.
For the excel-export function I am using the external packaged EPPlus.
I decided to make a very simple report on the pages that are exist on a website, feel free to get inspired from it.
First I created a Controller that looks like this:
namespace AlloyExample.Controllers
{
[EPiServer.PlugIn.GuiPlugIn(
Area = EPiServer.PlugIn.PlugInArea.ReportMenu,
Url = "~/existingpagesreport",
Category = "Existing Pages",
DisplayName = "Pages By PageType")]
[Authorize(Roles = "Administrators, WebAdmins")]
public class ExistingPagesReportController : Controller
{
public ActionResult Index()
{
return View();
}
}
}
Here I define that this controller also is a GuiPlugin that should be showed in the reportmenu and I give it a url that is /existingpagesreport. I also protect it so you have to be part of the administrator group to be able to use it.
For this url to work, we need to add a route to it so I add this in global.asax.cs
protected override void RegisterRoutes(RouteCollection routes)
{
base.RegisterRoutes(routes);
routes.MapRoute("ExistingPagesReport","existingpagesreport/{action}",
new { controller = "ExistingPagesReport", action = "Index" });
}
After that I created a simple view and just tried out that it showed up in the report center.
To make it a little more interesting I first added a ViewModel with the properties I needed. It looks like this:
public class ExistingPagesReportViewModel
{
public IEnumerable<PageType> PageTypes { get; set; }
public PageDataCollection Pages { get; set; }
public string SelectedPageType { get; set; }
}
Then I created a helper class to make my request against Episerver and that looks like this:
public static class ExistingPagesHelper
{
public static IEnumerable<PageType> GetAllPageTypes()
{
var contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
return contentTypeRepository.List().OfType<PageType>();
}
public static void SetPagesForPageTypeName(ExistingPagesReportViewModel model)
{
var criterias = new PropertyCriteriaCollection();
var criteria = new PropertyCriteria();
criteria.Condition = CompareCondition.Equal;
criteria.Name = "PageTypeID";
criteria.Type = PropertyDataType.PageType;
criteria.Value = model.SelectedPageType;
criteria.Required = true;
criterias.Add(criteria);
var pages = DataFactory.Instance.FindPagesWithCriteria(ContentReference.RootPage, criterias);
model.Pages = pages;
}
}
After that I updated my index action so it created an instance of the view model and assigned all pagetypes to it. So now it looks like this.
public ActionResult Index()
{
var model = new ExistingPagesReportViewModel { PageTypes = ExistingPagesHelper.GetAllPageTypes() };
return View(model);
}
Now I created my view and since I wanted it to feel the same way as the built in reports I added Epi’s css and javascript to it. The complete view looks like this:
@model ExistingPagesReportViewModel
@using EPiServer.DataAbstraction
@using EPiServer.Framework.Web.Resources
@{
Layout = null;
}<!DOCTYPE html><html><head><title>@ViewBag.Title</title><meta http-equiv="X-UA-Compatible" content="IE=Edge" /><!-- Shell -->
@Html.Raw(ClientResources.RenderResources("ShellCore"))<!-- LightTheme -->
@Html.Raw(ClientResources.RenderResources("ShellCoreLightTheme"))<link href="/EPiServer/CMS/App_Themes/Default/Styles/system.css" type="text/css" rel="stylesheet"><link href="/EPiServer/CMS/App_Themes/Default/Styles/ToolButton.css" type="text/css" rel="stylesheet"></head><body>
@Html.Raw(Html.ShellInitializationScript())<div class="epi-contentContainer epi-padding"><div class="epi-contentArea"><div class="EP-systemImage" style="background-image: url('/App_Themes/Default/Images/ReportCenter/PublishedPages.gif');"><h1 class="EP-prefix">
Existing Pages</h1><p class="EP-systemInfo">
This report displays pages that exists on the site.</p></div><div id="FullRegion_ValidationSummary" class="EP-validationSummary" style="color: Black; display: none;"></div></div>
@using (Html.BeginForm("ListPages", "ExistingPagesReport", FormMethod.Post))
{<script src="/Util/javascript/episerverscriptmanager.js" type="text/javascript"></script><script src="/EPiServer/CMS/javascript/system.js" type="text/javascript"></script><script src="/EPiServer/CMS/javascript/dialog.js" type="text/javascript"></script><script src="/EPiServer/CMS/javascript/system.aspx" type="text/javascript"></script><input type="hidden" id="doExport" name="doExport" value="False"><div class="epi-formArea"><fieldset><legend>
Report Criteria</legend><div class="epi-size10"><label for="pageTypes">Select PageType</label><select name="pageType" id="pageType">
@foreach (var type in Model.PageTypes.Where(w => w.ID != 1).OrderBy(o => o.Name))
{<option value="@type.ID" @(type.ID.ToString() == Model.SelectedPageType ? "selected=selected" : "") >@type.Name</option>
}</select></div></fieldset><div class="epitoolbuttonrow"><span class="epi-cmsButton"><input class="epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Report" type="submit" name="showReport" id="showReport" value="Show Report" onmouseover="EPi.ToolButton.MouseDownHandler(this)" onmouseout="EPi.ToolButton.ResetMouseDownHandler(this)" /></span><span class="epi-cmsButton"><input class="epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Report" type="submit" name="exportReport" id="exportReport" value="Export Report" onmouseover="EPi.ToolButton.MouseDownHandler(this)" onmouseout="EPi.ToolButton.ResetMouseDownHandler(this)" /></span></div></div>
}
@if (Model.Pages != null && Model.Pages.Count > 0)
{<div class="epi-floatLeft epi-marginVertical-small">Number of Hits: @Model.Pages.Count</div><div class="epi-contentArea epi-clear"><div><table class="epi-default epi-default-legacy" cellspacing="0" id="FullRegion_MainRegion_ReportView" style="border-style: None; width: 100%; border-collapse: collapse;"><tr><th scope="col">Page Id</th><th scope="col">Page Name</th><th scope="col">Page Url</th><th scope="col">Published Date</th></tr>
@foreach (var page in Model.Pages)
{<tr><td style="width: 27%;">@page.ContentLink.ID</td><td>@page.PageName</td><td>@Url.ContentUrl(page.ContentLink)</td><td>@(page.StartPublish.HasValue ? page.StartPublish.Value.ToString("yyyy-MM-dd HH:mm") : "Not published")</td></tr>
}</table></div></div>
}</div><script type="text/javascript">
document.getElementById("exportReport").onclick = function () {
document.getElementById("doExport").value = "True";
};
document.getElementById("showReport").onclick = function () {
document.getElementById("doExport").value = "False";
};</script></body></html>
As you can see I have two submit buttons but only one form actions and because of that I have connected javascript function to the click event on the export and search submit button that updates a hidden value telling the controller to do an export of the data or not.
The action in the controller that handles this looks like this:
[HttpPost]
public ActionResult ListPages(FormCollection form)
{
var model = new ExistingPagesReportViewModel
{
PageTypes = ExistingPagesHelper.GetAllPageTypes(),
SelectedPageType = form["pageType"]
};
ExistingPagesHelper.SetPagesForPageTypeName(model);
var doExport = false;
if (bool.TryParse(form["doExport"], out doExport) && doExport && model.Pages != null && model.Pages.Count > 0)
{
Export(model.Pages, System.Web.HttpContext.Current.Response);
}
return View("Index", model);
}
As you can see it returns the view if the export values is not true. To do the export I use EPPlus and for this simple export I just created a function in the same class that looks like this:
public void Export(PageDataCollection pagesToExport, HttpResponse response)
{
using (var package = new ExcelPackage())
{
ExcelWorksheet ws = package.Workbook.Worksheets.Add("pages");
ws.Cells[1, 1].Value = "PageId";
ws.Cells[1, 2].Value = "PageName";
ws.Cells[1, 3].Value = "PageUrl";
ws.Cells[1, 4].Value = "Published Date";
ws.Row(1).Style.Font.Bold = true;
ws.Row(1).Style.Locked = true;
int row = 2;
foreach (var page in pagesToExport)
{
ws.Cells[row, 1].Value = page.ContentLink.ID;
ws.Cells[row, 2].Value = page.PageName;
ws.Cells[row, 3].Value = Url.ContentUrl(page.ContentLink);
ws.Cells[row, 4].Value = page.StartPublish.HasValue ? page.StartPublish.Value.ToString("yyyy-MM-dd HH:mm") : "Not published";
++row;
}
response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
response.AddHeader("content-disposition", string.Format("attachment; filename=pages{0}.xlsx", DateTime.Now.ToString("yyyyMMdd")));
response.BinaryWrite(package.GetAsByteArray());
response.Flush();
response.End();
}
}
This is a simple example on how to make a custom report in MVC with an Excel export function in it. Hope that it will help you get started and make your own great reports!
Happy coding!!