The most straightforward way to download a file in mvc - asp.net

I am currently saving an excel file like so on my c drive.
public ActionResult Export()
{
try
{
Excel.Application application = new Excel.Application();
Excel.Workbook workbook = application.Workbooks.Add(System.Reflection.Missing.Value);
Excel.Worksheet worksheet = workbook.ActiveSheet;
var people = db.People.ToList();
worksheet.Cells[1, 1] = "Last Name";
worksheet.Cells[1, 2] = "First Name";
int row = 2;
foreach (var person in people)
{
worksheet.Cells[row, 1] = person.PersonFName;
worksheet.Cells[row, 2] = person.PersonLName;
row++;
}
workbook.SaveAs("c:\\test\\worksheet.xls");
workbook.Close();
Marshal.ReleaseComObject(workbook);
application.Quit();
Marshal.FinalReleaseComObject(application);
ViewBag.Result = "Done";
}
catch(Exception ex)
{
ViewBag.Result = ex.Message;
}
return File("c:\\test\\workseet.xls", "application/vnd.ms-excel", "workseet.xls");
// return View("Success");
}
I can go to c:\\test\workseet.xls and it exists there I can do what ever with it...
I am wanting to transform my method from return a view to return a file download...
I figured that it was as simple as this:
return File("c:\\test\\workseet.xls", "application/vnd.ms-excel", "workseet.xls");
However when I do this method and click the link to download, it gives me this error.
The process cannot access the file 'c:\test\workseet.xls' because it is being used by another process.

This duplicate question is just one of those that show how to use EPPlus to generate Excel files on the server side in a scaleable manner. It's actually a lot easier than using Excel interop and a lot faster. You don't even have to save the file to the disk.
public ActionResult ExportData()
{
//Somehow, load data to a DataTable
using (ExcelPackage package = new ExcelPackage())
{
var ws = package.Workbook.Worksheets.Add("My Sheet");
//true generates headers
ws.Cells["A1"].LoadFromDataTable(dataTable, true);
//Save the workbook to a stream
var stream = new MemoryStream();
package.SaveAs(stream);
string fileName = "myfilename.xlsx";
string contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
stream.Position = 0;
return File(stream, contentType, fileName);
}
}
You can use LoadFromDataTable to fill a sheet from a data table or LoadFromCollection to load data from a collection, eg List<Sale>.
Both methods return an ExcelRange object (a range of cells) that you can use to format individual cells, rows, and columns. You can also create tables from a range and apply themes.
The duplicate goes even farther and shows how you can avoid even the MemoryStream

Related

Spire.xls - return a File from a stream for client download

Here is what I'm trying to accomplish.
I am creating an asp.net MVC application. My restrictions are that I cannot programmatically save anything to the file structure of the server, so I can't save it as a physical file on the host, and then grab it for client download.
I am loading a PDF to a stream, extracting information from the PDF, dynamically building an excel file, and then offering the file for download to the client. My code is below.
// Loads the incoming PDF document to stream
PdfDocument doc = new PdfDocument();
using (var stream = model.BudgetPdfFile.OpenReadStream())
{
doc.LoadFromStream(stream);
}
var pageCount = doc.Pages.Count;
var date = DateTime.Now.ToShortDateString().Replace("/", "-");
// Extracts data from the PDF and separates it by NewLine
SimpleTextExtractionStrategy strategy = new SimpleTextExtractionStrategy();
StringBuilder allText = new StringBuilder();
for (var i = 0; i < pageCount; i++)
{
allText.Append(doc.Pages[i].ExtractText(strategy));
}
var fullDocText = allText.ToString();
List<string> linesList = new List<string>(fullDocText.Split(new[] { Environment.NewLine }, StringSplitOptions.None).ToList());
// generates a comparison list for output data manipulation from static data
var finalList = linesList.BuildFinalList(budgetItems);
// creates a new Spire.PDF.Workbook for the final output excel file
var result = new Workbook();
// checks for whether the submitted budget is for a case in litigation or not and builds the correct excel workbook
if (model.isTrial)
{
result = ExportExcelBudget.TrialBudgetSheet(model, finalList);
}
else
{
result = ExportExcelBudget.PreTrialBudgetSheet(model, finalList);
}
Absolutely everything up to the last section below works perfectly. However, I cannot figure out how to load the workbook into a new stream and then return the file for download.
// saves the final workbook to a stream and offers it for download to the client
Stream outStream = new MemoryStream();
var fileName = "Budget Report_" + model.ClaimNumber + "_" + date + ".xlsx";
var contentType = "application/vnd.ms-excel";
result.SaveToStream(outStream, Spire.Xls.FileFormat.Version2016);
return File(outStream, contentType, fileName);
I've searched and tried multiple different variations but when the application hits the return File(), it returns a null.
I've stepped through execution and the results seem to be there, but it's not passing anything. Any help on what is wrong here would be greatly appreciated.
Stream outStream = new MemoryStream();
var fileName = "Budget Report_" + model.ClaimNumber + "_" + date + ".xlsx";
var contentType = "application/vnd.ms-excel";
result.SaveToStream(outStream, Spire.Xls.FileFormat.Version2016);
**outStream.Position = 0;**
return File(outStream, contentType, fileName);
Had to reset the stream position to 0. Working perfectly now.

How to build and stream a csv string as a file to a client (.net c#)

I have the following method for talking a list of a SomeMetric model and converting it to a csv string. The method write the csv string as a file to HttpResponseMessage:
private HttpResponseMessage ConvertToCsvFileResponse(IEnumerable<SomeMetric> filterRecords, string fileName)
{
var csvBuilder = new StringBuilder();
//write the header
csvBuilder.AppendLine("ColA,ColB,ColC,ColD,ColE");
// Write the data lines.
foreach (var record in filterRecords)
csvBuilder.AppendFormat("{0},{1},{2},{3},{4}{5}", record.A, record.B, record.C, record.D, record.E, Environment.NewLine);
// Convert to Http response message for outputting from api.
HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment"); //attachment will force download
result.Content.Headers.ContentDisposition.FileName = fileName;
result.Content = new StringContent(csvBuilder.ToString());
return result;
}
This method is called from my API controller and the response is output to the client as follows:
[HttpGet]
public HttpResponseMessage GetExampleCsv()
{
try
{
var entries = _auditTableStorage.ListEntities<SomeMetric>();
return ConvertToCsvFileResponse(entries.ToList(), "Example.csv");
}
catch (Exception ex)
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError) {
Content = new StringContent($"Problem occurred connecting to storage: {ex.Message}")
};
}
}
I generate the data by querying Azure Table Storage - but that's kind of irrelevant for my question.
The data comes from a data source in a paginated format. My current approach is to gather all the data together first (paging through until the end) to generate a list (IEnumerable is returned and .ToList(); is called). This data is then passed to the ConvertToCsv method.
As you can imagine, building the dataset up front in this way is Ok for small to medium size data sets BUT quickly becomes inefficient for large datasets.
Ideally, I'd like to modify my code to take chunks of the CSV string as it's being built and stream that down as file to the client making the request - just not sure how to convert what I have into a streamed version. Any points greatly appreciated!
NOTE: The code for generating csv here is only for demonstration purposes. I could integrate an object to CSV parser but wanted to show the string generation and dropping that out as it happens.
For anyone else looking to get an example of this, here's how I achieved streaming content to the client browser as it was being built:
private HttpResponseMessage ConvertToCsvFileResponse(IEnumerable<SomeMetric> filterRecords, string fileName)
{
HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
result.Content = new PushStreamContent((outputStream, httpContext, transportContext) =>
{
using (StreamWriter sWriter = new StreamWriter(outputStream, UnicodeEncoding.ASCII))
{
sWriter.Write("A,B,C,D,E");
foreach (var record in filterRecords)
sWriter.Write($"{Environment.NewLine}{record.A},{record.B},{record.C},{record.D},{record.E}");
}
});
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
result.Content.Headers.ContentDisposition.FileName = fileName;
return result;
}

Issue with Returning Epplus spreadsheets with an Image in .zip file using DotNetZip

This scenario works fine without any images on the spreadsheet, but after attempting to add an image to the spreadsheets that get put in the zip file, the spreadsheets open with the excel error of "We found a problem with some content ....".
I have other methods using Epplus without DotNetZip that use the exact same code to insert the image into a spreadsheet and they work fine with no errors or issues.
Code that works to return a single spreadsheet with an image
public async Task<ActionResult> GenerateSpreadsheet(ReportViewModel reportViewModel)
{
using (var excelPackage = new ExcelPackage())
{
Bitmap logoFile = getLogoFile();
var companyLogo = worksheet.Drawings.AddPicture("File Name", logoFile);
companyLogo.From.Column = columnIndex - 4;
companyLogo.From.Row = rowIndex;
companyLogo.SetSize(logoFile.Width, logoFile.Height);
//Write all the stuff to the spreadsheet
Response.ClearContent();
Response.BinaryWrite(excelPackage.GetAsByteArray());
string fileName = "attachment;filename=Project_Report_Export.xlsx";
Response.AddHeader("content-disposition", fileName);
Response.ContentType = "application/excel";
Response.Flush();
Response.End();
}
}
Code that will build a spreadsheet, add it to a zip file, but where the spreadsheet will open with the "We found a problem with some content ...." if an image was added to the spreadsheet as shown below. If there is no image added to it, it will open without the error.
public async Task<ActionResult> GenerateSpreadsheet(ReportViewModel reportViewModel)
{
using (var stream = new MemoryStream())
{
using (ZipFile zip = new ZipFile())
{
foreach(var spreadSheet in listOfStuffToBuildFrom)
{
using (var excelPackage = new ExcelPackage())
{
Bitmap logoFile = getLogoFile();
var companyLogo = worksheet.Drawings.AddPicture("File Name", logoFile);
companyLogo.From.Column = columnIndex - 4;
companyLogo.From.Row = rowIndex;
companyLogo.SetSize(logoFile.Width, logoFile.Height);
//Write all the stuff to the spreadsheet
//Add the workbook to the zip file
zip.AddEntry(excelPackage.Workbook.Properties.Title, excelPackage.GetAsByteArray());
}
}
zip.Save(stream);
return File(stream.ToArray(), System.Net.Mime.MediaTypeNames.Application.Zip, "Project Reports.zip");
}
}
}
Why does the second method return spreadsheets that open with the error "We found a problem with some content ...."??

ASP.NET MVC - Save Excel Contents to Database

I would like to add a functionality to my web application where user can upload an excel file that would probably look like this (i will be supplying an excel file template that the user can fill up):
Name Address Phone
John California 000-111
Matt Seattle 000-222
...
And pass each line after the headings Name-Address-Phone to my registration controller or service. Now my question is what would be the best way (for user experience; speed is important) to do this?
I am not looking for a full working code, I'm just looking for the most efficient approach with regards to speed.
You should create a stored procedure which holds the insert logic.Once the user uploads the Excel file you can open it using Excel interop, get the captured data and call the insert stored procedure.If you need even faster insert speeds you can always use SQL Bulk Copy.
1.In your MVC project add a reference to Microsoft.Office.Interop.Excel.dll
2.Create a View which prompts the user to upload an Excel file.
#using (Html.BeginForm("Import", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.Raw(ViewBag.Error)
<span>Excel File </span><input type="file" name="excelfile" />
<br />
<input type="submit" value="Import" />
}
3.Create a controller action which opens the Excel file -> loops through the rows -> inserts the data into SQL
[HttpPost]
public ActionResult Import(HttpPostedFileBase excelFile)
{
//Add this to the using statements after adding a refrence to Microsoft.Office.Interop.Excel.dll - using Excel = Microsoft.Office.Interop.Excel;
if ((excelFile.ContentLength != 0) && (excelFile.FileName.EndsWith("xls") || excelFile.FileName.EndsWith("xlsx")))
{
string path = Server.MapPath("~/Files/" + excelFile.FileName);
if (!System.IO.File.Exists(path))
{
excelFile.SaveAs(path);
Excel.Application application = new Excel.Application();
Excel.Workbook workbook = application.Workbooks.Open(path);
Excel.Worksheet worksheet = (Excel.Worksheet)workbook.ActiveSheet;
Excel.Range range = worksheet.UsedRange;
for (int i = 2; i < range.Rows.Count + 1; i++)
{
string name = ((Excel.Range)range.Cells[i,1]).Text;
string address = ((Excel.Range)range.Cells[i,2]).Text;
string phone = ((Excel.Range)range.Cells[i,3]).Text;
//Write the logic to add the values to the database
}
}
}
return View();
}
You could also make the action asynchronous but it doesn't really help because writing to a database is an IO operation so async won't do anything.
You can make use of OleDb to connect to your excel file. After successfull connection you can populate a DataTable with the rows in your excel file and then do a foreach to pass each row to your controller.
void Process(string path)
{
try
{
string connString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + path + ";Extended Properties=\"Excel 8.0;HDR=Yes;IMEX=1\";";
using (var connection = new OleDbConnection(connString))
{
using (var command = new OleDbCommand("select * from [SheetName$]", connection))
{
connection.Open();
using (var adapter = new OleDbDataAdapter(command))
{
DataTable dataTable = new DataTable();
adapter.Fill(dataTable);
foreach (DataRow row in dataTable.Rows)
{
string Name, Address, Phone = string.Empty;
try
{
Name = row["Name"].ToString();
Address = row["Address"].ToString();
Phone = row["Phone"].ToString();
//new RegistrationController(Name, Address, Phone);
}
catch (Exception ex)
{
// Handle exceptions
}
}
}
}
}
}
// Might be possibly thrown when opening connection
catch (OleDbException ex)
{
// Handle exceptions
}
// Might be possibly thrown when filling datatable
catch (InvalidOperationException ex)
{
// Handle exceptions
}
}

Biztalk 2010 Custom Pipeline Component returns binary

I have created a custom pipeline component which transforms a complex excel spreadsheet to XML. The transformation works fine and I can write out the data to check. However when I assign this data to the BodyPart.Data part of the inMsg or a new message I always get a routing failure. When I look at the message in the admin console it appears that the body contains binary data (I presume the original excel) rather than the XML I have assigned - see screen shot below. I have followed numerous tutorials and many different ways of doing this but always get the same result.
My current code is:
public Microsoft.BizTalk.Message.Interop.IBaseMessage Execute(Microsoft.BizTalk.Component.Interop.IPipelineContext pc, Microsoft.BizTalk.Message.Interop.IBaseMessage inmsg)
{
//make sure we have something
if (inmsg == null || inmsg.BodyPart == null || inmsg.BodyPart.Data == null)
{
throw new ArgumentNullException("inmsg");
}
IBaseMessagePart bodyPart = inmsg.BodyPart;
//create a temporary directory
const string tempDir = #"C:\test\excel";
if (!Directory.Exists(tempDir))
{
Directory.CreateDirectory(tempDir);
}
//get the input filename
string inputFileName = Convert.ToString(inmsg.Context.Read("ReceivedFileName", "http://schemas.microsoft.com/BizTalk/2003/file-properties"));
swTemp.WriteLine("inputFileName: " + inputFileName);
//set path to write excel file
string excelPath = tempDir + #"\" + Path.GetFileName(inputFileName);
swTemp.WriteLine("excelPath: " + excelPath);
//write the excel file to a temporary folder
bodyPart = inmsg.BodyPart;
Stream inboundStream = bodyPart.GetOriginalDataStream();
Stream outFile = File.Create(excelPath);
inboundStream.CopyTo(outFile);
outFile.Close();
//process excel file to return XML
var spreadsheet = new SpreadSheet();
string strXmlOut = spreadsheet.ProcessWorkbook(excelPath);
//now build an XML doc to hold this data
XmlDocument xDoc = new XmlDocument();
xDoc.LoadXml(strXmlOut);
XmlDocument finalMsg = new XmlDocument();
XmlElement xEle;
xEle = finalMsg.CreateElement("ns0", "BizTalk_Test_Amey_Pipeline.textXML",
"http://tempuri.org/INT018_Workbook.xsd");
finalMsg.AppendChild(xEle);
finalMsg.FirstChild.InnerXml = xDoc.FirstChild.InnerXml;
//write xml to memory stream
swTemp.WriteLine("Write xml to memory stream");
MemoryStream streamXmlOut = new MemoryStream();
finalMsg.Save(streamXmlOut);
streamXmlOut.Position = 0;
inmsg.BodyPart.Data = streamXmlOut;
pc.ResourceTracker.AddResource(streamXmlOut);
return inmsg;
}
Here is a sample of writing the message back:
IBaseMessage Microsoft.BizTalk.Component.Interop.IComponent.Execute(IPipelineContext pContext, IBaseMessage pInMsg)
{
IBaseMessagePart bodyPart = pInMsg.BodyPart;
if (bodyPart != null)
{
using (Stream originalStrm = bodyPart.GetOriginalDataStream())
{
byte[] changedMessage = ConvertToBytes(ret);
using (Stream strm = new AsciiStream(originalStrm, changedMessage, resManager))
{
// Setup the custom stream to put it back in the message.
bodyPart.Data = strm;
pContext.ResourceTracker.AddResource(strm);
}
}
}
return pInMsg;
}
The AsciiStream used a method like this to read the stream:
override public int Read(byte[] buffer, int offset, int count)
{
int ret = 0;
int bytesRead = 0;
byte[] FixedData = this.changedBytes;
if (FixedData != null)
{
bytesRead = count > (FixedData.Length - overallOffset) ? FixedData.Length - overallOffset : count;
Array.Copy(FixedData, overallOffset, buffer, offset, bytesRead);
if (FixedData.Length == (bytesRead + overallOffset))
this.changedBytes = null;
// Increment the overall offset.
overallOffset += bytesRead;
offset += bytesRead;
count -= bytesRead;
ret += bytesRead;
}
return ret;
}
I would first of all add more logging to your component around the MemoryStream logic - maybe write the file out to the file system so you can make sure the Xml version is correct. You can also attach to the BizTalk process and step through the code for the component which makes debugging a lot easier.
I would try switching the use of MemoryStream to a more basic custom stream that writes the bytes for you. In the BizTalk SDK samples for pipeline components there are some examples for a custom stream. You would have to customize the stream sample so it just writes the stream. I can work on posting an example. So do the additional diagnostics above first.
Thanks,

Resources