I'm new to MCV and I'm learning MVC3. I created a model and a controller and view was generated for me. The generated code makes perfect sense to me. I wanted to modify the generated view and controller so that I could upload a file when I "create" a new record. There is a lot of good information out there about how to do this. Specifically I tried this: http://haacked.com/archive/2010/07/16/uploading-files-with-aspnetmvc.aspx
The problem is that even when I select a file (not large) and submit, there are no files in the request. That is, Request.Files.Count is 0.
If I create the controller and and view from scratch, in the same project (no model), the example works just fine. I just can't add that functionality to the generated page. Basically, I'm trying get the Create action to also send the file. For example, create a new product entry and send the picture with it.
Example Create view:
#model Product.Models.Find
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm("Create", "Find", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Find</legend>
<input type="file" id="file" />
<div class="editor-label">
#Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Description)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Description)
#Html.ValidationMessageFor(model => model.Description)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
Example Controller:
[HttpPost]
public ActionResult Create(Product product)
{
if (ModelState.IsValid)
{
if (Request.Files.Count > 0 && Request.Files[0] != null)
{
//Not getting here
}
db.Products.Add(product);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(find);
}
This will create the record just fine but there are not files associated with the Request.
I've also tried a controller action like this:
[HttpPost]
public ActionResult Create(HttpPostedFileBase file)
{
if (file.ContentLength > 0)
{
//Not getting here
}
return RedirectToAction("Index");
}
I'm wondering if maybe you can't post a file at the same time as posting form fields? If that is the case, what are some patterns for creating a new record and associating a picture (or other file) with it?
Thanks
Create a ViewModel which has properties to handle your image and Product deatils
public class ProductViewModel
{
public string ImageURL { set;get;}
public string Title { set;get;}
public string Description { set;get;}
}
And in your HTTPGET Action method, return this ViewModel object to your strongly typed view
public ActionResult Create()
{
ProductViewModel objVM = new ProductViewModel();
return View(objVM);
}
And in your View
#model ProductViewModel
<h2>Add Product</h2>
#using (Html.BeginForm("Create", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.TextBoxFor(m => m.Title) <br/>
#Html.TextBoxFor(m => m.Description ) <br/>
<input type="file" name="file" />
<input type="submit" value="Upload" />
#Html.HiddenFor(m => m.ImageURL )
}
Now in your HttpPost action method, accept this ViewModel and File
[HttpPost]
public ActionResult Create(HttpPostedFileBase file, ProductViewModel objVM)
{
if(file==null)
{
return View("Create",objVM);
}
else
{
//You can check ModeState.IsValid if you have to check any model validations and do further processing with the data here.
//Now you have everything here in your parameters, you can access those and save
}
}
You will have to create a ViewModel for Product (maybe ProductViewModel) and add a HttpPostedFileBase field with the same name as the field of the form and use that instead of the Product in the action of the controller.
A ViewModel is nothing but a model used for specific views. Most of the times, with extra data to generate the view or to decompose and form the model on the controller action.
public ProductViewModel
{
public string Cod { get; set; }
// All needed fields goes here
public HttpPostedFileBase File{ get; set; }
/// Empty constructor and so on ...
}
Related
I have a requirement to separate parts of one page into Partial Views and one of those parts contains a form to submit data. I've been playing around with this and have managed to get the form to submit without reloading the page.
However I have two problems:
The form fields don't clear after a successful post
If validation is broken, those validation messages don't appear when returning the result.
I'll admit i'm not too familiar with AJAX in ASP to begin with but hopefully someone can hope. Here's my code:
Model
using System.ComponentModel.DataAnnotations;
namespace MVCValidation.Models
{
public class Thing
{
public int Id { get; set; }
[Required]
public string Value { get; set; }
public string OtherValue { get; set; }
}
}
Main View (_Index.cshtml)
#model MVCValidation.Models.Thing
#{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Ajax Partial Test</h1>
</div>
<div class="row">
<div class="col">
<form asp-controller="Home" asp-action="Edit" data-ajax="true" data-ajax-method="POST">
#await Html.PartialAsync("_Form", Model)
</form>
</div>
</div>
Partial View (_Form.cshtml)
#model MVCValidation.Models.Thing
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary(true, "", new {#class = "text-danger"})
#Html.HiddenFor(m => m.Id)
<div class="form-group">
#Html.LabelFor(m => m.Value, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(m => m.Value, new {htmlAttributes = new { #class = "form-control" }})
#Html.ValidationMessageFor(m => m.Value, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="submit" class="btn btn-success"/>
</div>
</div>
</div>
Controller (HomeController)
namespace MVCValidation.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public Thing GetThing()
{
return new Thing(){Id = 1, OtherValue = "Other"};
}
public IActionResult Index()
{
return View(GetThing());
}
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult Edit(Thing thing)
{
if(ModelState.IsValid)
{
ModelState.Clear();
return PartialView("_Form", GetThing());
}
return PartialView("_Form", thing);
}
}
}
In my _Layout view I have the jquery.unobtrusive-ajax.min.js referenced and it's loading fine. Please can anyone suggest where I'm going wrong?
So, I eventually found this article: https://damienbod.com/2018/11/09/asp-net-core-mvc-ajax-form-requests-using-jquery-unobtrusive/ and saw what I was doing wrong.
I expanded my tag to look like below:
<form asp-controller="Home" asp-action="Edit"
data-ajax="true"
data-ajax-method="POST"
data-ajax-mode="replace"
data-ajax-update="#result">
<div id="result">
#await Html.PartialAsync("_Form", Model)
</div>
</form>
the PartialAsync call now takes place inside a div that ultimately will be the target for the result to populate... so it effectively replaces itself.
I also had to change the controller method to this:
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult Edit(Thing thing)
{
if(ModelState.IsValid)
{
return RedirectToAction(nameof(Index));
}
return PartialView("_Form", thing);
}
This correctly returns the partial view when the model is invalid, and allows the page to be used again if it is valid.
I am using MVC4.
Validation is failing but validation error messages are not getting displayed.
This is my model.
public class Configuration
{
public int Id { get; set; }
[Required(AllowEmptyStrings = false, ErrorMessage = "Site name is required.")]
[MinLength(6, ErrorMessage = "Name should be at least 6 characters.")]
public string SiteName { get; set; }
}
Controller.
[HttpPost]
public ActionResult Create(Configuration configItem)
{
if (ModelState.IsValid)
{
// do something.
}
return View("Index", configItem);
}
View is
#model Models.SitesConfig.Configuration
#{
ViewBag.Title = "Sites Configurations";
}
<div>
#Html.ActionLink("Sites List", "List", "SiteConfig")
</div>
#using (Html.BeginForm("Create", "SiteConfig", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<fieldset>
<legend>New Satellitesite</legend>
<div>
#Html.LabelFor(m => m.SiteName, "Name")
#Html.TextBoxFor(m => m.SiteName)
#Html.ValidationMessageFor(m=>m.SiteName)
</div>
<br />
<input type="submit" value="Save" />
</fieldset>
}
Please also suggest me if there is any better way of doing the validations.
I don't know what was the problem. After clean and build the application, I am able to see the error message.
Thanks,
Naresh
There should be #Html.ValidationSummary("Please correct the errors") in the View
#using (Html.BeginForm("Create", "SiteConfig", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.ValidationSummary()
<fieldset>
<legend>New Satellitesite</legend>
<div>
#Html.LabelFor(m => m.SiteName, "Name")
#Html.TextBoxFor(m => m.SiteName)
#Html.ValidationMessageFor(m=>m.SiteName)
</div>
<br />
<input type="submit" value="Save" />
</fieldset>
}
Try this one .. #Html.ValidationSummary() helps in displaying the error messages.
If you try the above solutions and still didn't work, then maybe you need to format your View file. Go to Edit-Advanced-Format Document. Remember not to be in the debug mode when trying to format your file because you wouldn't see the above listed procedures if you are debugging your project.
Make sure your View is referencing the jQuery validations in the Scripts tag -
#section Scripts
{
#Scripts.Render("~/bundles/jqueryval")
}
I have multiple file upload Views with ViewModel binding as following:
#model IVRControlPanel.Models.UploadNewsModel
#using (Html.BeginForm("index", "NewsUpload", FormMethod.Post, new { name = "form1", #id = "form1" }))
{
#Html.ValidationSummary(true)
<div class="field fullwidth">
<label for="text-input-normal">
#Html.Label("Select Active Date Time")</label>
<input type="text" id="active" value="#DateTime.Now" />
#Html.ValidationMessageFor(model => model.ActiveDateTime)
</div>
<div class="field fullwidth">
<label>
#Html.Label("Select Language")
</label>
#Html.DropDownList("Language", (SelectList)ViewBag.lang)
</div>
<div class="field">
<label>
#Html.Label("General News")
</label>
#Html.TextBoxFor(model => model.generalnews, new { name = "files", #class="custom-file-input", type = "file" })
#Html.ValidationMessageFor(model => model.generalnews)
</div>
<div class="field">
<label>
#Html.Label("Sports News")
</label>
#Html.TextBoxFor(model => model.sportsnews, new { name = "files", #class = "custom-file-input", type = "file" })
#Html.ValidationMessageFor(model => model.sportsnews)
</div>
<div class="field">
<label>
#Html.Label("Business News")
</label>
#Html.TextBoxFor(model => model.businessnews, new { name = "files", #class = "custom-file-input", type = "file" })
#Html.ValidationMessageFor(model => model.businessnews)
</div>
<div class="field">
<label>
#Html.Label("International News")
</label>
#Html.TextBoxFor(model => model.internationalnews, new { name = "files", #class = "custom-file-input", type = "file" })
#Html.ValidationMessageFor(model => model.internationalnews)
</div>
<div class="field">
<label>
#Html.Label("Entertaintment News")
</label>
#Html.TextBoxFor(model => model.entertaintmentnews, new { name = "files", #class = "custom-file-input", type = "file" })
#Html.ValidationMessageFor(model => model.entertaintmentnews)
</div>
<footer class="pane">
<input type="submit" class="bt blue" value="Submit" />
</footer>
}
View model with data annotation for validating file upload for allowed extension as follows:
public class UploadNewsModel
{
public DateTime ActiveDateTime { get; set; }
// public IEnumerable<SelectListItem> Language { get; set; }
[File(AllowedFileExtensions = new string[] { ".jpg", ".gif", ".tiff", ".png", ".pdf", ".wav" }, MaxContentLength = 1024 * 1024 * 8, ErrorMessage = "Invalid File")]
public HttpPostedFileBase files { get; set; }
}
Controller: for saving multiple file and return view if error is exist
[HttpPost]
public ActionResult Index(UploadNewsModel news, IEnumerable<HttpPostedFileBase> files)
{
if (ModelState.IsValid)
{
foreach (var file in files)
{
if (file != null && file.ContentLength > 0)
{
var fileName = Path.GetFileName(file.FileName);
var serverpath = Server.MapPath("~/App_Data/uploads/News");
var path = Path.Combine(serverpath, fileName);
if (!Directory.Exists(serverpath))
{
Directory.CreateDirectory(serverpath);
}
file.SaveAs(path);
}
}
}
return View(news);
}
}
Problem explaination
How do I define view model for those five file upload input control so that corresponding error is shown for respective validation error if file extension of file uploaded is not allowed type. I have only one view model items for all five file upload control.
What can be best way to define view model for those multiple file upload control for showing respective validation error instead user try to upload file of unallowed extension???
The real problem here is that MVC doesn't have a decent model binder for http files.
So unless you use a project like mvc futures which has some extra support around file uploads you are going to have to get some what dirty and do the hard work yourself.
Here is an example that might work a bit better for you.
Firstly I would create a ViewModel to represent one file something like this:
public class FileViewModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public bool Delete { get; set; }
public string ExistingUrl { get; set; }
public HttpPostedFileBase FileBase { get; set; }
}
Obviously properties depend on your requirements, the important bit is the FileBase and that it is its own model.
Next, a ViewModel for your page (UploadNewsModel in your case):
public class IndexViewModel
{
public IList<FileViewModel> Files { get; set; }
}
The important bit here is the IList of Files, this is how we capture multiple files (in your current implementation with 'file' you are only capturing one.
Onto the page level view:
#model IndexViewModel
<form method="post" action="#Url.Action("Index")" enctype="multipart/form-data">
#Html.ValidationSummary(true)
#Html.EditorFor(x => x.Files)
<input type="submit" value="Submit" />
</form>
Note the EditorFor, what we will do next is create a EditorTemplate for the FileViewModel which should be used at this point.
Like this:
#model FileViewModel
<h4>#Model.Name</h4>
#Html.HiddenFor(x => x.Id)
#Html.CheckBoxFor(x => x.Delete)
<input #( "name=" + ViewData.TemplateInfo.HtmlFieldPrefix + ".FileBase") type="file" />
Note the usage of ViewData.TemplateInfo.HtmlFieldPrefix, it kinda sucks but like I said this is because of the poor mvc support for the file input type. We have to do it ourselves. We except the name for each file to be something like '[0].FileBase' etc.
This will populate the FileViewModel correctly on the POST action.
So far so good, what about validation?
Well again you can do that manually on the server. Test the file extensions yourself, and simply use the following to add the error to the model like:
ModelState.AddModelError("","Invalid extension.")
On another note, extension validation should be done on the client side (and as well as on the server side)
I am trying to create what I feel is a very simple form submission using a ViewModel. I have worked on this off and on all day and for some reason cannot understand why when my app gets to my HttpPost action my EmailViewModel is empty. I get a "NullReference Exception Occurred" "Object reference not set to an instance of an object" error.
Can you take a look at my code and tell me where I am being crazy?
Here is my httpPost action:
[HttpPost]
public ActionResult SendStudentAnEmail(EmailViewModel email)
{
Debug.Write(email.Subject); // First NullReferenceException
Debug.Write(email.Body);
Debug.Write(email.Email);
etc. . .
My ViewModel:
namespace MyApp.ViewModels
{
public class EmailViewModel
{
public string Email { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
}
and My View:
#model MyApp.ViewModels.EmailViewModel
#{
ViewBag.Title = "SendStudentAnEmail";
}
<h2>SendStudentAnEmail</h2>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>EmailViewModel</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Email)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Email)
#Html.ValidationMessageFor(model => model.Email)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Subject)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Subject)
#Html.ValidationMessageFor(model => model.Subject)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Body)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Body)
#Html.ValidationMessageFor(model => model.Body)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
Thank you.
*UPDATE*
If I change my HttpPost Action to use FormCollection, I can use the values just fine, I can even re-cast the FormCollection values back to my EmailViewModel. Why is this?
[HttpPost]
public ActionResult SendStudentAnEmail(FormCollection emailFormCollection)
{
Debug.Write(emailFormCollection["email"]);
Debug.Write(emailFormCollection["subject"]);
Debug.Write(emailFormCollection["body"]);
var email = new EmailViewModel
{
Email = emailFormCollection["email"],
Subject = emailFormCollection["subject"],
Body = emailFormCollection["body"]
};
. . . . then the rest of my code works just how I wanted. . .
Why do I have to cast from FormCollection over to my EmailViewModel? Why isn't it giving me the NullReference Exception if I attempt to simply push an EmailViewModel into my Action?
Your EmailViewModel class has a property called Email of type string. And your controller action takes an argument called email of type EmailViewModel. This confuses the default model binder. So either rename the property inside the view model or the action argument:
[HttpPost]
public ActionResult SendStudentAnEmail(EmailViewModel model)
{
Debug.Write(model.Subject);
Debug.Write(model.Body);
Debug.Write(model.Email);
...
}
I have a simplified test scenario useful for asking this question: A Product can have many Components, a Component can belong to many Products. EF generated the classes, I've slimmed them as follows:
public partial class Product
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Component> Components { get; set; }
}
public partial class Component
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
The creation of a component is accomplished via these controller actions:
public ActionResult Create(int ProductId)
{
Product p = db.Products.Find(ProductId);
Component c = new Component();
c.Products.Add(p);
return PartialView(c);
}
[HttpPost]
public ActionResult Create(Component model)
{
db.Components.Add(model);
db.SaveChanges();
}
and the view returned by the GET method looks like this:
#model Test.Models.Product
<fieldset>
<legend>Product</legend>
<div class="display-label">Name</div>
<div class="display-field">#Model.Name</div>
</fieldset>
#Html.Action("Create", "Component", new {ProductId = Model.Id})
<p>
#Html.ActionLink("Edit", "Edit", new { id=Model.Id }) |
#Html.ActionLink("Back to List", "Index")
</p>
From which can be seen that the component creation is handled on the same page via the above Html.Action - the code for that view follows:
#model Test.Models.Component
#using Test.Models
<script type="text/javascript">
function Success() {
alert('ok');
}
function Failure() {
alert('err');
}
</script>
#using (Ajax.BeginForm("Create", "Component", new AjaxOptions
{
HttpMethod = "Post",
OnSuccess = "Success",
OnFailure = "Failure"
}))
{
<fieldset>
<legend>Components</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
#Html.HiddenFor(x => x.Products.First().Id)
#Html.HiddenFor(x => x.Products)
#foreach (Product p in Model.Products)
{
#Html.Hidden("Products[0].Id", p.Id)
}
#foreach (Product p in Model.Products)
{
#Html.Hidden("[0].Id", p.Id)
}
</fieldset>
<input type="submit" value="go" />
}
ok. so this is what I'm struggling with: I need the model parameter of the [HttpPost]back to get properly populated i.e. it should contain a Product, since I can't create the new component with a null product. To get the product I need to look it up via the product's id. I expect I should be able to do:
model.Products.Add(db.Products.Find(model.Products.First().Id));
or some such thing, which relies on model receiving the id. This means the view has to place the id there, presumably in a hidden field, and as can be seen from my view code, I've made several attempts at populating this, all of which have failed.
Normally I prefer the *For methods since they become responsible for generating correct nomenclature. If .Products were singular (.Product), I could reference it as x => x.Product.Id and everything would be fine, but since it's plural, I can't do x => x.Products.Id so I tried x => x.Products.First().Id which compiles and produces the right value but gets name Id (which is wrong since the model binder thinks it's Component.Id and not Component.Products[0].Id.
My second attempt was to let HiddenFor iterate (like I would with EditorFor):
#Html.HiddenFor(x => x.Products)
but that produces nothing - I've read that this helper doesn't iterate. I tried x => x.Products.First() but that doesn't even compile. Finally, I decided to abandon the *For and code the name myself:
#foreach (Product p in Model.Products)
{
#Html.Hidden("Products[0].Id", p.Id)
and though that looks right, the postback doesn't see my value (Products.Count == 0). I saw in some posting that format should look like [0].Id but that doesn't work either. grr...
I gather I could code it like this:
#Html.Hidden("ProductId", p.Id)
and then redeclare my controller action like this:
[HttpPost] ActionResult Create(Component model, int ProductId)
but that seems eecky. it's hard to believe this is so difficult. can anyone help?
e
p.s. I have a project I could make available for download if anyone cares
Instead of writing those foreach loops try using editor templates:
<fieldset>
<legend>Components</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
#Html.EditorFor(x => x.Products)
</fieldset>
and inside the corresponding editor template (~/Views/Shared/EditorTemplates/Product.cshtml)
#model Product
#Html.HiddenFor(x => x.Id)