I have an MVC3 application that uploads a file from the users' hard drive and manipulates it. One requirement is that the file extension should be .xls(or xlsx).
I would like to validate the file name but I would like to know what is the best option in terms of reusability and performance (and of course best coding practices).
I have the following Index view:
#using (Html.BeginForm("Index", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<br />
<p><input type="file" id="file" name="file" size="23"/></p><br />
<p><input type="submit" value="Upload file" /></p>
}
Called by the controller action method Index:
public ActionResult Index()
{
return View("Index");
}
And the user response is handled by the Index action method (HttpPost):
[HttpPost]
public ActionResult Index(HttpPostedFileBase file)
{
FileServices lfileServices = new FileServices();
var lfilePath = lfileServices.UploadFile(file, "~/App_Data/uploads");
//Manipulation;
}
Now I can use the Path.GetExtension(filename) in the FileServices to get the extension and then perform the check but it will be performed just after the upload completes.
I would like to do it once the user attempts to upload the file. The only thing that came up to my mind is create a Model(or better a ViewModel) and use the remote validation from DataAnnotations.
I saw a demonstration from Scott Hanselman live but it seemed like he was not really confortable with that because the application was compiling but was not performing the check.
Has anybody a good approach in order to perform such kind of remote validation or any other solution (jQuery for instance)?
Thanks
Francesco
You could do this using javascript:
$(function () {
$('form').submit(function () {
var selectedFile = $('#file').val();
var matches = selectedFile.match(/\.(xlsx?)$/i);
if (matches == null) {
alert('please select an Excel file');
return false;
}
return true;
});
});
Of course this doesn't in any case free you from the obligation of performing the same check on the server because if the client has no javascript enabled that will be the only way. And even this wouldn't be 100% reliable as there is nothing preventing users from renaming any garbage file to .xls and upload it. Heuristics could be used on the server to try to guess the actual file type by looking at some known byte sequences.
UPDATE:
Example with remote AJAX validation (due to demand in the comments, I don't recommend it though). You could use the excellent jquery.validate plugin which by the way comes bundled with ASP.NET MVC 3:
<script src="#Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
#using (Html.BeginForm("Index", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<input type="file" id="file" name="file" size="23" data-remote-val-url="#Url.Action("CheckExtension")"/>
<br />
<input type="submit" value="Upload file" />
}
<script type="text/javascript">
$('form').validate({
rules: {
file: {
remote: $('#file').data('remote-val-url')
}
},
messages: {
file: {
remote: 'Please select an Excel file'
}
}
});
</script>
and on the server:
public ActionResult CheckExtension(string file)
{
var extension = Path.GetExtension(file ?? string.Empty);
var validExtensions = new[] { ".xls", ".xlsx" };
var isValid = validExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
return Json(isValid, JsonRequestBehavior.AllowGet);
}
Related
I have a web app in which has the following features:
Extract data from an MS Excel file
Process the data and store it in data structures based on certain criteria.
Pass the data structures from the controller to the view where they can be rendered.
The issue I'm having occurs with the first few steps. On my view page, I have a button where the user can upload the excel file. Once they click submit, a POST request is sent to transmit the file to the controller action (I'm using the index action for this which I'm not sure is correct) where the data is extracted from the file. Once the file is processed, I want to display the extracted data back on the same page as the upload button.
I've tried to implement this first by creating a class in the controller which is instantiated for each excel row and then each row is stored in one of three different lists of objects.
I then stored each of these lists in the ViewBag object:
//Handle POST request and determine in the file uploaded was of correct type
List<Dictionary<string, string>> dictionary = new List<Dictionary<string, string>>();
bool isSuccess = true;
int colID = 0;
int colTier = 0;
if (Request != null)
{
HttpPostedFileBase file = Request.Files["UploadedFile"];
if ((file != null) && (file.ContentLength > 0) && !string.IsNullOrEmpty(file.FileName))
{
string fileName = "";
//string fileinitPath = "//app235wnd1t/equityfrontoffice/INGDS/TierBulkUpload/";
string fileinitPath = "C:/Users/chawtho/Desktop/";
Regex regex = new Regex("WTDA.+xlsx"); //find correct filename
if (match.Success)
{
fileName = (match.Value);
}
if (fileName != "")
{
Match match = regex.Match(file.FileName);
//Extract data from excel file and store in collections
ViewBag.inactive_subscriptions = inactiveSubscriptions;
ViewBag.active_subscriptions = activeSubscriptions;
}
return View();
}
In the view I have the following:
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title></title>
</head>
<body>
random text
#using (Html.BeginForm("Index", "Subscription", FormMethod.Post, new { enctype = "multipart/form-data" })) {
<fieldset class="form">
<legend>Upload Document</legend>
<input type="file" name="UploadedFile" id="FileUpload" required />
<input type="submit" name="Submit" value="Upload" />
<label id="saveStatus" style="color: red">
</label>
</fieldset>
}
#{
<li>#ViewBag.inactive_subscriptions[0].ID</li>
}
Here, I'm simply trying to read the ID field of the first object in the list of Subscriptions but I get the error:
Cannot perform runtime binding on a null reference
I'm not sure where this error is coming from because when I debug the controller code, the Viewbag is populated with the two lists before the View() is returned. I also tried moving the Subscription Class from the controller to a model class and created a container to hold the list of subscriptions but that didn't resolve the issue.
I think the problem might have something to do with the fact that the code the print the viewbag data is present when the page initially loads but I'm not sure if/how it should be kept from running until the file is processed.
How should I go about structuring this mvc setup to implement what I have outlined?
I put the next example, this is the way that I use to manipulate the excel files, notice that I don't use viewbag variables, this could be an option, like I've said I return the processed data into a json object and then I manipullate it via javascript.
-- Razor ViewPage
<!--Upload File-->
#using (Html.BeginForm("ProcessExcelFile", "ControllerName", FormMethod.Post,
new { id = "formUploadExcel", enctype = "multipart/form-data" }))
{
<div class="row">
<div class="col-md-4">
<label for="file">Excel File (.xls, .xlsx)</label>
<input type="file" name="file" id="file" required="required">
</div>
</div>
<br>
<button type="submit" class="btn btn-primary">Upload File</button>
}
-- JS Script
<script type="text/javascript">
$('form#formUploadExcel').unbind('submit').bind('submit', function () {
formdata = new FormData($('form#formUploadExcel').get(0));
$.ajax({
url: this.action,
type: this.method,
cache: false,
processData: false,
contentType: false,
data: formdata,
success: function (data, status) {
console.log(data);
},
complete: function () {
//code...
}
});
return false;
});
</script>
-- Controller
[HttpPost]
public JsonResult ProcessExcelFile(HttpPostedFileBase file)
{
// Process the excel...
var business = new BusinessLayer();
var data = business.ValidateExcel(file);
// Return the Json with the procced excel data.
return Json(data, JsonRequestBehavior.AllowGet);
}
I have two Create methods one decorated with HttpGet, and the other with HttpPost. I have a create view for the first one looking like this :
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
<form action="/" method="post">
<input type="text" name="txt" value="" />
<input type="submit" />
</form>
The methods :
List<string> myList = new List<string> { "element1", "element2", "element3" };
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(string txt)
{
//myList.Add(Request.Form["txt"]);
myList.Add(txt);
return View();
}
I am simly trying to pass the data from my form on button to my second Create() and save it to myList.
I need some advice on how to make this work.
Once you've fixed your form (in that you're posting back to your application's default route (by default HomeController.Index() method) by sending the request to /, instead of your Create method), you are actually correctly adding the value to your list. The problem is, that value only stays for the current request.
To make things persistent, you need to consider a persistence layer in memory, in database, or in session. I've provided a full sample below that uses the session, which will give you a per-user list instance. Without this layer, your Controller is being routinely disposed of once the action has completed processing, and so the amends to your list are not persisted. This is the normal request lifecycle in ASP.NET and makes sense when you consider that your app is basically only ever dealing with 1 request at a time. It's important to note that making something static isn't a form of persistence per-se, in that its lifetime and reliability is indeterminable. It will appear to work, but once your application pool recycles (ie. the app is destroyed and reloaded in memory) you will have again lost all amends to your list.
I would suggest you read up on Session State to understand exactly what is going on below. In a nutshell, each application user / unique visitor to your site will be given a unique 'session ID', you can then use this session ID to store data that you wish to use on the server side. This is why, if you were to visit your Create method from separate browsers (or try Private mode) you will be maintaining two separate lists of data.
View (which also outputs the list to the user):
#model List<string>
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
<ul>
#foreach(var str in Model)
{
<li>#str</li>
}
</ul>
#using (Html.BeginForm())
{
<input type="text" name="txt" />
<input type="submit" />
}
Controller contents:
public List<string> MyList
{
get
{
return (List<string>)(
// Return list if it already exists in the session
Session[nameof(MyList)] ??
// Or create it with the default values
(Session[nameof(MyList)] = new List<string> { "element1", "element2", "element3" }));
}
set
{
Session[nameof(MyList)] = value;
}
}
public ActionResult Create()
{
return View(MyList);
}
[HttpPost]
public ActionResult Create(string txt)
{
MyList.Add(txt);
return View(MyList);
}
Please use this:
#using (Html.BeginForm("Create", "Controller", FormMethod.Post)){
<input type="text" name="txt" value="" />
<input type="submit" />
}
Replace Controller with your Controller name.
Or simply use:
#using (Html.BeginForm()){
<input type="text" name="txt" value="" />
<input type="submit" />
}
When you call BeginForm() without any parameters it default to using the same controller/action used to render the current page.
I'm attempting to wrap my head around .NET MVC5 routing.
I've got a form:
#using (Html.BeginForm("ProductsCheaperThan", "Home", FormMethod.Post))
{
<input type="text" name="comparisonPrice" />
<button type="submit">Search!</button>
}
And I've got a controller Home and an action ProductsCheaperThan which takes a parameter comparisonPrice
public ActionResult ProductsCheaperThan(decimal comparisonPrice)
{
ViewBag.FilterPrice = comparisonPrice;
var resultSet = new ProductService().GetProductsCheaperThan(comparisonPrice);
return View(resultSet);
}
This posts the value in the input (let's suppose that the value I'm posting is 20) back to my action, and correctly routes me to ~/Home/ProductsCheaperThan. The problem is, I'd like to be routed to ~/Home/ProductsCheaperThan/20
I'd like to do this so that if somebody bookmarks the page they don't end up getting an error when they revisit the page.
I thought that adding something like:
routes.MapRoute(
name: "ProductsCheaperThan",
url: "Home/ProductsCheaperThan/{comparisonPrice}",
defaults: new { controller = "Home", action = "ProductsCheaperThan", comparisonPrice = 20 }
);
might work, and I have one solution to my problem which changes the form to a GET
#using (Html.BeginForm("ProductsCheaperThan", "Home", FormMethod.Get))
and produces a URL of ~/Home/ProductsCheaperThan?comparisonPrice=20, but that uses a query string instead, and isn't exactly what I was aiming for.
Can anybody help me get my URL right?
You should add [HttpPost] attribute to your action
[HttpPost]
public ActionResult ProductsCheaperThan(decimal comparisonPrice)
{
ViewBag.FilterPrice = comparisonPrice;
var resultSet = new ProductService().GetProductsCheaperThan(comparisonPrice);
return View(resultSet);
}
One option is to use JQuery -
<div>
<input type="text" name="comparisonPrice" id="comparisonPrice" />
<button type="button" id="Search">Search!</button>
</div>
#section scripts{
<script>
$(function () {
$("#Search").click(function () {
window.location = "#Url.Action("PriceToCompare", "Home")" + "/" + $("#comparisonPrice").val();
});
});
</script>
}
Above script will result in - http://localhost:1655/PriceToCompare/Home/123
I think you can specify your route values using an overload:
#using (Html.BeginForm("Login", "Account", new { comparisonPrice= "20" }))
{
...
}
I have an ASP.NET MVC Website that has a dropdown list that is being created using this in the view...
#Html.DropDownList("Programs")
Programs is populated from a Business Object collection and stuffed into the ViewBag in the index action on the Home Controller...
\\get items...
ViewBag.Programs = items;
The view also has potentially three files I am getting like this in the same view...
<input type="file" name="files" id="txtUploadPlayer" size="40" />
<input type="file" name="files" id="txtUploadCoaches" size="40" />
<input type="file" name="files" id="txtUploadVolunteers" size="40" />
All of the aforementioned controls are contained in a Form that is created in the view using...
#using (Html.BeginForm("Index", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<!-- file and other input types -->
<input type="submit" name="btnSubmit" value="Import Data" />
}
My problems is that I cannot find a way to process all of the files AND reference the form fields.
Specifically, I need to know what Program the user selected from the dropdown.
I can process the files using this code with no problem...
[HttpPost]
public ActionResult Index(IEnumerable<HttpPostedFileBase> files)
//public ActionResult Index(FormCollection form)
{
_tmpFilePath = Server.MapPath("~/App_Data/uploads");
if (files == null) return RedirectToAction("Index");
foreach (var file in files)
{
if (file != null && file.ContentLength > 0)
{
var fileName = Path.GetFileName(file.FileName);
var path = Path.Combine(_tmpFilePath, fileName);
if (System.IO.File.Exists(path)) System.IO.File.Delete(path);
_file = file;
file.SaveAs(path);
break; //just use the first file that was not null.
}
}
//SelectedProgramId = 0;
//DoImport();
return RedirectToAction("Index");
}
But I cannot figure how to ALSO get access to the POST form values especially the Programs dropdown selected value (and for the record there is also a checkbox that I cannot read the value from.) Fiddler shows me that the Response has the file references AND the selected program but I cannot figure out how to get them out of the POST using ASP.NET MVC.
I know this question is pretty basic but I am stilling learning the whole web/http thing not just MVC.
EDIT
Thanks for your answers. I had the thought that the answer might lie in passing in both the files and the form values into the POST.
So my last question is ... how do I change the HTML.BeginForm block to pass in both the files and form values? Right now I have ...
#using (Html.BeginForm("Index", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
//do stuff
}
What should that using statement be to get both form values and files as separate parameters of the ActionResult?
EDIT MY EDIT
It seems that I don't have to make any changes...the debugger is showing that both files and form are non-null. Cool! Is that right?
I think that this should do it
[HttpPost]
public ActionResult Index(IEnumerable<HttpPostedFileBase> files, FormCollection form)
{
//handle the files
//handle the returned form values in the form collection
}
You should be able to pass in 2 parameters in the [HttpPost] action. you can also pass in the HTML name.
Edit: I also had problems with forms in ASP.net. I suggest looking into this blog post by Scott Allen.
http://odetocode.com/blogs/scott/archive/2009/04/27/6-tips-for-asp-net-mvc-model-binding.aspx
Use a ViewModel type that contains both the posted files and form values, or use the HttpRequest (accessed via the Controller.Request property) object, which has .Form[key] for POST values and .Files[key] for posted files.
I'm trying to make uploading a file to the server at my mvc project. I wrote my class,
public class MyModule: IHttpModule
which defines the event
void app_BeginRequest (object sender, EventArgs e)
In it, I check the length of the file that the user has selected to send.
if (context.Request.ContentLength> 4096000)
{
//What should I write here, that file is not loaded? I tried
context.Response.Redirect ("address here");
//but the file is still loaded and then going on Redirect.
}
In ASP.NET MVC you don't usually write http modules to handle file uploads. You write controllers and inside those controllers you write actions. Phil Haack blogged about uploading files ni ASP.NET MVC:
You have a view containing a form:
<% using (Html.BeginForm("upload", "home", FormMethod.Post,
new { enctype = "multipart/form-data" })) { %>
<label for="file">Filename:</label>
<input type="file" name="file" id="file" />
<input type="submit" />
<% } %>
And a controller action to handle the upload:
[HttpPost]
public ActionResult Upload(HttpPostedFileBase file)
{
if (file != null && file.ContentLength > 0)
{
if (file.ContentLength > 4096000)
{
return RedirectToAction("FileTooBig");
}
var fileName = Path.GetFileName(file.FileName);
var path = Path.Combine(Server.MapPath("~/App_Data/uploads"), fileName);
file.SaveAs(path);
}
return RedirectToAction("Index");
}