29 January 2010 HowTo; ASP.NET MVC; MVC; Excel Robert Muehsig

imageMan kann relativ einfach auf seiner Seite einen Excel-Export bei tabellarischen Daten anbieten ohne großartige SDKs zu wälzen. Excel versteht von Haus aus auch HTML Tabellen. Man ist zwar eingeschränkt, aber es ist schnell gemacht. Bei einer ASP.NET MVC Anwendung wäre es nun noch schön, dass man das Markup der HTML Tabelle als View speichert. Über eine kleine Extension kann man sich den View auch als String ausgeben lassen.

Unser Ziel: Excel Export

Wir möchten auf unserer Webseite einen Excelexport anbieten. Etwas ähnliches, nur etwas kurioser gemacht, habe ich auch schonmal auf dem Blog geschrieben.

Was muss man dafür tun?

Im Grunde reicht es aus, wenn man in die Response eine HTML Tabelle rendert und als Contenttype "application/ms-excel" angibt. Wir möchten aber die Tabelle als View speichern um Sie einfacher editieren zu können und nicht hardcoded im Source Code zu pflegen.

RenderViewToString

Wir benötigen zu aller erst eine Komponente die einen View als String uns zurückgibt. Wichtig dabei ist, dass die Response während des Vorgangs nicht schon zum Client geschickt wird (Response.Flush()), da man ansonsten den Contenttyp nicht mehr ändern kann! Eine der Lösungen in diesem Thread hat auch funktioniert - in ASP.NET MVC 2.0 muss man allerdings eine kleine Sache ändern -> Siehe Stackoverflow.

/// <summary>Renders a view to string.</summary>
public static string RenderViewToString(this Controller controller,
                                        string viewName, object viewData) {
    //Create memory writer
    var sb = new StringBuilder();
    var memWriter = new StringWriter(sb);

    //Create fake http context to render the view
    var fakeResponse = new HttpResponse(memWriter);
    var fakeContext = new HttpContext(HttpContext.Current.Request, fakeResponse);
    var fakeControllerContext = new ControllerContext(
        new HttpContextWrapper(fakeContext),
        controller.ControllerContext.RouteData,
        controller.ControllerContext.Controller);

    var oldContext = HttpContext.Current;
    HttpContext.Current = fakeContext;

    //Use HtmlHelper to render partial view to fake context
    var html = new HtmlHelper(new ViewContext(fakeControllerContext,
        new FakeView(), new ViewDataDictionary(), new TempDataDictionary()),
        new ViewPage());
    html.RenderPartial(viewName, viewData);

    //Restore context
    HttpContext.Current = oldContext;    

    //Flush memory and return output
    memWriter.Flush();
    return sb.ToString();
}

/// <summary>Fake IView implementation used to instantiate an HtmlHelper.</summary>
public class FakeView : IView {
    #region IView Members

    public void Render(ViewContext viewContext, System.IO.TextWriter writer) {
        throw new NotImplementedException();
    }

    #endregion
}

Nun können wir über diese Methode einen String aus einem View erzeugen - schonmal nicht schlecht.

Jetzt kann man entweder direkt das ContentResult nutzen oder man baut sich sein eigens ExcelResult:

public class ExcelResult : ActionResult
    {
        public string FileName { get; set; }
        public string Content { get; set; }

        public ExcelResult(string filename, string content)
        {
            this.FileName = filename;
            this.Content = content;
        }

        public override void ExecuteResult(ControllerContext context)
        {
            WriteFile(this.FileName, "application/ms-excel", this.Content);
        }

        private static void WriteFile(string fileName, string contentType, string content)
        {

            HttpContext context = HttpContext.Current;
            context.Response.Clear();
            context.Response.AddHeader("content-disposition", "attachment;filename=" + fileName);
            context.Response.Charset = "";
            context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
            context.Response.ContentType = contentType;
            context.Response.Write(content);
            context.Response.End();
        }
    }

Die Anwendung

Folgender View ist unser Exceltemplate (mit einem ViewModel)

%@ Import Namespace="MvcRenderToString.Models"%>
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IList<ExcelData>>" %>

<table>
    <tr>
        <% foreach(ExcelData excel in Model) { %>
        <td><%=excel.Foobar %></td>
        <% } %>
    </tr>
</table>

Und der Controller:

        public ExcelResult Excel()
        {
            List<ExcelData> foobars = new List<ExcelData>();
            foobars.Add(new ExcelData() { Foobar = "HelloWorld!"});
            foobars.Add(new ExcelData() { Foobar = "HelloWorld!" });
            foobars.Add(new ExcelData() { Foobar = "HelloWorld!" });
            foobars.Add(new ExcelData() { Foobar = "HelloWorld!" });

            string content = this.RenderViewToString("Excel", foobars);
            return new ExcelResult("Foobar.xls", content);
        }

Das Ergebnis:

image

Einziges Manko ist, dass Excel dem nicht ganz vertraut. Leider ist das ByDesign so:

image

Bei Bestätigung auf "Ja":

image

Fetzt, oder?

Weitere Anwendungsmöglichkeiten

Dadurch das wir eine HTML Tabelle benutzen, können wir die Tabelle auch genauso auf unserer normalen Webseite benutzen. Hierbei Rufe ich dann einfach RenderPartial etc. auf.

Man könnte so auch Email Templates als View ablegen. Die Möglichkeiten sind jedenfalls da :)

[ Download Democode ]


Written by Robert Muehsig

Software Developer - from Dresden, Germany, now living & working in Switzerland. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de

If you like the content and want to support me you could buy me a beer or a coffee via Litecoin or Bitcoin - thanks for reading!