How can I safely access key groups in the FormCollection? - asp.net-mvc-3

I have a table where each tr is grouped by having their input elements name set to a value that is unique for each row.
For example,
<td>
<input data-field="opps" type="text" value="somevalue" name="#item.Code" />
</td>
<td>
<input data-field="asc" type="text" value="somevalue2" name="#item.Code" />
</td>
On POST
[HttpPost]
public ActionResult Update(FormCollection collection)
{
try
{
//doin work on collection...order assumed static
return RedirectToAction("Index");
}
catch
{
return View();
}
}
my System.Web.MVC.FormCollection is grouped in the same order I define the <td>. I don't like to assume order, but without access to my data-field, I'm not sure what else I can do (maybe I could append the data-field value as a prefix to the name and put it all together with a custom collection and Regex..but that seems nutty).
Is there a way to access the data-field? This way I'm not fragile to re-ordering or adding new columns in the View.

Let's say you have a class (model) defined like this:
public class MyModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
In you controller, you might have an action called Create, like so:
[HttpGet]
public ViewResult Create()
{
MyModel sampleModel = new MyModel();
return View(sampleModel);
}
[HttpPost]
public ActionResult Create(MyModel sampleModel)
{
if (ModelState.IsValid)
{
TempData["Error"] = "There were errors. Please correct the problem and submit again";
return View(sampleModel);
}
// At this point everything is fine and you can access data in your sampleModel
if (sampleModel.Age >= 16)
{
return RedirectToAction("LegalAccess");
}
else
{
TempData["Error"] = "You must be 16 or over to access this site";
return RedirectToAction("AgeRestriction");
}
}
When you create a strongly typed view that uses MyModel as model you might define it something like this:
#model MyModel
#{
Layout = "~/Shared/_Layout.cshtml";
}
#using (Html.BeginForm())
{
#Html.LabelFor(m => m.FirstName)
#Html.TextBoxFor(m => m.FirstName)
<br />
#Html.LabelFor(m => m.LastName)
#Html.TextBoxFor(m => m.LastName)
<br />
#Html.LabelFor(m => m.LastName)
#Html.TextBoxFor(m => m.LastName)
<input type="submit" value="Submit" />
}
When you submit this form, the model binder will in the background copy data from this form using Request.Form into an object of type MyModel which it creates in the background. This new object is passed to an action that handles HTTP POST method. With this you get strongly typed object and you don't have to worry about the order of items in FormCollection.
I hope I helped answer your question.
BTW. I wrote this without Visual Studio, so I hope there are not errors. :-)

Related

MVC4: ViewModel (with radiobuttonlist) is empty after HttpPost

I'm having a hard time getting the values from a small multiple choice questionnaire posted to the Controller in my MVC4 app:
The model looks like this:
public class Evaluation
{
public int Id { get; set; }
public IEnumerable<MultipleChoiceQuestion> Question { get; set; }
public Remark Rem { get; set; }
}
public class MultipleChoiceQuestion
{
public int Id { get; set; }
public string Question { get; set; }
public MultipleChoiceAnswer Answer { get; set; }
}
public enum MultipleChoiceAnswer
{
DISAGREE,
NEUTRAL,
AGREE,
NA,
}
This is the View (leaving out some markup):
#model Models.Evaluation
#using (Html.BeginForm("EvaluationB", "Evaluation", FormMethod.Post))
{
#foreach (var item in Model.Question)
{
#Html.DisplayFor(model => item.Question)
#Html.EditorFor(model => item.Question, "Enum_RadioButtonList", new { Id = item.Id })
}
#Html.Label("Remark")
#Html.TextAreaFor(model => model.Rem)
<input type="submit" value="Next" />
}
The "Enum_RadioButtonList" is a View a grabbed from here: https://gist.github.com/973482. It seems like the best way to show enum values in a radiobuttonlist (tho their should be an easier way in MVC 4)
The Controller looks like this:
public ActionResult EvaluationA()
{
Models.Evaluation evm = new Models.Evaluation();
evm.Question = db.MultipleChoiceQuestions.ToList(); //feeding the View some predefined questions
return View(evm);
}
public ActionResult EvaluationB(Models.Evaluation ev)
{
if (ModelState.IsValid)
{
// TODO: save model
return View("EvaluationB", evm);
}
return View("EvaluationA", ev);
}
The questions are loaded fine in the View, but for some reason, the model posted to the Controller remains empty after an HttpPost, and i don't understand why.
I did not see the form have mapping for the Id like below:
#Html.HiddenFor(model => model.Id)
Each of the inner collection of question should also have Id so that it is posted along with the form. So inside the foreach loop in your form for each question you can have:
#Html.HiddenFor(model => item.Question.Id)
Also the model has a collection of type MultipleChoiceQuestion. For model binding to the collection the name of the collection elements should have name attribute with ordered numbers as explained in this post http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
Ive done some more research stumbled upon this post:
http://dotnetslackers.com/articles/aspnet/Understanding-ASP-NET-MVC-Model-Binding.aspx
The problem was in the naming of the html fields. Ive added my View like this and now i can read out the values in the Controller correctly:
#for (int i = 0; i < 6; i++)
{
<tr>
<td>
#Html.DisplayFor(m => m.Question[i].Question)
</td>
<td class="mult_question">
#Html.EditorFor(m => m.Question[i].Answer, "Enum_RadioButtonList" )
</td>
</tr>
}
<tr>
<td>
#Html.Label("Remark")
#Html.TextAreaFor(m => m.Remark)

Partial View and ajax

I want to update Partial View via ajax, but it does not work. Look at this model class:
public class LogOnModel
{
[Required]
[Display(Name = "User name")]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
public bool IsLoggedIn { get; set; }
public string ReturnUrl { get; set; }
}
the following view:
#model ITW2012Mobile.ViewModels.LogOnModel
<div id='LogOn' style="background-color: White;">
#using (Ajax.BeginForm("LogOnAjax", "Home", new AjaxOptions { UpdateTargetId = "LogOn", OnSuccess = "logInComplete" }))
{
ITW2012Mobile.ViewModels.LogOnModel m = Model;
#Html.EditorFor(model => model.IsLoggedIn)
#Html.EditorFor(model => model.ReturnUrl)
<div>
#Html.ValidationSummary()
</div>
<div>
#Html.LabelFor(model => model.UserName)
#Html.EditorFor(model => model.UserName)
</div>
<div>
#Html.LabelFor(model => model.Password)
#Html.EditorFor(model => model.Password)
</div>
<div>
<input type="submit" value="Login" />
</div>
}
</div>
and the following controller class:
public ActionResult LogOnAjax(LogOnModel model)
{
if (!User.Identity.IsAuthenticated)
{
if (ModelState.IsValid)
{
if (Membership.ValidateUser(model.UserName, model.Password))
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
model.IsLoggedIn = true;
model.ReturnUrl = Url.Action("Index", "Home");
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
// If we got this far, something failed, redisplay form
return PartialView("PartialViewAjaxLogOn", model);
}
else
{
return PartialView("PartialViewLogOut");
}
}
even when username/password are correct and IsLoggedIn = true and ReturnUrl!=empty view shows empty fields for these variables (but debugger shows values inside). Why and how to make it correctly?
Try clearing the values you are modifying in your action from modelstate or if you use them in html helpers the old values will be used:
ModelState.Remove("IsLoggedIn");
model.IsLoggedIn = true;
ModelState.Remove("ReturnUrl");
model.ReturnUrl = Url.Action("Index", "Home");
Also bear in mind that upon successful authentication and cookie emission you should not display a view (partial in your case). You should redirect so that the authentication cookie is sent by the client on the subsequent request. You should redirect to the return url. But since you are doing this using AJAX you should probably send some indication to the client that the authentication was successful so that you can redirect on the client.

Values don't change after posting back to view?

Edit: I've changed my question and code to clarify my question better
I've got this (strongly typed view) that does use the values provided in the controller for that specific model
I want to add something to a Model from another model, after posting back from my httpPost action nothing happens...
Thanks in advance!
--------------------------------------other code to clarify my question a bit more----
public class Address
{
public int Id { get; set;}
public String Name { get; set;}
}
public class OtherAddress
{
public int Id { get; set;}
public String Name { get; set;}
public String City { get; set;}
}
public class MasterModel
{
public Address Address { get; set;}
public List<OtherAddress> OtherAddressess { get; set;}
}
public ActionResult Create()
{
MasterModel Model = new MasterModel();
Model.Person = new Person();
Model.Address = new Address();
Model.OtherAdressess = new List<OtherAddress>();
DBContext _db = new DBContext();
Model.OtherAdressess = _db.OtherAddressess.Where(a=> a.City == "Amsterdam");
return View(Model);
}
in the view
#model Project.Models.MasterModel
List<SelectListItems> items = new List<SelectListItems>();
foreach(var a in Model.OtherAddressess)
{
SelectListItem item = new SelectListItem();
item.Value = a.Id.toString();
item.Text = a.Street;
}
#using (#Html.BeginForm())
{
<div>
<select name="otheraddress">
foreach(var i in Items)
{
<option value=#i.Value>#i.Text</option>
}
</select>
<input type="submit" name="select" value="Select Address"/>
</div>
<div>
#Html.EditorFor(model => Model.Address.Name)
<div>
<p>
<input type="submit" value="Submit"/>
</p>
}
in post
[HttpPost]
public ActionResult Create(MasterModel Model)
{
String otherAddressSelected = Request.Params["select"];
if(!String.IsNullOrEmpty(otherAddressSelected))
{
int id = int.Parse(Request.Params["otheraddress"]);
DBContext _db = new DBContext();
OtherAddress oa = _db.OtherAddress.Single(oa=> oa.Id == id);
Model.Address.Name = oa.Name;
return View(Model);
}
//other stuff here
}
If you want to change the value of your model in a [HttpPost] controller you have to remove the modelstate for the instance/attribute that you want to change. For example:
[HttpPost]
public ActionResult Index(SomeModel model)
{
ModelState.Remove("Name");
model.Name = "some new name";
return View(model);
}
Got the answer from this example
I would create action method called details that would accept person id as parameter:
public ActionResult Details(int id)
{
// Get person and display
}
Your Create action method is for creating Person type objects, and not displaying their details. So logically what you are doing doesn't seem right to me.
There should be action method to display view for creating a person and equivalent HTTP action method for persisting it into the database.
I would then re-direct to an action method for displaying Person type object information.
return RedirectToAction("Details", new { id = Person.Id });
The input helpers in asp.net mvc will use the post values if they can find any before looking at the model.
In this situation here I think the problem is that you are trying to do more then one thing in the Create POST action. A action (as with any method in the application) should only do one thing. In your case I would do something like this (if I understand the work flow correctly that is):
//Action: SelectAddress
public ActionResult SelectAddress() {
var addresses = _db.OtherAddressess.Where(a=> a.City == "Amsterdam");
return View(new SelectAddressViewModel(addresses));
}
//View SelectAddress
....
<ul>
#foreach(var address in Model.Addresses) {
<li>
<a href="#Url.Action("Create", "Product", new { addressId = address.Id })">
#Model.Name
</a>
</li>
}
</ul>
....
//Action Create
public ActionResult Create(int addressId) {
var address = _db.OtherAddress.Single(oa=> oa.Id == addressId);
var Model = new MasterModel();
Model.Person = new Person();
Model.Address = new Address {
Name = address.Name
}
return View(Model);
}

Html Helper for IEnumerable<T> collection

I have two classes MyClassA and MyClassB:
public class MyClassA
{
public int Age { get; set; }
public string Name { get; set; }
}
public class MyClassB
{
public IEnumerable<MyClassA> Data { get; set; }
}
Now, I want to create custom strongly typed html helper to generate textboxes with names from collection MyClassA, something like this:
#model MyClassB
#Html.MyTextBoxFor(p => p.MapFrom(o => o.Age))
#Html.MyTextBoxFor(p => p.MapFrom(o => o.Name))
... with the output:
<input type="text" name="Age" />
<input type="text" name="Name" />
How can I accomplish this?
PS. I know, I can write sth like this:
#Html.TextBoxFor(p => p.Data.First().Name)
but it feels so wrong and inelegant...
Any ideas?
Let me make sure I understand this correctly... you want to create a textbox for Name and Age for each MyClassA in Data property of MyClassB. If so, then editor templates to the rescue.
Create /Views/Shared/EditorTemplates/MyClassA.cshtml
#model MyClassA
#Html.TextBoxFor(m => m.Name)
#Html.TextBoxFor(m => m.Age)
then in you view:
#model MyClassB
#Html.EditorFor(m => m.Data)

MVC3 Entity Framework 4.1RC how does #Html.DropDownListFor actually work?

OK, I've been over Google and StackOverflow and ASP.net - am I really the only person who doesn't get this?
Rough problem = I have an Employee entity that references an Office entity. I was previously able to create new employees (beat me with a hammer for forgetting how that code worked) but now I just can't create an employee nor edit an existing one.
Now, here's what I learned;
1) Make sure you add the Offices list to the ViewBag at every step
2) That includes a failed POST/edit; call the function to re-populate the ViewBag with the Offices list
3) I think(!!) that you always want to set the Employee.Office, not the Employee.Office.OfficeID; the latter leads to "is part of the object's key information and cannot be modified" errors
So, what I have is;
A controller that has the following method;
private void AddOfficesToViewBag()
{
Dictionary<string, Office> list = new Dictionary<string, Office>();
foreach (Office office in company.GetAllOffices())
list.Add(office.ToString(), office);
SelectList items = new SelectList(list, "Value", "Key");
ViewBag.OfficeList = items;
}
Create pair looking like;
public ActionResult Create()
{
if (company.Offices.Count() < 1)
return RedirectToAction("Create", "Office", (object) "You need to create one or more offices first");
AddOfficesToViewBag();
return View(new Employee());
}
//
// POST: /Employee/Create
[HttpPost]
public ActionResult Create(Employee emp)
{
if (TryUpdateModel<Employee>(emp))
{
company.Employees.Add(emp);
company.SaveChanges();
return RedirectToAction("Index");
}
else
{
AddOfficesToViewBag();
return View(emp);
}
}
and an Edit pair that looks like;
public ActionResult Edit(int id)
{
Employee emp = company.Employees.Single(e => e.EmployeeID == id);
AddOfficesToViewBag();
return View(emp);
}
//
// POST: /Employee/Edit/5
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
Employee emp = company.Employees.Single(e => e.EmployeeID == id);
if (TryUpdateModel(emp))
{
company.SaveChanges();
return RedirectToAction("Index");
}
else
{
AddOfficesToViewBag();
return View(emp);
}
}
I'll pick the Edit View, which is pretty much the same as the Create View;
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
Employee
#Html.HiddenFor(model => model.EmployeeID)
<div class="editor-label">
#Html.LabelFor(model => model.Office)
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.Office, (SelectList) ViewBag.OfficeList)
#Html.ValidationMessageFor(model => model.Office)
</div>
<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>
<div class="editor-label">
#Html.LabelFor(model => model.Age)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Age)
#Html.ValidationMessageFor(model => model.Age)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
I would say that the Edit, in particular, looks almost there. It manages to bind to the Employee object passed in and sets the dropdown to the appropriate entry.
Viewing the original HTML source shows that the output value is the Office.ToString() value.
The odd thing to me is that some magic is happening that binds Employee->Office to the correct entry, which makes the Edit view work, but there is no corresponding conversion of the selected item (a string, aka object->ToString()) to the original list.
This seems so basic (MVC / EF4 / DropDownList) that I feel I'm missing something incredibly fundamental.
All thoughts appreciated.
Regards
Scott
Based on the following you can
http://forums.asp.net/t/1655622.aspx/1?MVC+3+Razor+DropDownListFor+and+Model+property+from+EFCodeFirst
Do the following:
[HttpPost]
public ActionResult Edit(Guid id, FormCollection collection)
{
CollectionViewModel cvc = new CollectionViewModel();
cvc.Collection = _db.Collections.Where(c => c.CollectionId == id).Include("CollectionType").First();
Guid collectionTypeId = Guid.Parse(collection["CollectionTypeId"].ToString());
cvc.Collection.CollectionType =_db.CollectionTypes.Where(ct =>ct.CollectionTypeId == collectionTypeId).First();
if (TryUpdateModel(cvc))
{
_db.SaveChanges();
return RedirectToAction("Index");
}
}
ViewModel
public class CollectionViewModel
{
public Collection Collection {get; set; }
public Guid CollectionTypeId { get; set; }
public SelectList CollectionTypes { get; set; }
}

Resources