How should I validate different sections of MVC3 page - c#

I am trying to understand how I should validate on the client sections of my MVC3 page independently and have come up with a simplyfied version of what I am trying to achieve.
If I use one form:
Pros: When I submit back to the "PostData" controller method I receive all data contained within the form. In this case both values "name" and "description", which means that I can instantiate "PersonHobbyModel" and assign the data I have received. I can either store in the database or I can return the same view.
Cons: I cant validate independently. So if "name" isn't completed and I complete "description" I can still submit the page. (This is a simplyfied version of what I am trying to do and I would have more fields than just "name" and "description")
With two forms:
Pros: I can validate independently.
Cons: The controller method only receives the subitted forms data which, in this case either "Persons name" or "Hobby description" which means that I can't recreate a full instance of "PersonHobbyModel".
This is the model:
public class Person {
[Display(Name = "Person name:")]
[Required(ErrorMessage = "Person name required.")]
public string Name { get; set; }
}
public class Hobby {
[Display(Name = "Hobby description:")]
[Required(ErrorMessage = "Hobby description required.")]
public string Description { get; set; }
}
public class PersonHobbyModel {
public PersonHobbyModel() {
this.Person = new Person();
this.Hobby = new Hobby();
}
public Person Person { get; set; }
public Hobby Hobby { get; set; }
}
This is the controller:
public class PersonHobbyController : Controller
{
//
// GET: /PersonHobby/
public ActionResult Index()
{
var model = new PersonHobbyModel();
return View(model);
}
public ActionResult PostData(FormCollection data) {
var model = new PersonHobbyModel();
TryUpdateModel(model.Person, "Person");
TryUpdateModel(model.Hobby,"Hobby");
return View("Index", model);
}
}
This is the view:
#model MultipleFORMStest.PersonHobbyModel
#{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
#using (Html.BeginForm("PostData", "PersonHobby")) {
<div>
#Html.LabelFor(model => model.Person.Name)
#Html.TextBoxFor(model => model.Person.Name)
#Html.ValidationMessageFor(model => model.Person.Name)
<input type="submit" value="Submit person" />
</div>
}
#using (Html.BeginForm("PostData", "PersonHobby")) {
<div>
#Html.LabelFor(model => model.Hobby.Description)
#Html.TextBoxFor(model => model.Hobby.Description)
#Html.ValidationMessageFor(model => model.Hobby.Description)
<input type="submit" value="Submit hobby" />
</div>
}
UPDATE 1
I didnt mention, as I wanted to keep the question as simple as possible, but for one of the sections I am using "jquery ui dialog". I initially used a DIV to define the dialog, which I had inside my main form. This would of caused one problem as I wouldn't have been able to validate on the client the "JQuery dialog form" independently from the rest of the form.
Saying this jquery did removed the "div jquery ui dialog" from the main form which made me include the dialog in it's own form. For this reason I have ended up with two forms. The advantage is that I can now independently validate the "jquery dialog ui form".
But I am confused as to how should I handle on the server data submited from various forms on the client as there is a chance that the user has JS disabled. If I submit from one form I can't access the data in other forms.
UPDATE 2
Thanks for the replies. I believe I do need two forms and two entities as I want to validate them independently on the client, (apart from being kind of forced to by "Jquery UI Dialog"). For instance if I have, instead of one hobby I have a list of hobbies, which I could posible display in a grid in the same view. So I could not fill in the person name, but continue to add hobbies to the grid, If I do not complete the hobby description I'd get a validation error. (Sorry as I should of included both of my updates in the initial question but for the purpose of clarity I wanted to keep it as simple as posible)

From my perspective, you have a single view model that corresponds to two entity models. In your place I would use a single form and validate the view model and not really think about it as two (dependent) entities. Receive back the view model in your action, instead of a generic form collection, and use model-based validation via data annotation attributes. Once you have a valid, posted model you can then translate that into the appropriate entities and save it to the database.
Model
public class PersonHobbyViewModel {
[Display(Name = "Person name:")]
[Required(ErrorMessage = "Person name required.")]
public string Name { get; set; }
[Display(Name = "Hobby description:")]
[Required(ErrorMessage = "Hobby description required.")]
public string Description { get; set; }
}
Controller
public class PersonHobbyController : Controller
{
//
// GET: /PersonHobby/
[HttpGet] // mark as accepting only GET
public ActionResult Create() // Index should probably provide some summary of people and hobbies
{
var model = new PersonHobbyViewModel();
return View(model);
}
[HttpPost] // mark as accepting only POST
public ActionResult Create(PersonHobbyViewModel model) {
if (ModelState.IsValid) {
var person = new Person { Name = model.Name };
var hobby = new Hobby { Description = model.Description };
person.Hobbies = new List<Hobby> { hobby };
db.Persons.Add( person );
db.SaveChanges();
}
return RedirectToAction( "details", new { id = person.Id } ); // view the newly created entity
}
}
View
#model MultipleFORMStest.PersonHobbyViewModel
#{
ViewBag.Title = "Create";
}
<h2>
Create</h2>
#using (Html.BeginForm("Create", "PersonHobby")) {
<div>
#Html.LabelFor(model => model.Person.Name)
#Html.TextBoxFor(model => model.Person.Name)
#Html.ValidationMessageFor(model => model.Person.Name)
<input type="submit" value="Submit person" />
</div>
<div>
#Html.LabelFor(model => model.Hobby.Description)
#Html.TextBoxFor(model => model.Hobby.Description)
#Html.ValidationMessageFor(model => model.Hobby.Description)
<input type="submit" value="Submit hobby" />
</div>
}

I think your ViewModel should be only only specific to that view you are representing. In this case, i would use a ViewModel like this
public class AddPersonHobbyViewModel
{
[Required]
[Display (Name="Person Name")]
public string PersonName { set;get;}
[Required]
[Display (Name="Hobby Description")]
public string HobbyDescription { set;get;}
}
And in my PostData ActionMethod, I will check for Model Validation
[HttpPost]
public ActionResult PostData(AddPersonHobbyViewModel objVM)
{
if(ModelState.IsValid)
{
// Everything is fine. Lets save and redirect to another get View( for PRG pattern)
}
return View(objVm);
}
And you use only one Form in your View which is strongly typed to AddPersonHobbyViewModel
#model AddPersonHobbyViewModel
#using (Html.BeginForm("PostData","Person"))
{
#Html.TextBoxFor(m=>m.PersonName)
#Html.ValidationMessageFor(m => m.PersonName)
#Html.TextBoxFor(m=>m.HobbyDescription )
#Html.ValidationMessageFor(m => m.HobbyDescription )
<input type="submit" value="Save" />
}

Related

Remember search params

I'm new in mvc and I try to create a simple page with table and ajax search.
For example, I have a search model, which pass parameters from form to controller.
Model:
public class OrderSearchViewModel
{
[Display(ResourceType = typeof(Lang), Name = "OrderID")]
public int? OrderID { get; set; }
[Display(ResourceType = typeof(Lang), Name = "DeliveryType")]
public int? DeliveryTypeID { get; set; }
[Display(ResourceType = typeof(Lang), Name = "Partner")]
public string CustomerName { get; set; }
public SelectList DeliveryTypes { get; set; }
}
In controller I have an action witch return View with form:
public ActionResult Index()
{
var ordersSearchModel = // default init;
return View(model);
}
In my Index.cshtml I have a form
#model Models.Order.OrderSearchViewModel
<div class="row">
#using (Ajax.BeginForm("Orders", "Order", new AjaxOptions {UpdateTargetId = "ordersList"}, new {#id = "searchForm", #class = "form-horizontal"}))
{
// Editors templates for each params
}
</div>
<div id="ordersList" class="row">
</div>
}
In my Controller I have a method, witch take search model and return a partial view
[HttpPost]
public async Task<ActionResult> Orders(OrderSearchViewModel model, int page = 1, int pageSize = 50)
{
var models = // connect to db and get data filtered by model params
return PartilaView("_View", models);
}
In result partial I have a table with order num and link to edit view.
In edit view I have a link back to search:
#Html.ActionLink("Back", "Index", "Order", new { #class = "btn btn-default" })
And by click this link I get the Index view in default (without search parameters) and user must fill it once again.
What will be the best practice to remember user search parameters?
Thanks for any advice.
As devqon said, the preferred approach is to use query parameters, but it does sound like your usage might make this a bit difficult to manage
TempData/SessionData may solve your issue, but using this approach will cause problems if the user decides to use your application in multiple tabs/windows (they will all share the same search params)
Something that might be worth looking into is SessionStorage.(Link below)
This type of storage persists as long as the browser stays open
And importantly
Opening a page in a new tab or window will cause a new session to be initiated
So your pages shouldn't share state.
More details here:
https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage

MVC Model child object null on HTTP post

Hope someone can help me. I am new to MVC, coming from a winforms/console/vb6background.
Apologies if this has already been answered, I am stuggling to understand how I can resolve the below issue.
I have a view model :
public class testvm
{
public int id { get; set; }
public DateTime date { get; set; }
public student studentID { get; set; }
public testvm() { }
public testvm (student s)
{
studentID = s;
}
}
I am pre-populating the student child object of this ViewModel before it is passed to the view.
Student Model :
public class student
{
[Key]
public int ID { get; set; }
public string Name { get; set; }
}
The problem I have is when the model is returned to the create HTTP post method the student child object is blank.
The controller code :
// GET: testvms/Create
public ActionResult Create(int sId)
{
student a = db.students.Find(sId);
testvm b = new testvm(a);
return View(b);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "id,date,student")] testvm testvm)
{
if (ModelState.IsValid)
{
db.testvws.Add(testvm);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(testvm);
}
View code:
#model WebApplication2.Models.testvm
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>testvm</h4>
<hr />
#Html.HiddenFor(model => model.studentID.ID)
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.date, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.date, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.date, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
The model object on the view is populated with the student information. When this is passed back to Create POST controller the student child object is null!
Can somebody please advise where I am going wrong or of the correct way to achieve this?
My application will have many forms that will all need to be pre-populated with student information. Each student will have many forms that will need to be filled out for them.
Many thanks in advance,
Rob
For every property in domain model (in your case testvm) you must have an EditorFor or Input element (like TextBoxFor or so) on your view(or HiddenFor for ID or other non user ui data).It may be a pain binding nested models in MVC as the DefaultModelBinder may not be able to bind whole object.However it would be safer approach to expose only the required properties on view like
#Html.HiddenFor(model => model.studentID.ID)
#Html.HiddenFor(model => model.studentID.Name)
and later on Controller Side
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(testvm testvm)
{
var originalobj=db.get //get fresh copy from data store
originalobj.Name=testvm.Name;
// ...other properties
//perform required operations on originalobj
}
you may use AutoMapper for this Purpose as
Mapper.CreateMap<testvm,testvm>();
originalobj=Mapper.Map<testvm,testvm>(testvm,originalobj);
you may find more information about Automapper on :
https://github.com/AutoMapper/AutoMapper/wiki/Getting-started
Your property name is called studentId (even though standard C# property naming convention dictates that it should have been called StudentId):
public student studentID { get; set; }
But in your Bind attribute you seem to have specified some student property which doesn't really exist on your view model:
[Bind(Include = "id,date,student")]
So you probably want to get rid of this Bind attribute from your controller action:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(testvm testvm)
{
...
}
Also note that you only have a hidden field for the student id inside your form:
#Html.HiddenFor(model => model.studentID.ID)
You don't have a corresponding hidden field for the student name property, so it will never be posted back to your controller action.
Your attribute [Bind(Include = "id,date,student")] should include the names of the properties that you want to be set, student isn't in your model, but studentID is, they have to match.
You don't have to explicitly specify all of the field names that you want to be bound to your model, by default they will be bound anyway unless you tell the binder NOT to bind it by using [Bind(Exclude = "id,date,student")]. Therefore as it currently stands, I'd recommend removing your Include attribute to ease maintenance unless there is an important reason for using it and simply ensure that the models that you bind to only include the values you need.
Secondly, you have to make sure that any values that you are posting back from a form in your view have the same parameter names and are structured the same as the ones that you want to be bound to the request model.
This:
#Html.HiddenFor(model => model.studentID.ID)
Is not the same as:
#Html.HiddenFor(model => model.studentID)

model returned to view has no effect on field

My Model;
namespace GeneralUtility.Models
{
public class MyModel
{
public int BirthDateYear { get; set; }
public String Details { get; set; }
}
}
My Controller
namespace GeneralUtility.Controllers
{
public class WorkspaceController : Controller
{
public ActionResult MyHelper(MyModel model)
{
if(model.someCondition)
model.Details= "TEST";
else
model.Details= "Some other TEST";
return View(model);
}
}
}
My View
<div data-role="fieldcontain">
#using (Html.BeginForm("MyHelper", "WorkSpace", FormMethod.Post, new { id = "frmMyForm" }))
{
...
<div data-role="fieldcontain">
#Html.EditorFor(x => x.Details )
</div>
...
}
</div>
After I submit my form, I can see the model and any changes I make in MyHelper action method. However, when I make changes and return the model from the controller (I can see the changes in the model while in debug). I get the same value as before in #Html.EditorFor(x => x.Details ) field. What can I do to get the changed Details value of the model to show?
That's because by default ASP.NET MVC returns the model that was passed into a method (using what is stored in the ModelState, if it exists). If it doesn't exist, it uses what you pass to the View.
In order to prevent this, you need to call ModelState.Clear() before returning your view.
Check this blog post for a more detailed explanation.

How to show validation message in mvc4 razor

I am newbie in MVC Razor and I want to implement validation message on textboxes. Here I'm creating some textbox dynamically as follows:
View Code:
foreach (var items in (IEnumerable<System.Data.DataRow>)Model.UsersOfList)
{
#Html.TextBoxFor(m => m.LoginNameOfLoginInfoTab, new { #class = "textBox_LoginInfoAndPermission", #value = (Model.LoginNameOfLoginInfoTab = items["UserName"].ToString()), #id = ("txtUserLoginName" + Model.UsernameOfLoginInfoTab.Trim()) })
#Html.ValidationMessageFor(m => m.LoginNameOfLoginInfoTab, null, new { #class = "ErrorMessage" })
#Html.TextBoxFor(m => m.UsernameOfLoginInfoTab, new { #class = "textBox_LoginInfoAndPermission", #value = (Model.UsernameOfLoginInfoTab = items["FirstName"].ToString()), #id = ("txtUserName" + Model.UsernameOfLoginInfoTab.Trim()) })
#Html.ValidationMessageFor(m => m.UsernameOfLoginInfoTab, null, new { #class = "ErrorMessage" })
}
in the module I've written code for validation as follows:
[Required (ErrorMessage="*")]
public string UsernameOfLoginInfoTab
{
get;
set;
}
[Required(ErrorMessage = "*")]
public string LoginNameOfLoginInfoTab
{
get;
set;
}
Now when all textboxes has been created and when one validation message is displaying for first loop iteration textbox then it will automatically displaying in front of another textbox too which is created on second loop iteration.
Please tell me whats going wrong.
The problem is because the expression you're using in TextBoxFor and ValidationMessageFor, which is used by MVC to create a string name for the field and look up validation messages from ModelState, is always the same throughout the iteration of the loop.
Your approach here seems a bit flawed, so my answer is more comprehensive.
1) Make view models that structurally represent the info you are trying to display.
Fix your view models:
public class UserInfoViewModel
{
[Required (ErrorMessage="*")]
public string UserName { get; set; }
[Required(ErrorMessage = "*")]
public string LoginName { get; set; }
}
// I don't know if you actually need this or not, but your existing model may contain additional properties relevant to the view that I don't know about, so I'll keep it.
public class ListOfUsersViewModel
{
public IList<UserInfoViewModel> UsersOfList { get; set; }
}
Fix your action (I'm making this up here to illustrate a point):
public ActionResult ListOfUsers()
{
var users = GetUserDataRows(); // gets your collection of DataRows
var model = new ListOfUsersViewModel
{
UsersOfList = users.Select(row = new UserViewModel { UserName = row["FirstName"], LoginName = row["UserName"] }).ToList()
};
return View(model);
}
2) Now you can iterate through users in your view and create proper fields with validation messages.
Let's call this view ListOfUsers.cshtml. Include whatever other things you need in your view, but use a for loop instead.
#using(Html.BeginForm("ListOfUsers"))
{
<ul>
#for (var i = 0; i < Model.UsersOfList.Count; i++)
{
<li>
#Html.TextBoxFor(m.UsersOfList[i].LoginName, new {#class="textbox_LoginInfoAndPermission"})
#Html.ValidationMessageFor(m => m.UsersOfList[i].LoginName)
#Html.TextBoxFor(m.UsersOfList[i].UserName, new {#class="textbox_LoginInfoAndPermission"})
#Html.ValidationMessageFor(m => m.UsersOfList[i].UserName)
</li>
}
</ul>
<button type="submit">Submit changes</button>
}
This will result in HTML like this for each item (0 in the name and id will be the index of the user in the collection):
<li>
<input type="text" id="UsersOfList_0_LoginName" name="UsersOfList[0].LoginName" value="..." />
<span class="field-validation-valid" data-valmsg-for="UsersOfList_0_LoginName" ... ></span>
<input type="text" id="UsersOfList_0_UserName" name="UsersOfList[0].UserName" value="..." />
<span class="field-validation-valid" data-valmsg-for="UsersOfList_0_UserName" ... ></span>
</li>
3) Create an action to receive submitted changes. This action will automatically bind the submitted values to the model argument, and do validation for you. All you need to do is check ModelState.IsValid.
[HttpPost, ActionName("ListOfUsers")]
public ActionResult ListOfUsersPost(ListOfUsersViewModel model)
{
// at this point, model will be instantiated, complete with UsersOfList with values submitted by the user
if (ModelState.IsValid) // check to see if any users are missing required fields. if not...
{
// save the submitted changes, then redirect to a success page or whatever, like I do below
return RedirectToAction("UsersUpdated");
}
// if ModelState.IsValid is false, a required field or other validation failed. Just return the model and reuse the ListOfUsers view. Doing this will keep the values the user submitted, but also include the validation error messages so they can correct their errors and try submitting again
return View("ListOfUsers", model);
}

mvc error on http post Object reference not set to an instance of an object

I am having trouble with the following error message:
Object reference not set to an instance of an object.
break on lines:
Line 16: </div>
Line 17: <div class="editor-field">
Line 18: #Html.DropDownList("KPI.CSF.FYID", Model.Financial_Years)
Line 19: #Html.ValidationMessageFor(model => model.KPI.CSF.FYID)
Line 20: </div>
This occurs during form post in my mvc project.
I am passing a viewmodel of the following when I load the create form:
public class KPICreateFormViewModel
{
//Properties
public KPI KPI { get; set; }
public SelectList Financial_Years { get; private set; }
FYRepository fyrepo = new FYRepository();
public KPICreateFormViewModel(KPI kpi)
{
KPI = kpi;
Financial_Years = new SelectList(fyrepo.GetFys(), "ID", "Financial_Year");
}
}
I use the financial years for a dropdown which then initiates some ajax to cascade some other dropdowns which will populate ID numbers in my post.
I do not want my financial years dropdown to post anything back during the http post method of my controller.. so I figured if I just pass back the following I would be ok:
[HttpPost]
public ActionResult Create(KPI kpi)
{
try
{
kpirepository.Add(kpi);
kpirepository.Save();
return RedirectToAction("Details", new { id = kpi.ID });
}
catch
{
return View();
}
}
I assume this is all happening because of my view being based on the viewmodel and then on post I am not handing this back? After hours of messing with the cascade code to get it working my head is a bit fried to try and tackle this issue.. help!
The relevant part for my view showing the financial year dropdown:
#model ES_Business_Intelligence.ViewModels.Admin.KPICreateFormViewModel
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>KPI</legend>
<div class="editor-label">
#Html.LabelFor(model => model.KPI.CSF.FYID)
</div>
<div class="editor-field">
#Html.DropDownList("KPI.CSF.FYID", Model.Financial_Years)
#Html.ValidationMessageFor(model => model.KPI.CSF.FYID)
</div>
After confirmation of the problem from webdeveloper I did some more digging and found I can tell certain elements of my viewmodel to not bind when returning data from my form submit. I added the following code to my viewmodel:
[Bind(Exclude = "Financial_Years")]
public class KPICreateFormViewModel
{
//Properties
public KPI KPI { get; set; }
public SelectList Financial_Years { get; private set; }
FYRepository fyrepo = new FYRepository();
public KPICreateFormViewModel(KPI kpi)
{
KPI = kpi;
Financial_Years = new SelectList(fyrepo.GetFys(), "ID", "Financial_Year");
}
}
The magic is all in the first line:
[Bind(Exclude = "Financial_Years")]
You are absolutely right, you use strongly typed view and after post you path null to view, thats why exception is thrown when you trying to access properties (model => model.KPI.CSF.FYID). As solution you can add if block and ignore html markup, if model is null.

Categories

Resources