World is now on Opti ID! Learn more

Henrik Fransas
Mar 19, 2016
  4272
(0 votes)

Create custom report in MVC with Excel Export

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!!

Mar 19, 2016

Comments

Please login to comment.
Latest blogs
Make Global Assets Site- and Language-Aware at Indexing Time

I had a support case the other day with a question around search on global assets on a multisite. This is the result of that investigation. This co...

dada | Jun 26, 2025

The remote server returned an error: (400) Bad Request – when configuring Azure Storage for an older Optimizely CMS site

How to fix a strange issue that occurred when I moved editor-uploaded files for some old Optimizely CMS 11 solutions to Azure Storage.

Tomas Hensrud Gulla | Jun 26, 2025 |

Enable Opal AI for your Optimizely products

Learn how to enable Opal AI, and meet your infinite workforce.

Tomas Hensrud Gulla | Jun 25, 2025 |

Deploying to Optimizely Frontend Hosting: A Practical Guide

Optimizely Frontend Hosting is a cloud-based solution for deploying headless frontend applications - currently supporting only Next.js projects. It...

Szymon Uryga | Jun 25, 2025

World on Opti ID

We're excited to announce that world.optimizely.com is now integrated with Opti ID! What does this mean for you? New Users:  You can now log in wit...

Patrick Lam | Jun 22, 2025

Avoid Scandinavian Letters in File Names in Optimizely CMS

Discover how Scandinavian letters in file names can break media in Optimizely CMS—and learn a simple code fix to automatically sanitize uploads for...

Henning Sjørbotten | Jun 19, 2025 |