Monday, May 18, 2009

ASP NET MVC - Print excel file using aspx template

My friend met a problem with creating excel report using ASPX as view in ASP.NET MVC. He already built an action to view printer friendly report and wanted to make use of that view for excel output.
To do that, he decide to get string output from the printer friendly action (by combine model + aspx view) and return the output content as binary with a little bit change in content type ("application/vnd.ms-excel")

Unfortunately, ASP NET MVC does not provide any official way to build string content from a View + Model data.

To support him, I read many articles and found a helpful one on Stackoverflow:

http://stackoverflow.com/questions/483091/render-a-view-as-a-string

However, solutions there are not good ways. Why? Actually, they try to use current context to render the view+model. After that, if you try to Redirect or return a FileContentResult or StreamContentResult, you will meet this exception:

Server cannot set content type after HTTP headers have been sent

Or

Server cannot redirect to action after HTTP headers have been sent

So, how to solve this issue? Finally, I found a very stupid but simple solution:

1. Create 2 actions: 1 action for returning Stream content, 1 action to return HTML content from aspx page + model
2. In the returning Stream action, create a HTTP request to the URL of the action that render the HTML for report by System.Net.WebClient => get return HTML
2. Using the return HTML to create the stream + set content type and return to browser

Below is the code

[AcceptVerbs("GET", "POST")]
public ActionResult ViewExcelReport(int? month, int? year)
{
   string excelFileStringContent = DownloadReportHTMLContent(month.Value, 
                                   year.Value);
   byte[] excelFileBytesContent = this.Response.ContentEncoding.GetBytes (excelFileStringContent);
   FileContentResult excelFileContentResult = new FileContentResult(
                                        excelFileBytesContent,
                                       "application/vnd.ms-excel");
   return excelFileContentResult;
}
        
[AcceptVerbs("GET", "POST")]
public ActionResult ViewHTMLReport(int? month, int? year) {
   PrepareReportData(month.Value, year.Value);
   return View();
}

private string DownloadReportHTMLContent(int month, int year)
{
    string linkFormat = Url.Action("ViewHTMLReport", 
                                   "MyReport", 
                                    null, 
                                   "http");
   linkFormat += "?month={0}&year={1}";
   System.Net.WebClient webClient = new System.Net.WebClient();
   webClient.Encoding = Encoding.UTF8;
   return webClient.DownloadString(string.Format(linkFormat, month, year));
}

Is that simple?

8 comments:

Anonymous said...

Khoa, how to handle the security of the url that you use to render html ?

Tran Dang Khoa said...

It's very simple! You can check security in that action. If user is anonymous, you can return an error or a message.

It likes the way you protect the printer friendly version page.

Alex said...

In this situation recommend to use this software-corrupted repair Excel recovery tool,because when my friends were in same situation,program helped their,tool is free as far as I can see,it can be easily solved with Excel recovery software for recovery xls,will allow you to Excel files recovery and make sure, that it works and that your work results can now be recovered just in several clicks,allows to save many hours of your work for recovery for Excel download and recover everything, that was done before.

Adrian Grigore said...

Thanks for the great post! I was close to installing Spark as a second view engine to render views to strings before finding this.

Vi Nguyễn said...

Sorry, i'm type by VietNamese. My English is poor.
Chào a Khoa ! Giải pháp của anh đơn giản và hiệu quả trong trường hợp này.
THật sự cách này về bản chất cũng giống như cách Fake HttpResponse đã đề cập trên stackoveflow.com , bởi anh dùng WebClient lấy kết quả trả về từ 1 Request. Trong khi cách kia render Action vào 1 HttpResponse giả. Tuy nhiên ta nên sử dụng WebClient đúng bản chất của nó, tức là Request tới 1 Host khác, ở đây có lẻ dùng Fake HttpResponse hợp lý hơn.
Trên đây chỉ là nhận xét của em. Cả hai cách đều đáng học hỏi. Xin cảm ơn các bạn đã chỉ giáo.
Em tiếp xúc với ASP.net-MVC được 1 tháng nay, cảm thấy thật thú vị. Và đang dự định làm đồ án tốt nghiệp với 1 CMS trên nền tảng này.
Thật sự em rất thích kiến trúc Component-Base của Joomla giống như anh. Tuy nhiên sau khi tham khảo 1 số sách MVC (Manning in Action, Pro MVC) vẫn chưa thể hiện thực kiến trúc này. Em đã tham khảo SubController của thư viện MvcContrib. Nói chung đã hiện thực gần giống 1 phần. Tuy nhiên còn 1 điều khó khăn.
Vấn đề ở đây tương tự như chủ đề đang bàn. Tức là lấy về nội dung Html của một hành động. Từ đó ta sẽ găn nó vào MasterPage tự động.
Điều cần thiết là phải lấy về nội dung Html. Bởi chỉ như thế mới gắn vào các vị trí position đã cấu hình bên ngoài, vào MasterPage 1 cách tự động được. (giống như Joomla, chỉ cần chỉ định Position của 1 module trong trang Admin). Nếu không cần chức năng này. Tức là Render ở một vị trí cụ thể trên trang MasterPage ta có thể sử dụng RenderAction hoặc Action.Invoke(Execute(MyRequest)) -> SubController là đã giải quyết được.
Sử dụng cách của anh thì giải quyết được vấn đề tuy nhiên về mặt hiệu năng chắc chắn không tốt. Không thể vì theo đuổi kiến trúc Joomla mà đưa ra 1 giải pháp ko tốt.
Hi vọng với kinh nghiệm của anh có thể tư vấn cho em một cách để hoàn thành mong muốn của mình.

Thân chào các bạn ! Vi Nguyễn

Vi Nguyễn said...

Sorry, i'm type by VietNamese. My English is poor.
Chào a Khoa ! Giải pháp của anh đơn giản và hiệu quả trong trường hợp này.
THật sự cách này về bản chất cũng giống như cách Fake HttpResponse đã đề cập trên stackoveflow.com , bởi anh dùng WebClient lấy kết quả trả về từ 1 Request. Trong khi cách kia render Action vào 1 HttpResponse giả. Tuy nhiên ta nên sử dụng WebClient đúng bản chất của nó, tức là Request tới 1 Host khác, ở đây có lẻ dùng Fake HttpResponse hợp lý hơn.
Trên đây chỉ là nhận xét của em. Cả hai cách đều đáng học hỏi. Xin cảm ơn các bạn đã chỉ giáo.
Em tiếp xúc với ASP.net-MVC được 1 tháng nay, cảm thấy thật thú vị. Và đang dự định làm đồ án tốt nghiệp với 1 CMS trên nền tảng này.
Thật sự em rất thích kiến trúc Component-Base của Joomla giống như anh. Tuy nhiên sau khi tham khảo 1 số sách MVC (Manning in Action, Pro MVC) vẫn chưa thể hiện thực kiến trúc này. Em đã tham khảo SubController của thư viện MvcContrib. Nói chung đã hiện thực gần giống 1 phần. Tuy nhiên còn 1 điều khó khăn.
Vấn đề ở đây tương tự như chủ đề đang bàn. Tức là lấy về nội dung Html của một hành động. Từ đó ta sẽ găn nó vào MasterPage tự động.
Điều cần thiết là phải lấy về nội dung Html. Bởi chỉ như thế mới gắn vào các vị trí position đã cấu hình bên ngoài, vào MasterPage 1 cách tự động được. (giống như Joomla, chỉ cần chỉ định Position của 1 module trong trang Admin). Nếu không cần chức năng này. Tức là Render ở một vị trí cụ thể trên trang MasterPage ta có thể sử dụng RenderAction hoặc Action.Invoke(Execute(MyRequest)) -> SubController là đã giải quyết được.
Sử dụng cách của anh thì giải quyết được vấn đề tuy nhiên về mặt hiệu năng chắc chắn không tốt. Không thể vì theo đuổi kiến trúc Joomla mà đưa ra 1 giải pháp ko tốt.
Hi vọng với kinh nghiệm của anh có thể tư vấn cho em một cách để hoàn thành mong muốn của mình.

Thân chào các bạn ! Vi Nguyễn

Tran Dang Khoa said...

Hi Vi,
Rất vui được làm quen với em. Trước hết thanks em đã quan tâm và đóng góp ý kiến cho bài viết của anh.

Anh có một số ý để trả lời cho câu hỏi của em như sau:
1. Cách sử dụng WebClient này chỉ là một trick để giải quyết nhanh vấn đề của việc "lấy html content từ một action" trong ASP.NET MVC - đây không phải là cách chính quy (official way :)). Anh nghĩ ASP.NET MVC sẽ nhanh chóng update để fix issue này thôi.
2. ASP.NET MVC hiện tại chưa cung cấp đủ API để hiện thực được kiến trúc module based như Joomla. Cần phải có một API cho phép lầy content từ Action. Tuy nhiên, nếu muốn em vẫn có thể làm được bằng 1 trong 2 cách:
a. Sử dụng fake request giống như bài viết này. Về hiệu quả, nếu site vừa và nhỏ, thì vẫn ok. Vì bản chất request là localhost, nên việc xử lý và truyền dữ liệu khá nhanh. Tuy nhiên sẽ dẫn tới một hậu quả là việc xử lý cho 1 request bên ngoài sẽ phát sinh n local request.
b. Đục code của ASP.NET MVC và viết 1 hàm render html content từ Action. Anh chưa có thời gian để xem cách này, tuy nhiên anh nghĩ vẫn khả thi.

Tuy nhiên, anh nghĩ nếu em muốn hiện thực cấu trúc module based như Joomla, thì em nên dùng ASP.NET sẽ tiện hơn, tự nhiên hơn và ít tốn thời gian hơn.

Công nghệ mới có thể hay, tuy nhiên việc lựa chọn nó cho một giải pháp đòi hỏi phải cân nhắc nhiều yếu tố, mà quan trọng nhất là yếu tố kinh tế (thời gian, tiền bạc, nhân lực,...).
Chúc em làm luận văn tốt.

Thân chào em

Vi Nguyễn said...

Xin cảm ơn những gợi ý của anh ! Quả thật em cũng suy nghĩ giống anh, nhưng vì chưa có tí kinh nghiệm làm việc nào, cũng như tầm nhìn còn thấp nên không dám quyết định sẽ lựa chọn giải pháp ra sao.
Lúc trước cũng định sử dụng Webpart của .Net , nhưng thấy MVC hấp dẫn nên lại vọc MVC. Đọc xong lại thấy ông MVC 1.0 này hỗ trợ còn ít quá. Tuy nhiên e quyết định theo gợi ý B của anh, chắc là sẻ được.
Cảm ơn đã giúp đở.! Vi nguyễn