I am creating a feature in my app to process an uploaded CSV file containing multiple records to be imported. Data needs to be validated, and I want to show any validation errors BEFORE the Import button is clicked. High-level plan:
Step 1: Upload CSV file
Step 2: Display all records from CSV file and any validation errors next to each record (missing required fields, etc.)
Step 3: Click "Import" in order to actually import the valid records.
Here's a simplified version of what I have:
User View Model
public class UserViewModel
{
[Required]
[StringLength(100)]
public string Name { get; set; }
[Required]
[StringLength(150)]
public string Email { get; set; }
[Required]
[StringLength(10)]
public string Phone { get; set; }
}
File Upload Action Post
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Upload(HttpPostedFileBase csvFile)
{
// var csvRecords = do stuff to retrieve data from CSV file
var newUsersToCreate = new List<UserViewModel>();
foreach (var csvRecord in csvRecords)
{
newUsersToCreate.Add(new UserViewModel
{
Name = csvRecord.Name,
Email = csvRecord.Email,
Phone = csvRecord.Phone
});
}
return View("ImportPreview", newUsersToCreate);
}
View ImportPreview.cshtml
#model IEnumerable<App.ViewModels.UserViewModel>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "alert alert-danger", role = "alert" })
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Validation Errors</th>
</tr>
</thead>
<tbody>
#Html.EditorFor(model => model)
</tbody>
</table>
<button type="submit">Import</button>
}
Editor Template for UserViewModel.cshtml
#model App.ViewModels.UserViewModel
<tr>
<td>
#Html.HiddenFor(model => model.Name)
#Html.DisplayFor(model => model.Name)
</td>
<td>
#Html.HiddenFor(model => model.Email)
#Html.DisplayFor(model => model.Email)
</td>
<td>
#Html.HiddenFor(model => model.Phone)
#Html.DisplayFor(model => model.Phone)
</td>
<td>
#Html.ValidationMessageFor(model => model.Name, "", new { #class = "text-danger" })
#Html.ValidationMessageFor(model => model.Email, "", new { #class = "text-danger" })
#Html.ValidationMessageFor(model => model.Phone, "", new { #class = "text-danger" })
</td>
</tr>
Problem
While this generates a nice "preview" table with all prepared User records as essentially rows of hidden fields ready to go, the problem is that it does not display validation errors until the Import button is clicked.
How can I get it to show per-field validation errors in each row, right after the return View('ImportPreview', newUsersToCreate) comes back with the view?
You could do this in the view by checking if the $.validator is valid. Since hidden inputs are not validated by default, you also need to override the validator. Add the following after the jquery-{version}.js, jquery.validate.js and jquery.validate.unobtrusive.js scripts (but not in $(document).ready())
<script>
// override validator to include hidden inputs
$.validator.setDefaults({
ignore: []
});
// validate form and display errors
$('form').valid();
</script>
Note that you might include a (say) <p id="error" style="display:none;"> tag containing a 'general' error message that the data is invalid and use
if ($('form').valid()) {
$('#error').show();
}
The disadvantage is that you need to include the jQuery scripts that otherwise are not needed.
Another option is to validate in the controller using TryValidateObject on each item in the collection, and add any errors to ModelState which will be displayed in your ValidationMessageFor() placeholders. Note the following assumes csvRecords implements IList<T> so that you can use a for loop.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Upload(HttpPostedFileBase csvFile)
{
// var csvRecords = do stuff to retrieve data from CSV file
var newUsersToCreate = new List<UserViewModel>();
for (int i = 0; i < csvRecords.Count; i++)
{
UserViewModel model = new UserViewModel
{
Name = csvRecords[i].Name,
....
};
newUsersToCreate.Add(model);
// validate the model and include the collection indexer
bool isValid = ValidateModel(model, i));
}
return View("ImportPreview", newUsersToCreate);
}
private bool ValidateModel(object model, int index)
{
var validationResults = new List<ValidationResult>();
var context = new ValidationContext(model);
if (!Validator.TryValidateObject(model, context, validationResults, true))
{
foreach (var error in validationResults)
{
string propertyName = $"[{index}].{error.MemberNames.First()}";
ModelState.AddModelError(propertyName, error.ErrorMessage);
}
return false;
}
return true;
}
The advantage of the controller code is that you could add an additional property to your view model (say bool IsValid) and use it for conditional styling of your table rows, and that you could decide that if there are 'too many' errors, you could just display a different view rather that rendering the whole table and displaying potentially hundreds of repeated error messages
Related
I want to save the file path to my database reports table. I have a column of type: string FilePath.
The end goal is that I want to be able to download the file from a report details view. Obviously the report download link would be different depending on the report ID.
Currently it doesn't seem that the controller is receiving anything as before I had Object reference not set to an instance of an object exception. I then added file != null in my if statement so I don't get the error anymore. However clearly the underlying issue is still present. Here is my controller save action:
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "AdminManager")]
public ActionResult Save(Report report, HttpPostedFileBase file)
{
if (!ModelState.IsValid)
{
var viewModel = new ReportFormViewModel
{
Report = report,
Members = _context.Members.ToList(),
Subjects = _context.Subjects.ToList()
};
return View("ReportForm", viewModel);
}
if (file != null && file.ContentLength > 0)
{
string filePath = Path.Combine(
Server.MapPath("~/App_Data/Uploads"),
Path.GetFileName(file.FileName));
file.SaveAs(filePath);
}
if (report.Id == 0)
_context.Reports.Add(report);
else
{
var reportInDb = _context.Reports.Single(e => e.Id == report.Id);
reportInDb.Name = report.Name;
reportInDb.MemberId = report.MemberId;
reportInDb.SubjectId = report.SubjectId;
reportInDb.Date = report.Date;
reportInDb.FilePath = report.FilePath;
}
_context.SaveChanges();
return RedirectToAction("Index", "Report");
}
Here is my form view:
<h2>#Model.Title</h2>
#using (Html.BeginForm("Save", "Report", new {enctype = "multipart/form-data"}))
{
<div class="form-group">
#Html.LabelFor(r => r.Report.Name)
#Html.TextBoxFor(r => r.Report.Name, new { #class = "form-control" })
#Html.ValidationMessageFor(r => r.Report.Name)
</div>
<div class="form-group">
#Html.LabelFor(r => r.Report.Date) e.g. 01 Jan 2000
#Html.TextBoxFor(r => r.Report.Date, "{0: d MMM yyyy}", new { #class = "form-control" })
#Html.ValidationMessageFor(r => r.Report.Date)
</div>
<div class="form-group">
#Html.LabelFor(m => m.Report.MemberId)
#Html.DropDownListFor(m => m.Report.MemberId, new SelectList(Model.Members, "Id", "Name"), "Select Author", new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.Report.MemberId)
</div>
<div class="form-group">
#Html.LabelFor(m => m.Report.SubjectId)
#Html.DropDownListFor(m => m.Report.SubjectId, new SelectList(Model.Subjects, "Id", "Name"), "Select Subject", new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.Report.SubjectId)
</div>
<div class="form-group">
#Html.LabelFor(m => m.Report.FilePath)
<input type="file" name="file" id="file"/>
</div>
#Html.HiddenFor((m => m.Report.Id))
#Html.AntiForgeryToken()
<button type="submit" class="btn btn-primary">Save</button>
}
Current code doesn't seem to send file data to action.
It is recommended to add the file to your model:
public class Report {
[Required]
[Display(Name = "Report File")]
public HttpPostedFileBase ReportFile { get; set; }
//... The other fields
}
Usually I would append ViewModel, so ReportViewModel instead of Report. This makes it easier to distinguish between view models and business/data models.
And in your Razor:
<div class="form-group">
#Html.LabelFor(m => m.Report.ReportFile)
#Html.TextBoxFor(m => m.ReportFile, new { type = "file" })
<!--You can also use <input type="file" name="ReportFile" id="ReportFile"/>-->
</div>
Note that the name that you use in the LabelFor must match the ID of the control. In your code FilePath and file didn't match.
And finally in the controller:
public ActionResult Save(Report report)
{
//...some code
string filePath = Path.Combine(Server.MapPath("~/App_Data/Uploads"),
Path.GetFileName(report.ReportFile.FileName));
report.ReportFile.SaveAs(filePath);
//...other code
}
I wouldn't use the name of the uploaded file. Instead, I would give it a name according to my project's naming convention. I often use the ID as the name, perhaps with some prefix. Example:
var fileName = "F" + report.Id + ".jpg"; //You can get the extension from the uploaded file
string filePath = Path.Combine(Server.MapPath("~/App_Data/Uploads"), fileName);
Obviously, when you're inserting a new object, you won't have an ID until you insert it into the database, so the code to save the physical file must be placed after the code to insert it into the database. If you follow this logic, you don't need to save the path in the database, because the path can be always calculated from the ID. So you save a column in the database, gain performance in your code as you don't need to handle another string column, and you have a clear and simply file naming convention that is safe without user input risk.
Another way I follow, especially when the type of the file may vary (i.e. you can upload files with different extensions), is using a GUID for the file name. In this case, the file name must be saved in the database, but the GUID can be generated before inserting the object into the database. Example:
string ext = report.ReportFile.FileName.Substring(
report.ReportFile.FileName.LastIndexOf('.')).ToLower();
var fileName = Guid.NewGuid().ToString() + ext;
string filePath = Path.Combine(Server.MapPath("~/App_Data/Uploads"), fileName);
I need a help.
I have a User registration form and I have to map "Customer" with user.
Now I want to validate user "customer" which is came from another source and I put the "customer" in Select list "customer" are more then 2000 that's why I use JQuery Chosen plugin to search in select list
but "customer" Field depend on "roles" that's why on page load "customer" field is hidden by default when I change the role "customer" field(chosen select list) display and when i am Selecting customer its not firing remote validation.
I tried to make it visible on "inspect element" and I change the display:none to display:bock and try to change value from chosen its not working when i change the orignal select list value from clicking on select list then its working fine i mean its firing my remote validator method here is full code example what i am doing.
please help i want to validate on when chosen select list value change.
This is RegisterViewModel
public class RegisterViewModel
{
[Required]
[Display(Name = "Role")]
public string Role { get; set; }
//for edit view model additionalFields which will only require for edit mode
//[System.Web.Mvc.Remote("DoesCustomerCodeExist", "Account", AdditionalFields = "OldCustomerCode")]
[Required(AllowEmptyStrings = false, ErrorMessage = "Customer Code is required.")]
[Display(Name = "Customer Code", Description = "A customer code come from our oracle system.")]
[System.Web.Mvc.Remote("DoesCustomerCodeExist", "Account")]
[Range(0, int.MaxValue, ErrorMessage = "Please enter valid Customer Code in number only.")]
public string CustomerCode { get; set; }
}
Here is my view cshtml in this file also have js code to display customers chosen Select list when role changed.
//select Role
<div class="form-group">
#Html.LabelFor(m => m.Role, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.DropDownListFor(x => x.Role, ViewBag.Roles as SelectList,"", new { #class = "form-control chosen-select", data_placeholder = "Select a Role" })
#Html.ValidationMessageFor(m => m.Role, "", new { #class = "text-danger" })
</div>
</div>
//Customer Code
<div class="form-group condition-div user hidden ">
//this hidden field is only for edit mode
//#Html.Hidden("OldCustomerCode", Model.CustomerCode)
#Html.LabelFor(m => m.CustomerCode, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.DropDownListFor(x => x.CustomerCode, (SelectList)ViewBag.Customers, "", new { #class = "form-control chosen-customers", data_placeholder = "Select Customer" })
#Html.ValidationMessageFor(m => m.CustomerCode, "", new { #class = "text-danger" })
</div>
</div>
#section Styles{
#Styles.Render("~/Content/chosen")
}
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
#Scripts.Render("~/bundles/chosen")
<script type="text/javascript">
$('input[type=text]').tooltip(
{
placement: "right",
trigger: "focus"
}
);
$(".chosen-select").chosen({ allow_single_deselect: true});
$('#Role').change(function () {
if (this.value == "") {
$('.condition-div').addClass('hidden'); // hide all the conidional divs
} else if (this.value == "NBP User" || this.value == "NBP Head" ) {
$('.condition-div.admin').addClass('hidden'); /// hide admin conditional divs
$('.condition-div.user').removeClass('hidden'); // show user role conditioanl div
//configure selectlist to Chosen select and if i remove this line and show orignal select list its working fine mean remote validating on change but if i use this is not working on change.
$(".chosen-customers").chosen({ allow_single_deselect: true, search_contains: true });
$.validator.setDefaults({ ignore: ":hidden:not(.chosen-customers)" });
} else if (this.value == "ICIL User" || this.value == "ICIL Head" || this.value == "FIO User" ) {
$('.condition-div.user').addClass('hidden'); /// hide user role conditional divs
$('.condition-div.admin').removeClass('hidden'); // show admin role conditional divs
$(".chosen-branch").chosen({ allow_single_deselect: true });
$.validator.setDefaults();
}
});
</script>
}
Controller Action to validate Customer Code
public ActionResult DoesCustomerCodeExist(string CustomerCode, string OldCustomerCode)
{
//the oldCustomerCode will come null in this case cause its register view and in edit view OldCustomerCode will be use
if (CustomerCode == OldCustomerCode)
return Json(true, JsonRequestBehavior.AllowGet);
if (DbContext.Users.Any(x => x.CustomerCode == CustomerCode))
return Json("Customer code already exists in application. Please verify user details.", JsonRequestBehavior.AllowGet);
if (DbOracle.IsCustomerCodeExist(CustomerCode))
return Json(true, JsonRequestBehavior.AllowGet);
else
return Json("The customer code does not exist in database.", JsonRequestBehavior.AllowGet);
}
All code working fine if i did not use jquery chosen plugin.
In short issue is when I use chosen plugin for select list remote validation is stop validating values.
I can share images if u guys need now I have a limited account so i cant upload snaps shots....
Please help me.
you should have to put some JQuery on client side to track the "CustomerCode" field when change of customer field jsut call "focusout()" event of "CustomerCode" e.g:
$('#CustomerCode').change(function () {
$(this).focusout();
});
I have one form and one uploader (I use PLUploader) and want user fill textboxs and select image in PLUploader and when click on submit button,
I pass image and textboxs value to one action, I write this code, but always I get null in textboxs value but get image in action.
I think this problem related to call the one action with form and PLuploader.
public ActionResult Insert(News news, HttpPostedFileBase file)
{
// I get null in new but get file in HttpPostedFileBase
int result = 0;
HttpPostedFileBase FileData = Request.Files[0];
string fileName = null;
fileName = Path.GetFileName(FileData.FileName);
if (ModelState.IsValid)
{
//do some thing
}
else
{
return View(news);
}
}
#using (Html.BeginForm("Insert", "News", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<div class="col-xs-12">
#Html.LabelFor(model => model.NewsTitle)
#Html.TextBoxFor(model => model.NewsTitle, new { #class = "form-control",#name="title" })
#Html.ValidationMessageFor(model => model.NewsTitle)
</div>
<div class="col-xs-12">
<div id="uploader" class="img-plc">
<p>You browser doesn't have Flash, Silverlight, Gears, BrowserPlus or HTML5 support.</p>
</div>
<ul id="gallery"></ul>
</div>
<div class="col-xs-12">
#Html.LabelFor(model => model.NewsText, new { #class = "text-right" })
#Html.ValidationMessageFor(model => model.NewsText)
#Html.TextAreaFor(model => model.NewsText, new { #rows = "10", #cols = "80", #class = "text-editor", #name = "title" })
</div>
<button type="submit">Submit</button>
}
var uploader = $("#uploader").pluploadQueue({
// General settings
runtimes: 'html5,gears,flash,silverlight,browserplus,html4',
url: '#Url.Action("Insert", "News")',
max_file_size: '10mb',
chunk_size: '1mb',
unique_names: true,
multi_selection: false,
multiple_queues: false,
// Specify what files to browse for
filters: [
{ title: "Image files", extensions: "jpg,png" }
],
// Flash settings
flash_swf_url: '/Scripts/Moxie.swf',
// Silverlight settings
silverlight_xap_url: '/Scripts/Moxie.xap'
})
$('form').submit(function (e) {
var uploader = $('#uploader').pluploadQueue();
// Files in queue upload them first
if (uploader.files.length > 0) {
// When all files are uploaded submit form
uploader.bind('StateChanged', function () {
if (uploader.files.length === (uploader.total.uploaded + uploader.total.failed)) {
$('form')[0].submit();
}
});
uploader.start();
} else {
alert('You must queue at least one file.');
}
return false;
});
How can I fix this? I want to get news and file in this action.
Create a ViewModel to contain both properties
public class NewsViewModel {
public News News { get; set; }
public HttpPostedFileBase File { get; set; }
}
public ActionResult Insert(NewsViewModel model) {
/* ... */
}
When you create the view pass the ViewModel into the view. Make sure you use the right name for the input field to make it bind correctly:
#Html.TextBoxFor(model => model.File, new { type = "file" })
I would assume you might have to tell your script what name the file input shoul have.
here's my question.
I have a ViewModel with some string property. I have a client validation for that property and what's more this client validation has two messages depending on some other boolean property. But also I have a Html.ValidationSummary on my View and here I have the issue: This ValidationSummary shows always only one message (that I have in ErrorMessage in my Required attribute). And as a result I've faced the problem when my validation message has one string, but ValidationSummary shows another.
My ViewModel:
[Required(ErrorMessage = "* Unique Facility Member ID Required")]
[Remote("ValidateFacilityMemberID", "Members", AdditionalFields = "MemberID,FacilityID")]
public string MemberFacilityEnrollmentsFacilityMemberID { get; set; }
My Controller where I try to change the ModelState error (it works):
private void ChangeModelStateErrorForKeyfobProperty()
{
string facilityMemberIdString = "Unique Facility Member ID";
string keyfobRequired = "* Keyfob Required";
string memberFacilityEnrollmentsFacilityMemberID = "MemberFacilityEnrollmentsFacilityMemberID";
if (ViewBag.CurrentFacility.IsAF)
{
List<string> errors = ModelState.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage).ToList();
foreach (var error in errors)
{
if (error.Contains(facilityMemberIdString))
{
ModelState.Remove(memberFacilityEnrollmentsFacilityMemberID);
ModelState.AddModelError(memberFacilityEnrollmentsFacilityMemberID, keyfobRequired);
}
break;
}
}
}
My View:
#model EditMemberViewData
#{
ViewBag.Title = "Edit " + Model.FullName();
var CurrentFacility = ViewBag.CurrentFacility as Facility;
var FacilityMemberIDLabel = (ViewBag.CurrentFacility.IsAF) ? "Keyfob #" : "Unique Facility Member ID #";
var FacilityMemberIDValidationMessage = (ViewBag.CurrentFacility.IsAF) ? "* Keyfob Required" : "* Unique Facility Member ID Required";
}
#using (Html.BeginForm("Edit", "Members", new { id = CurrentFacility.FacilityIDNumber, memid = Model.MemberId }))
{
#Html.ValidationSummary();
<td><label for="FacilityMemberID">#FacilityMemberIDLabel</label> </td>
<td>
#Html.TextBoxFor(x => x.MemberFacilityEnrollmentsFacilityMemberID, new {style = "margin-left: 9px;"})
<br/>
#Html.ValidationMessage("MemberFacilityEnrollmentsFacilityMemberID", FacilityMemberIDValidationMessage)
<span id="MemberFacilityIdValidWarning" style="display: none; color: red;"></span>
</td>
</tr>
So could you please help me? How can I teach ValidationSummary to show different messages depending on some situations?
I tried to put the code as MVC c# form from console.
I want to show key with account, meter numbers, but I have no idea what I'm wrong.
What I want to try is that prints out all info in Details page.
Here is controller
public ActionResult Index()
{
return View(db.fedex.ToList());
}
private static RateRequest CreateRateRequest()
{
FedexModel fedex = new FedexModel();
// Build a RateRequest
RateRequest request = new RateRequest();
//
request.WebAuthenticationDetail = new WebAuthenticationDetail();
request.WebAuthenticationDetail.UserCredential = new WebAuthenticationCredential();
request.WebAuthenticationDetail.UserCredential.Key = fedex.AccessKey; // Replace "XXX" with the Key
request.WebAuthenticationDetail.UserCredential.Password = fedex.Password; // Replace "XXX" with the Password
//
request.ClientDetail = new ClientDetail();
request.ClientDetail.AccountNumber = fedex.AccountNumber; // Replace "XXX" with the client's account number
request.ClientDetail.MeterNumber = fedex.MeterNumber; // Replace "XXX" with the client's meter number
//
request.TransactionDetail = new TransactionDetail();
request.TransactionDetail.CustomerTransactionId = "***Rate v14 Request using VC#***"; // This is a reference field for the customer. Any value can be used and will be provided in the response.
//
request.Version = new VersionId();
//
request.ReturnTransitAndCommit = true;
request.ReturnTransitAndCommitSpecified = true;
//
//SetShipmentDetails(request);
//
return request;
}
//
// GET: /Fedex/Details/5
public ActionResult Details(int id = 0)
{
var request = CreateRateRequest();
return View(request);
}
If I click the key then it goes to next in Details page.
Details View
#model FedExShipping.Models.FedexModel
#using FedExShipping.WebReference;
#using FedExShipping.Controllers;
<h2>Details</h2>
<fieldset>
<legend>FedexModel</legend>
<div>
#Html.DisplayFor(model => model.AccessKey)
</div>
<div>
#Html.DisplayFor(model => model.Password)
</div>
<div>
#Html.DisplayFor(model => model.AccountNumber)
</div>
<div>
#Html.DisplayFor(model => model.MeterNumber)
</div>
What do I need to change for correct output?
Your action is returning View(request) so your model is being set to RateRequest. Which means your view for this action is interacting with RateRequest, not FedexModel. You can interact with anything that's set on your instance of RateRequest only. If you need something else, you need to change the model for the view and pass something other than an instance of RateRequest to it.