How to convert a data scheme to image and save - c#

As for the title, I have trouble saving images in the data scheme.
This is my code, but it doesn't work at all.
[Route("api/account/update_avatar")]
[HttpPost]
public HttpResponseMessage Update()
{
if (AppCommon.LoginAuth("Web") == false)
{
return AppCommon.ResponseMessage("login-authentication-failed");
}
HttpResponseMessage Response = new HttpResponseMessage();
var EncodedImage = Context.Request.Params["image"].TypeString();
var Id = Context.Session["UserId"].TypeString() + ".png";
var StoragePath = Common.PathCombine(AppConfig.AvatarStoragePath, Id);
try
{
// From StackOverflow
// Split the string to escape the real data
var b64 = EncodedImage.Split(",".ToCharArray(), 2);
// Convert the base 64 String to byte array
byte[] byteArray = System.Convert.FromBase64String(b64[1]);
// Open a stream from the byte array
MemoryStream ImgStream = new MemoryStream(byteArray);
// Generate the image from the stream
Image DecodedImage = Image.FromStream(ImgStream);
// Save the image
DecodedImage.Save(StoragePath);
} catch (Exception e)
{
return AppCommon.ResponseMessage("internal-error"); // This does returned
}
return AppCommon.ResponseMessage("success");
}
There is no problem with data. I posted a data scheme with the script
var image = $("#photo");
// jquery cropper
var scheme = image
.cropper("getCroppedCanvas", {
width: 600,
height: 600,
})
.toDataURL("image/png");
fastui.coverShow("waiting-cover");
// Customized Ajax, never mind.
$ajax({
type: "POST",
url: "/api/account/update-avatar",
async: true,
data: "image=" + scheme,
callback: function (data) {
if (data == "internal-error") {
fastui.textTips(lang.user.tips["operation-failed"]);
} else if (data == "success") {
window.parent.fastui.iconTips("tick");
window.setTimeout(function () {
window.parent.fastui.windowClose("avatar");
}, 500);
} else {
fastui.textTips(lang.user.tips["operation-failed"]);
}
},
});
The program has proper permission to write files.
This has nothing to do with my front-end.
If someone can answer my question, thanks in advance.

There are 2 problems with the scripts.
Firstly, if you are trying to post a string with the character '+', the character will be replaced with space.
var scheme = image
.cropper("getCroppedCanvas", {
width: 600,
height: 600,
})
.toDataURL("image/png").replaceAll('+', '#');
var EncodedImage = Context.Request.Params["image"].TypeString().Replace('#', '+');
Secondly, if the path which contains the image doesn't exist, the image won't be saved but throw an exception.
if (!Directory.Exists(AppConfig.AvatarStoragePath))
{
Directory.CreateDirectory(AppConfig.AvatarStoragePath);
}
That's my solution, thanks

There's no reason to use a Data URL or deal with its many problems. As jQuery-Cropper's getCroppedCanvas docs show, it's possible to get a Blob from a Canvas object and post it either as raw binary or as a file in a FORM post.
image.cropper("getCroppedCanvas", {
width: 600,
height: 600,
})
.toBlob((blob) => {
const formData = new FormData();
// Pass the image file name as the third parameter if necessary.
formData.append('croppedImage', blob, 'User123.png');
// Use `jQuery.ajax` method for example
$.ajax('/path/to/upload', {
method: 'POST',
data: formData,
processData: false,
contentType: false,
success() {
console.log('Upload success');
},
error() {
console.log('Upload error');
},
});
}/*, 'image/png' */);
On ASP.NET Core's side, the files in a FORM post are available as IFormFile objects :
[HttpPost]
public async Task<IActionResult> Update(IFormFile croppedImage )
{
....
using(var targetStream=File.OpenWrite(StoragePath))
{
await croppedImage.CopyToAsync(targetStream);
}
return Ok("success");
}
Uploading files, both small and large, is described in Upload Files in ASP.NET Core.
If you use the old ASP.NET Web API:
it's time to upgrade to ASP.NET Core. .NET Old is essentially in maintenance mode, and soon even .NET 4.6.1 will be out of support.
The code is more complicated. This is documented in Sending HTML Form Data in ASP.NET Web API: File Upload and Multipart MIME.
In this case, the MultipartFormDataStreamProvider class will store uploaded files to the root location based on the posted file name
public async Task<HttpResponseMessage> PostFormData()
{
...
string root = HttpContext.Current.Server.MapPath("~/App_Data");
var provider = new MultipartFormDataStreamProvider(root);
// Read the form data.
await Request.Content.ReadAsMultipartAsync(provider);
...
}
If you want to generate the name on the server you need to call ReadAsMultipartAsync without a provider. This returns a MultipartMemoryStreamProvider , whose Contents property contains the files as HttpContent objects which, finally, allow you to save them:
var provider=await Request.Content.ReadAsMultipartAsync(provider);
foreach(var cnt in provider.Contents)
{
using(var targetStream=File.OpenWrite(somePath);
{
await cont.CopyToAsync(targetStream);
}
}

Related

Posting form with properties and multiple attachments in AJAX to ASP.NET WebAPI2

I have a regular form on view with a set of inputs. I use jQuery to serialize and post it.
$("#formId").submit(function (e) {
e.preventDefault();
var fields = {};
var formSerialized = $("#formId").serializeArray();
$.each(formSerialized, function (i, field) {
fields[field.name] = field.value; });
e.data = fields;
$.post("myUrl", {
FirstName: e.data.FirstName,
LastName: e.data.LastName
}, function (success) {
if (success) {
alert('Ok')
} else {
alert('fail');
}
});
}
});
On backend I have ASP.NET WebAPI2 server with action that gets this request and automatically binds all properties to model.
Now I need to add multiple file inputs to the same form.
Is there a way to:
- send files and regular properties that I send in code sample in the same time in the same request?
- is it possible to extend model on WebAPI side with HttpPostedFileBase properties and to keep automatic binding of data?
What is the best way to send regular text properties (values from form) + multiple files in the same request to process them in one single method on WebAPI2 side?
Thanks!
This answer discusses some options, but you might run into some browser compatibility issues:
jQuery Ajax File Upload
If it's an option, you might consider doing a regular non async form submission, and making sure your form has enctype="multipart/form-data", but if not, you can try some of the things discussed in that link. Hope this helps
Edit - another approach is to use this jQuery form plugin: http://malsup.com/jquery/form/
In this case, I wasn't using a viewmodel but instead was binding the inputs directly to parameters, but I can't think of any reason why it wouldn't work the exact same way with a typical vm.
Example use:
Front end
#using (Html.BeginForm("SaveComment", "Comments", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
[text and file inputs, submit button]
}
<script>
$('#addNote form').ajaxForm({
success: function (response) {
$.notify(response.result, {
className: "success",
position: "top center"
});
//reload comments section
$.ajax({
url: './Comments/Index',
data: {
labelId: '#Model.LabelId',
orderNumber: '#Model.OrderNumber'
},
success: function (response) {
$('#commentsSection').html(response);
}
});
},
error: function () {
$.notify("Failed to save note");
},
beforeSubmit: function () {
$('#addNote').modal('toggle');
}
});
Back end
public JsonResult SaveComment(string saveNote, string labelId, string orderNumber, string comment, string criteria, HttpPostedFileBase file)
{
[text input named saveNote went into saveNote param]
[file input named file went into HttpPostedFileBase file param]
[...process other params...]
var ms = new MemoryStream();
file.InputStream.CopyTo(ms);
deliveryItemComment.Attachment = ms.ToArray();
db.SaveChanges();
var result = "Note Succesfully Added";
return Json(new { result = result }, JsonRequestBehavior.AllowGet);
}
Pretty ugly solution I suppose, but I ended in that way.
JS uses form data:
var formSerialized = $("formId").serializeArray();
var data = new FormData();
var files = $("#fileInputId").get(0).files;
data.append("File", files[0]);
$.each(formSerialized, function (i, field) {
data.append(field.name, field.value);
});
$.ajax({
type: "POST",
contentType: false,
processData: false,
url: "some URL",
data: data
}
});
On WebAPI side I have to separately read form fields and files from form data:
var myModel = new MyModel();
var root = HttpContext.Current.Server.MapPath("~/App_Data/");
var provider = new MultipartFormDataStreamProvider(root);
await Request.Content.ReadAsMultipartAsync(provider);
//Maps fields from form data into my custom model
foreach (var key in provider.FormData.AllKeys)
{
var value = provider.FormData.GetValues(key).FirstOrDefault();
if (value != null && value != "undefined")
{
var prop = myModel.GetType().GetProperty(key);
if (prop != null)
{
prop.SetValue(myModel, value, null);
}
}
}
//Resaves all files in my custom location under App_Data and puts their paths into list
var fileNames = new Collection<string>();
foreach (var file in provider.FileData)
{
var fileExt = file.Headers.ContentDisposition.FileName.Split('.').Last().Replace("\"", string.Empty);
var combinedFileName = string.Format("{0}.{2}", file.Headers.ContentDisposition.Name.Replace("\"", string.Empty), fileExt);
var combinedPath = Path.Combine(root + "CustomDirectory/", combinedFileName);
File.Move(file.LocalFileName, combinedPath);
fileNames.Add(combinedPath);
}

Ajax post JSON data to MVC getting error Unexpected token P

This has been driving me nuts. I have a page where I need to make a JSON post to a controller, it will process it and return an excel file for download. So far it appears to be running correctly, but when it returns to the ajax call, I get an parsererror and the message "Unexpected token P." I have tried so many different configurations and call methods (standard MVC ActionRequest to WebApi post) and none of them change. Here is the code I'm running.
JavaScript:
var treatmentplan = {"PlanRecordStatusId":"1","PlanRecordDateBegin":"","PlanRecordDateEnd":"","ClientClaimNumber":"","PatientNumber":0,"requestAction":3};
$.ajax({
//global: true,
//url: '/home/ExcelRpt',
url: '/api/TreatmentPlanExcel',
type: 'POST',
dataType: 'json',
data: treatmentplan,
//contentType: 'application/json; charset=utf-8',
success: function (data) {
//var msg = data.Message;
//$('#results').html(msg);
$("#tmpFrame").attr('src', 'URL-TO-EXCEL-FILE');
}
, error: function (jqXHR, exception, error) {
if (jqXHR.status === 0) {
alert('Not connect.n Verify Network.');
} else if (jqXHR.status == 404) {
alert('Requested page not found. [404]');
} else if (jqXHR.status == 500) {
alert('Internal Server Error [500].');
} else if (exception === 'parsererror') {
alert('Requested JSON parse failed.');
} else if (exception === 'timeout') {
alert('Time out error.');
} else if (exception === 'abort') {
alert('Ajax request aborted.');
} else {
alert('Uncaught Error.n' + jqXHR.responseText);
}
$('#log').html(error.message);
}
});
Here is the C# code (both WebApi and MVC controller version), I am not going to include my ToExcel extension, I know this part works it's just a matter of getting it to download when it's returned. It is reaching this code, generates data and returning. I just need to see what the heck is going on. If there is a prefered way of making the call, let me know as well (WebApi or MVC)
C#
Web Api version
public HttpResponseMessage Post(TreatmentPlanRequest tpRequest) {
tpRequest.Verify();
List<TreatmentPlan> tpl = DataAccess.GetReportDap(tpRequest).ToList();
HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
var contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
var package = tpl.ToExcel("TreatmentReport");
var fileStream = new MemoryStream();
package.SaveAs(fileStream);
fileStream.Position = 0;
result.Content = new StreamContent(fileStream);
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
return result;
}
Here is the MVC controller version
[HttpPost]
public ActionResult ExcelRpt(TreatmentPlanRequest tpRequest) {
tpRequest.Verify();
List<TreatmentPlan> tpl = DataAccess.GetReportDap(tpRequest).ToList();
var contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
var package = tpl.ToExcel("TreatmentReport");
var fileStream = new MemoryStream();
package.SaveAs(fileStream);
fileStream.Position = 0;
var fsr = new FileStreamResult(fileStream, contentType);
fsr.FileDownloadName = "TreatmentReport";
return fsr;
}
From here, I have no clue as to why this isn't working. I have searched high and low on Google on how to do this in MVC (I use to do this with web forms and never had an issue). I am guessing my issue is on the return or
Change your Success method to open a new window with the URL instead of setting a frame in the current window.
So this:
$("#tmpFrame").attr('src', 'URL-TO-EXCEL-FILE');
becomes:
window.open('URL-TO-EXCEL-FILE');
In the vast majority of cases, this should do exactly what you're looking for. Occasionally, depending on specific browser settings, users may get a "Popup Blocked" message, but that rarely happens in this scenario in the apps that I've worked with.
EDIT:
After additional clarification, there is a second issue. Data returned from the server must be in the same format as the .ajax() method is expecting it, in this case 'JSON'. Instead of returning a FileStreamResult from your Action, try returning a JSON object which has the URL you'll need to call to generate the Excel file.
return Json(new { URL = 'URL-TO-EXCEL-FILE' });
Then, follow the suggestion in my original response.
I would like to thank Kris Hatcher for the solution on this. He suggested making two ActionResults. One that builds a query string from the parameters of the initial request. It returns a full URL with the parameters. It then does a Window.Open() using the returned url.
With all the examples I found, this was the only one that worked for me. Here's how the code works.
JavaScript:
function TestWebApiReport() {
var reportData = GetReport();
$.ajax({
url: '/home/ExcelResults'
, data: reportData
, type: 'POST'
, dataType: 'json'
, success: function (data) {
window.open(data.URL);
}, error: function (jqXHR, exception, error) {
alert("GRRRRRR!!!")
}
});
}
It creates the JSON data, then posts it to a JsonResult. Here's the controller code.
C#
[HttpPost]
public JsonResult ExcelResults(ReportRequest tpRequest) {
StringBuilder sb = new StringBuilder();
bool firstIn = true;
sb.AppendFormat("{0}/Home/ExcelRpt", Request.Url.Scheme + "://" + Request.Url.Authority);
foreach (var prop in tpRequest.GetType().GetProperties()) {
if (prop.GetValue(tpRequest, null) != null) {
if (firstIn) {
sb.AppendFormat("?");
firstIn = false;
} else {
sb.AppendFormat("&");
}
sb.AppendFormat("{0}={1}", prop.Name, prop.GetValue(tpRequest, null));
}
}
return Json(new { URL = sb.ToString() });
}
You go back to the JavaScript, you'll see the return data uses the URL to do a Window.Open(). Then the excel file is created. Here's the last call (ActionResult).
[HttpGet]
public ActionResult ExcelRpt(ReportRequest tpRequest) {
if (tpRequest.requestAction != RequestAction.Report) {
throw new Exception("Did not use action request type of 'Report'.");
}
tpRequest.requestAction = RequestAction.Report;
List<Report> tpl = DataAccess.GetReportDap(tpRequest).ToList();
var contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
var package = tpl.ToExcel("Report");
var fileStream = new MemoryStream();
package.SaveAs(fileStream);
fileStream.Position = 0;
var fsr = new FileStreamResult(fileStream, contentType);
fsr.FileDownloadName = "TreatmentReport.xlsx";
return fsr;
}
ReportRequest is a class I have and ToExcel(), I extended the List item. Now this works pretty well.

summernote upload images to s3 (asp.net)

Been scratching my head a lot for the past two week about trying to figure this out.
Summernote (https://github.com/HackerWins/summernote) is my preferred editor, but I need the images to be uploaded to S3 instead of being saved as base64, as this will be too large for the database, etc. On the summernote github page I have found this (https://github.com/HackerWins/summernote/issues/72), but there is not a .net code sample.
I am fine with uploading files to S3 from my asp website, my problem is that how should I 'send' the file to my webmethod, yes I am using a webmethod as the summernote code is handled in js, in a way that asp would understand it?
I have tried sending the 'file', and receiving it as an object on my server side, but this only results in a "[object File]" string being received.
I am coding in VB, but C# code will also be appreciated!
Thanks in advance.
I don't know any VB so I hope this C# code will help you.
First of all I've never used amazon-s3 therefor I won't provide an example to it's specifics however whilst making a quick search I've found another thread where a user points out on how to actually upload a image to amazon using a memory stream here.
An option would be to create a upload action server side, here's a snippet in C# using MVC:
[HttpPost]
public ActionResult UploadImage(HttpPostedFileBase file)
{
// TODO: your validation goes here,
// eg: file != null && file.ContentType.StartsWith("image/") etc...
var imageUrl = _myAmazonWrapper.UploadImage(file.InputStream);
return Json(imageUrl);
}
This action result will receive an HttpPostedFileBase image containing the actual image with content type, file name etc.
The last thing left is the actual initialization of the summernote script:
$('#summernote').summernote({
onImageUpload: uploadImages
});
Where the function uploadImages could be defined as following:
var uploadImages = function (files, editor, $editable) {
var formData = new FormData();
formData.append("file", files[0]);
$.ajax({
url: "Image/UploadImage",
data: formData,
type: 'POST',
cache: false,
contentType: false,
processData: false,
success: function (imageUrl) {
if (!imageUrl) {
// handle error
return;
}
editor.insertImage($editable, imageUrl);
},
error: function () {
// handle error
}
});
};
Note that the uploadImage function is not supporting multiple images, for instance you can drag-and-drop images to the summernote widget and in that particular case the "files" parameter will contain multiple images, so just enumerate over them and upload as you please.
Good luck!
I had to do this recently in .NET Core.
Here is the code of the action to upload the image to s3 (you need to install AWSSDK.S3):
[HttpPost]
public async Task<IActionResult> UploadFile(IFormFile file)
{
try
{
if (file != null && file.Length > 0)
{
var relativePath = string.Empty;
using (var client = new AmazonS3Client("awsAccessKeyId", "awsSecretAccessKey", RegionEndpoint.USEast1))
{
using (var newMemoryStream = new MemoryStream())
{
file.CopyTo(newMemoryStream);
var uploadRequest = new TransferUtilityUploadRequest
{
InputStream = newMemoryStream,
Key = file.FileName,
BucketName = "bucketName",
CannedACL = S3CannedACL.PublicRead
};
var fileTransferUtility = new TransferUtility(client);
await fileTransferUtility.UploadAsync(uploadRequest);
relativePath = "urlS3" + "bucketName" + "/" + file.FileName;
}
}
return Ok(new { FileUrl = relativePath });
}
return BadRequest("Select a file");
}
catch (Exception exception)
{
return BadRequest(exception.Message);
}
}
Here is the code to put in your view, to override the upload method of summernote:
$('.html-editor').summernote({
callbacks: {
onImageUpload: function (files) {
for (let i = 0; i < files.length; i++) {
uploadImageToS3ForSummerNote(files[i]);
}
}
}
});
function uploadImageToS3ForSummerNote(file) {
var url = '#Url.Action("UploadFile", "MyController")';
formData = new FormData();
formData.append("file", file);
$.ajax({
type: 'POST',
url: url,
data: formData,
cache: false,
contentType: false,
processData: false,
success: function (data) {
$('.html-editor').summernote('insertImage', data.fileUrl);
},
error: function (data) {
alert(data.responseText);
}
});
}

jQuery Image Upload to MVC5

I currently have the following code within my server side API:
// Enable both Get and Post so that our jquery call can send data, and get a status
[System.Web.Mvc.HttpGet]
[System.Web.Mvc.HttpPost]
public HttpResponseMessage Upload()
{
// Get a reference to the file that our jQuery sent. Even with multiple files, they will all be their own request and be the 0 index
HttpPostedFile file = HttpContext.Current.Request.Files[0];
// do something with the file in this space
// {....}
// end of file doing
// Now we need to wire up a response so that the calling script understands what happened
HttpContext.Current.Response.ContentType = "text/plain";
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
var result = new { name = file.FileName };
HttpContext.Current.Response.Write(serializer.Serialize(result));
HttpContext.Current.Response.StatusCode = 200;
// For compatibility with IE's "done" event we need to return a result as well as setting the context.response
return new HttpResponseMessage(HttpStatusCode.OK);
}
I use the following jQuery to call it:
$(function () {
$('#fileupload').fileupload({
dataType: "json",
url: "/api/Upload",
limitConcurrentUploads: 1,
sequentialUploads: true,
progressInterval: 100,
maxChunkSize: 10000,
add: function (e, data) {
$('#filelistholder').removeClass('hide');
data.context = $('<div />').text(data.files[0].name).appendTo('#filelistholder');
$('</div><div class="progress"><div class="bar" style="width:0%"></div></div>').appendTo(data.context);
$('#btnUploadAll').click(function () {
data.submit();
});
},
done: function (e, data) {
data.context.text(data.files[0].name + '... Completed');
$('</div><div class="progress"><div class="bar" style="width:100%"></div></div>').appendTo(data.context);
},
progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$('#overallbar').css('width', progress + '%');
},
progress: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
data.context.find('.bar').css('width', progress + '%');
}
});
});
The problem I am having is, I don't understand where I can grab the completed uploaded file. As when I add a breakpoint this one function get's called 100's of times on one image upload.
Could someone please give me help on why it's called so much?
And I suppose the best way to ensure that it is only been an image uploaded (no exe, viruses etc).
I just want to upload the file, confirm it's an image, resize the file and save to by database as a Base64 string or to the file system either or?
Thanks!

Download Excel file via AJAX MVC

I have a large(ish) form in MVC.
I need to be able to generate an excel file containing data from a subset of that form.
The tricky bit is that this shouldn't affect the rest of the form and so I want to do it via AJAX. I've come across a few questions on SO that seem to be related, but I can't quite work out what the answers mean.
This one seems the closest to what I'm after: asp-net-mvc-downloading-excel - but I'm not sure I understand the response, and it is a couple years old now. I also came across another article (can't find it anymore) about using an iframe to handle the file download, but I'm not sure how to get this working with MVC.
My excel file returns fine if I'm doing a full post back but I can't get it working with AJAX in mvc.
You can't directly return a file for download via an AJAX call so, an alternative approach is to to use an AJAX call to post the related data to your server. You can then use server side code to create the Excel File (I would recommend using EPPlus or NPOI for this although it sounds as if you have this part working).
UPDATE September 2016
My original answer (below) was over 3 years old, so I thought I would update as I no longer create files on the server when downloading files via AJAX however, I have left the original answer as it may be of some use still depending on your specific requirements.
A common scenario in my MVC applications is reporting via a web page that has some user configured report parameters (Date Ranges, Filters etc.). When the user has specified the parameters they post them to the server, the report is generated (say for example an Excel file as output) and then I store the resulting file as a byte array in the TempData bucket with a unique reference. This reference is passed back as a Json Result to my AJAX function that subsequently redirects to separate controller action to extract the data from TempData and download to the end users browser.
To give this more detail, assuming you have a MVC View that has a form bound to a Model class, lets call the Model ReportVM.
First, a controller action is required to receive the posted model, an example would be:
public ActionResult PostReportPartial(ReportVM model){
// Validate the Model is correct and contains valid data
// Generate your report output based on the model parameters
// This can be an Excel, PDF, Word file - whatever you need.
// As an example lets assume we've generated an EPPlus ExcelPackage
ExcelPackage workbook = new ExcelPackage();
// Do something to populate your workbook
// Generate a new unique identifier against which the file can be stored
string handle = Guid.NewGuid().ToString();
using(MemoryStream memoryStream = new MemoryStream()){
workbook.SaveAs(memoryStream);
memoryStream.Position = 0;
TempData[handle] = memoryStream.ToArray();
}
// Note we are returning a filename as well as the handle
return new JsonResult() {
Data = new { FileGuid = handle, FileName = "TestReportOutput.xlsx" }
};
}
The AJAX call that posts my MVC form to the above controller and receives the response looks like this:
$ajax({
cache: false,
url: '/Report/PostReportPartial',
data: _form.serialize(),
success: function (data){
var response = JSON.parse(data);
window.location = '/Report/Download?fileGuid=' + response.FileGuid
+ '&filename=' + response.FileName;
}
})
The controller action to handle the downloading of the file:
[HttpGet]
public virtual ActionResult Download(string fileGuid, string fileName)
{
if(TempData[fileGuid] != null){
byte[] data = TempData[fileGuid] as byte[];
return File(data, "application/vnd.ms-excel", fileName);
}
else{
// Problem - Log the error, generate a blank file,
// redirect to another controller action - whatever fits with your application
return new EmptyResult();
}
}
One other change that could easily be accommodated if required is to pass the MIME Type of the file as a third parameter so that the one Controller action could correctly serve a variety of output file formats.
This removes any need for any physical files to created and stored on the server, so no housekeeping routines required and once again this is seamless to the end user.
Note, the advantage of using TempData rather than Session is that once TempData is read the data is cleared so it will be more efficient in terms of memory usage if you have a high volume of file requests. See TempData Best Practice.
ORIGINAL Answer
You can't directly return a file for download via an AJAX call so, an alternative approach is to to use an AJAX call to post the related data to your server. You can then use server side code to create the Excel File (I would recommend using EPPlus or NPOI for this although it sounds as if you have this part working).
Once the file has been created on the server pass back the path to the file (or just the filename) as the return value to your AJAX call and then set the JavaScript window.location to this URL which will prompt the browser to download the file.
From the end users perspective, the file download operation is seamless as they never leave the page on which the request originates.
Below is a simple contrived example of an ajax call to achieve this:
$.ajax({
type: 'POST',
url: '/Reports/ExportMyData',
data: '{ "dataprop1": "test", "dataprop2" : "test2" }',
contentType: 'application/json; charset=utf-8',
dataType: 'json',
success: function (returnValue) {
window.location = '/Reports/Download?file=' + returnValue;
}
});
url parameter is the Controller/Action method where your code will create the Excel file.
data parameter contains the json data that would be extracted from the form.
returnValue would be the file name of your newly created Excel file.
The window.location command redirects to the Controller/Action method that actually returns your file for download.
A sample controller method for the Download action would be:
[HttpGet]
public virtual ActionResult Download(string file)
{
string fullPath = Path.Combine(Server.MapPath("~/MyFiles"), file);
return File(fullPath, "application/vnd.ms-excel", file);
}
My 2 cents - you don't need to store the excel as a physical file on the server - instead, store it in the (Session) Cache. Use a uniquely generated name for your Cache variable (that stores that excel file) - this will be the return of your (initial) ajax call. This way you don't have to deal with file access issues, managing (deleting) the files when not needed, etc. and, having the file in the Cache, is faster to retrieve it.
I was recently able to accomplish this in MVC (although there was no need to use AJAX) without creating a physical file and thought I'd share my code:
Super simple JavaScript function (datatables.net button click triggers this):
function getWinnersExcel(drawingId) {
window.location = "/drawing/drawingwinnersexcel?drawingid=" + drawingId;
}
C# Controller code:
public FileResult DrawingWinnersExcel(int drawingId)
{
MemoryStream stream = new MemoryStream(); // cleaned up automatically by MVC
List<DrawingWinner> winnerList = DrawingDataAccess.GetWinners(drawingId); // simple entity framework-based data retrieval
ExportHelper.GetWinnersAsExcelMemoryStream(stream, winnerList, drawingId);
string suggestedFilename = string.Format("Drawing_{0}_Winners.xlsx", drawingId);
return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", suggestedFilename);
}
In the ExportHelper class I do use a 3rd party tool (GemBox.Spreadsheet) to generate the Excel file and it has a Save to Stream option. That being said, there are a number of ways to create Excel files that can easily be written to a memory stream.
public static class ExportHelper
{
internal static void GetWinnersAsExcelMemoryStream(MemoryStream stream, List<DrawingWinner> winnerList, int drawingId)
{
ExcelFile ef = new ExcelFile();
// lots of excel worksheet building/formatting code here ...
ef.SaveXlsx(stream);
stream.Position = 0; // reset for future read
}
}
In IE, Chrome, and Firefox, the browser prompts to download the file and no actual navigation occurs.
First Create the controller action that will create the Excel File
[HttpPost]
public JsonResult ExportExcel()
{
DataTable dt = DataService.GetData();
var fileName = "Excel_" + DateTime.Now.ToString("yyyyMMddHHmm") + ".xls";
//save the file to server temp folder
string fullPath = Path.Combine(Server.MapPath("~/temp"), fileName);
using (var exportData = new MemoryStream())
{
//I don't show the detail how to create the Excel, this is not the point of this article,
//I just use the NPOI for Excel handler
Utility.WriteDataTableToExcel(dt, ".xls", exportData);
FileStream file = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
exportData.WriteTo(file);
file.Close();
}
var errorMessage = "you can return the errors in here!";
//return the Excel file name
return Json(new { fileName = fileName, errorMessage = "" });
}
then create the Download action
[HttpGet]
[DeleteFileAttribute] //Action Filter, it will auto delete the file after download,
//I will explain it later
public ActionResult Download(string file)
{
//get the temp folder and file path in server
string fullPath = Path.Combine(Server.MapPath("~/temp"), file);
//return the file for download, this is an Excel
//so I set the file content type to "application/vnd.ms-excel"
return File(fullPath, "application/vnd.ms-excel", file);
}
if you want to delete the file after downloaded create this
public class DeleteFileAttribute : ActionFilterAttribute
{
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
filterContext.HttpContext.Response.Flush();
//convert the current filter context to file and get the file path
string filePath = (filterContext.Result as FilePathResult).FileName;
//delete the file after download
System.IO.File.Delete(filePath);
}
}
and finally ajax call from you MVC Razor view
//I use blockUI for loading...
$.blockUI({ message: '<h3>Please wait a moment...</h3>' });
$.ajax({
type: "POST",
url: '#Url.Action("ExportExcel","YourController")', //call your controller and action
contentType: "application/json; charset=utf-8",
dataType: "json",
}).done(function (data) {
//console.log(data.result);
$.unblockUI();
//get the file name for download
if (data.fileName != "") {
//use window.location.href for redirect to download action for download the file
window.location.href = "#Url.RouteUrl(new
{ Controller = "YourController", Action = "Download"})/?file=" + data.fileName;
}
});
I used the solution posted by CSL but I would recommend you dont store the file data in Session during the whole session. By using TempData the file data is automatically removed after the next request (which is the GET request for the file). You could also manage removal of the file data in Session in download action.
Session could consume much memory/space depending on SessionState storage and how many files are exported during the session and if you have many users.
I've updated the serer side code from CSL to use TempData instead.
public ActionResult PostReportPartial(ReportVM model){
// Validate the Model is correct and contains valid data
// Generate your report output based on the model parameters
// This can be an Excel, PDF, Word file - whatever you need.
// As an example lets assume we've generated an EPPlus ExcelPackage
ExcelPackage workbook = new ExcelPackage();
// Do something to populate your workbook
// Generate a new unique identifier against which the file can be stored
string handle = Guid.NewGuid().ToString()
using(MemoryStream memoryStream = new MemoryStream()){
workbook.SaveAs(memoryStream);
memoryStream.Position = 0;
TempData[handle] = memoryStream.ToArray();
}
// Note we are returning a filename as well as the handle
return new JsonResult() {
Data = new { FileGuid = handle, FileName = "TestReportOutput.xlsx" }
};
}
[HttpGet]
public virtual ActionResult Download(string fileGuid, string fileName)
{
if(TempData[fileGuid] != null){
byte[] data = TempData[fileGuid] as byte[];
return File(data, "application/vnd.ms-excel", fileName);
}
else{
// Problem - Log the error, generate a blank file,
// redirect to another controller action - whatever fits with your application
return new EmptyResult();
}
}
using ClosedXML.Excel;
public ActionResult Downloadexcel()
{
var Emplist = JsonConvert.SerializeObject(dbcontext.Employees.ToList());
DataTable dt11 = (DataTable)JsonConvert.DeserializeObject(Emplist, (typeof(DataTable)));
dt11.TableName = "Emptbl";
FileContentResult robj;
using (XLWorkbook wb = new XLWorkbook())
{
wb.Worksheets.Add(dt11);
using (MemoryStream stream = new MemoryStream())
{
wb.SaveAs(stream);
var bytesdata = File(stream.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "myFileName.xlsx");
robj = bytesdata;
}
}
return Json(robj, JsonRequestBehavior.AllowGet);
}
$.ajax({
type: "GET",
url: "/Home/Downloadexcel/",
contentType: "application/json; charset=utf-8",
data: null,
success: function (Rdata) {
debugger;
var bytes = new Uint8Array(Rdata.FileContents);
var blob = new Blob([bytes], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = "myFileName.xlsx";
link.click();
},
error: function (err) {
}
});
The accepted answer didn't quite work for me as I got a 502 Bad Gateway result from the ajax call even though everything seemed to be returning fine from the controller.
Perhaps I was hitting a limit with TempData - not sure, but I found that if I used IMemoryCache instead of TempData, it worked fine, so here is my adapted version of the code in the accepted answer:
public ActionResult PostReportPartial(ReportVM model){
// Validate the Model is correct and contains valid data
// Generate your report output based on the model parameters
// This can be an Excel, PDF, Word file - whatever you need.
// As an example lets assume we've generated an EPPlus ExcelPackage
ExcelPackage workbook = new ExcelPackage();
// Do something to populate your workbook
// Generate a new unique identifier against which the file can be stored
string handle = Guid.NewGuid().ToString();
using(MemoryStream memoryStream = new MemoryStream()){
workbook.SaveAs(memoryStream);
memoryStream.Position = 0;
//TempData[handle] = memoryStream.ToArray();
//This is an equivalent to tempdata, but requires manual cleanup
_cache.Set(handle, memoryStream.ToArray(),
new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(10)));
//(I'd recommend you revise the expiration specifics to suit your application)
}
// Note we are returning a filename as well as the handle
return new JsonResult() {
Data = new { FileGuid = handle, FileName = "TestReportOutput.xlsx" }
};
}
AJAX call remains as with the accepted answer (I made no changes):
$ajax({
cache: false,
url: '/Report/PostReportPartial',
data: _form.serialize(),
success: function (data){
var response = JSON.parse(data);
window.location = '/Report/Download?fileGuid=' + response.FileGuid
+ '&filename=' + response.FileName;
}
})
The controller action to handle the downloading of the file:
[HttpGet]
public virtual ActionResult Download(string fileGuid, string fileName)
{
if (_cache.Get<byte[]>(fileGuid) != null)
{
byte[] data = _cache.Get<byte[]>(fileGuid);
_cache.Remove(fileGuid); //cleanup here as we don't need it in cache anymore
return File(data, "application/vnd.ms-excel", fileName);
}
else
{
// Something has gone wrong...
return View("Error"); // or whatever/wherever you want to return the user
}
}
...
Now there is some extra code for setting up MemoryCache...
In order to use "_cache" I injected in the constructor for the controller like so:
using Microsoft.Extensions.Caching.Memory;
namespace MySolution.Project.Controllers
{
public class MyController : Controller
{
private readonly IMemoryCache _cache;
public LogController(IMemoryCache cache)
{
_cache = cache;
}
//rest of controller code here
}
}
And make sure you have the following in ConfigureServices in Startup.cs:
services.AddDistributedMemoryCache();
$.ajax({
global: false,
url: SitePath + "/User/ExportTeamMembersInExcel",
"data": { 'UserName': UserName, 'RoleId': RoleId, UserIds: AppraseeId },
"type": "POST",
"dataType": "JSON",
"success": function (result) {
var bytes = new Uint8Array(result.FileContents);
var blob = new Blob([bytes], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = "myFileName.xlsx";
link.click();
},
"error": function () {
alert("error");
}
})
[HttpPost]
public JsonResult ExportTeamMembersInExcel(string UserName, long? RoleId, string[] UserIds)
{
MemoryStream stream = new MemoryStream();
FileContentResult robj;
DataTable data = objuserservice.ExportTeamToExcel(UserName, RoleId, UserIds);
using (XLWorkbook wb = new XLWorkbook())
{
wb.Worksheets.Add(data, "TeamMembers");
using (stream)
{
wb.SaveAs(stream);
}
}
robj = File(stream.ToArray(), System.Net.Mime.MediaTypeNames.Application.Octet, "TeamMembers.xlsx");
return Json(robj, JsonRequestBehavior.AllowGet);
}
I may sound quite naive, and may attract quite a criticism, but here's how I did it,
(It doesn't involve ajax for export, but it doesn't do a full postback either )
Thanks for this post and this answer.
Create a simple controller
public class HomeController : Controller
{
/* A demo action
public ActionResult Index()
{
return View(model);
}
*/
[HttpPost]
public FileResult ExportData()
{
/* An example filter
var filter = TempData["filterKeys"] as MyFilter;
TempData.Keep(); */
var someList = db.GetDataFromDb(/*filter*/) // filter as an example
/*May be here's the trick, I'm setting my filter in TempData["filterKeys"]
in an action,(GetFilteredPartial() illustrated below) when 'searching' for the data,
so do not really need ajax here..to pass my filters.. */
//Some utility to convert list to Datatable
var dt = Utility.ConvertToDataTable(someList);
// I am using EPPlus nuget package
using (ExcelPackage pck = new ExcelPackage())
{
ExcelWorksheet ws = pck.Workbook.Worksheets.Add("Sheet1");
ws.Cells["A1"].LoadFromDataTable(dt, true);
using (var memoryStream = new MemoryStream())
{
pck.SaveAs(memoryStream);
return File(memoryStream.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"ExportFileName.xlsx");
}
}
}
//This is just a supporting example to illustrate setting up filters ..
/* [HttpPost]
public PartialViewResult GetFilteredPartial(MyFilter filter)
{
TempData["filterKeys"] = filter;
var filteredData = db.GetConcernedData(filter);
var model = new MainViewModel();
model.PartialViewModel = filteredData;
return PartialView("_SomePartialView", model);
} */
}
And here are the Views..
/*Commenting out the View code, in order to focus on the imp. code
#model Models.MainViewModel
#{Layout...}
Some code for, say, a partial View
<div id="tblSampleBody">
#Html.Partial("_SomePartialView", Model.PartialViewModel)
</div>
*/
//The actual part.. Just **posting** this bit of data from the complete View...
//Here, you are not posting the full Form..or the complete View
#using (Html.BeginForm("ExportData", "Home", FormMethod.Post))
{
<input type="submit" value="Export Data" />
}
//...
//</div>
/*And you may require to pass search/filter values.. as said in the accepted answer..
That can be done while 'searching' the data.. and not while
we need an export..for instance:-
<script>
var filterData = {
SkipCount: someValue,
TakeCount: 20,
UserName: $("#UserName").val(),
DepartmentId: $("#DepartmentId").val(),
}
function GetFilteredData() {
$("#loader").show();
filterData.SkipCount = 0;
$.ajax({
url: '#Url.Action("GetFilteredPartial","Home")',
type: 'POST',
dataType: "html",
data: filterData,
success: function (dataHTML) {
if ((dataHTML === null) || (dataHTML == "")) {
$("#tblSampleBody").html('<tr><td>No Data Returned</td></tr>');
$("#loader").hide();
} else {
$("#tblSampleBody").html(dataHTML);
$("#loader").hide();
}
}
});
}
</script>*/
The whole point of the trick seems that, we are posting a form (a part of the Razor View ) upon which we are calling an Action method, which returns: a FileResult, and this FileResult returns the Excel File..
And for posting the filter values, as said, ( and if you require to), I am making a post request to another action, as has been attempted to describe..
This thread helped me create my own solution that I will share here. I was using a GET ajax request at first without issues but it got to a point where the request URL length was exceeded so I had to swith to a POST.
The javascript uses JQuery file download plugin and consists of 2 succeeding calls. One POST (To send params) and one GET to retreive the file.
function download(result) {
$.fileDownload(uri + "?guid=" + result,
{
successCallback: onSuccess.bind(this),
failCallback: onFail.bind(this)
});
}
var uri = BASE_EXPORT_METADATA_URL;
var data = createExportationData.call(this);
$.ajax({
url: uri,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
success: download.bind(this),
fail: onFail.bind(this)
});
Server side
[HttpPost]
public string MassExportDocuments(MassExportDocumentsInput input)
{
// Save query for file download use
var guid = Guid.NewGuid();
HttpContext.Current.Cache.Insert(guid.ToString(), input, null, DateTime.Now.AddMinutes(5), Cache.NoSlidingExpiration);
return guid.ToString();
}
[HttpGet]
public async Task<HttpResponseMessage> MassExportDocuments([FromUri] Guid guid)
{
//Get params from cache, generate and return
var model = (MassExportDocumentsInput)HttpContext.Current.Cache[guid.ToString()];
..... // Document generation
// to determine when file is downloaded
HttpContext.Current
.Response
.SetCookie(new HttpCookie("fileDownload", "true") { Path = "/" });
return FileResult(memoryStream, "documents.zip", "application/zip");
}
CSL's answer was implemented in a project I'm working on but the problem I incurred was scaling out on Azure broke our file downloads. Instead, I was able to do this with one AJAX call:
SERVER
[HttpPost]
public FileResult DownloadInvoice(int id1, int id2)
{
//necessary to get the filename in the success of the ajax callback
HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");
byte[] fileBytes = _service.GetInvoice(id1, id2);
string fileName = "Invoice.xlsx";
return File(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, fileName);
}
CLIENT
(modified version of Handle file download from ajax post)
$("#downloadInvoice").on("click", function() {
$("#loaderInvoice").removeClass("d-none");
var xhr = new XMLHttpRequest();
var params = [];
xhr.open('POST', "#Html.Raw(Url.Action("DownloadInvoice", "Controller", new { id1 = Model.Id1, id2 = Model.Id2 }))", true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (this.status === 200) {
var filename = "";
var disposition = xhr.getResponseHeader('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
}
var type = xhr.getResponseHeader('Content-Type');
var blob = typeof File === 'function'
? new File([this.response], filename, { type: type })
: new Blob([this.response], { type: type });
if (typeof window.navigator.msSaveBlob !== 'undefined') {
// IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
window.navigator.msSaveBlob(blob, filename);
} else {
var URL = window.URL || window.webkitURL;
var downloadUrl = URL.createObjectURL(blob);
if (filename) {
// use HTML5 a[download] attribute to specify filename
var a = document.createElement("a");
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.location = downloadUrl;
} else {
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
}
} else {
window.location = downloadUrl;
}
setTimeout(function() {
URL.revokeObjectURL(downloadUrl);
$("#loaderInvoice").addClass("d-none");
}, 100); // cleanup
}
}
};
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send($.param(params));
});
This works for me. Make sure you return a File from your controller action with contentType as "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" and file name as e.g. "List.xlsx" which should be the same as in the AJAX success call. I have used ClosedXML NuGet package to generate the excel file.
$.ajax({
url: "Home/Export",
type: 'GET',
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
xhrFields: { responseType: 'blob' },
success: function (data) {
var a = document.createElement('a');
var url = window.URL.createObjectURL(data);
a.href = url;
a.download = 'List.xlsx';
a.click();
window.URL.revokeObjectURL(url);
}
});
I am using Asp.Net WebForm and just I wanna to download a file from server side. There is a lot article but I cannot find just basic answer.
Now, I tried a basic way and got it.
That's my problem.
I have to create a lot of input button dynamically on runtime. And I want to add each button to download button with giving an unique fileNumber.
I create each button like this:
fragment += "<div><input type=\"button\" value=\"Create Excel\" onclick=\"CreateExcelFile(" + fileNumber + ");\" /></div>";
Each button call this ajax method.
$.ajax({
type: 'POST',
url: 'index.aspx/CreateExcelFile',
data: jsonData,
contentType: 'application/json; charset=utf-8',
dataType: 'json',
success: function (returnValue) {
window.location = '/Reports/Downloads/' + returnValue.d;
}
});
Then I wrote a basic simple method.
[WebMethod]
public static string CreateExcelFile2(string fileNumber)
{
string filePath = string.Format(#"Form_{0}.xlsx", fileNumber);
return filePath;
}
I am generating this Form_1, Form_2, Form_3.... And I am going to delete this old files with another program. But if there is a way to just sending byte array to download file like using Response. I wanna to use it.
I hope this will be usefull for anyone.
On Submit form
public ActionResult ExportXls()
{
var filePath="";
CommonHelper.WriteXls(filePath, "Text.xls");
}
public static void WriteXls(string filePath, string targetFileName)
{
if (!String.IsNullOrEmpty(filePath))
{
HttpResponse response = HttpContext.Current.Response;
response.Clear();
response.Charset = "utf-8";
response.ContentType = "text/xls";
response.AddHeader("content-disposition", string.Format("attachment; filename={0}", targetFileName));
response.BinaryWrite(File.ReadAllBytes(filePath));
response.End();
}
}

Categories