hiddenFor helper is pulling id from action parameter, not the viewModel - asp.net-mvc-3

I've stumbled upon a really weird situation with ASP.Net MVC, passing an "id" as an action parameter and an "id" hidden form element. I have an action that has an "id" parameter. This id value represents a project. I have a controller action where I'm creating a data entry form for assigning an employee to the passed in project (I create a drop down list where administrator selects an employee to be assigned to passed project). By assigning an employee to a project, I create a ProjectEmployee record (project id, employee id, and an id that represents the combination which is an identity column in the database). The identity column (which is named "id") is also a hidden form element. This is necessary because I need to be able to edit the project/employee assignment at a later date.
Anyways, when creating a new employee assignment to a project, the project id (which is the "id" being passed to the action) is being applied to the "id" hidden form element.
I'm passing 117 into the action. 117 is the projectId. It's being set to the "id" hidden field which should be 0 because 0 represents a new project/employee assignemnt.
View Model
id - unique id that represents the Project/Employee combination
projectId - the "Id" being passed to action
EmployeeId - what the admin is selecting from drop down
rate
startdate
endDate
...
Data Entry Form
#Html.HiddenFor(m => m.Id)
#Html.HiddenFor(m => m.ProjectId)
...
Id: #Model.Id <br />
ProjectId: #Model.ProjectId<br />
So, #Html.HiddenFor(m => m.Id) renders a hidden form element with a value of 117. #Model.Id renders 0 to the UI. Stepping through the code I can visually see the value of the Id property to be 0. How come HiddenFor is getting it's wires crossed and pulling the value of 117?
This bug has made it's way into production so I have a mess on my hands with data getting messed up because instead of creating a new record in the database table, I'm actually UPDATING existing records because the "Id" property is erroneously getting set from 0 (which represents a new record) to 117 (which is the projectId) and therefore am updating a different record.

How come HiddenFor is getting it's wires crossed and pulling the value of 117?
That's by design. All HTML helpers such as TextBoxFor and HiddenFor first use the value of ModelState when binding and then the value of the model. I presume your controller action looks like this:
public ActionResult Foo(int id)
{
SomeModel model = ...
// At this stage model.Id = 0
return View(model);
}
The thing is that the default model binder adds a value into the ModelState with the key id which is used by the helper and the model property value is ignored. This is not a bug. It is how the HTML helpers are designed. It's a bit confusing in the beginning when you don't know it, but once you get accustomed to this behavior you don't fall in the trap a second time.
One possibility to fix the problem is to remove this value from ModelState:
public ActionResult Foo(int id)
{
ModelState.Remove("id");
SomeModel model = ...
// At this stage model.Id = 0
return View(model);
}
Or rename the Id property inside your model to something else to avoid the conflict.

As an alternative to changing your field names, you can make a new helper method that doesn't use the route values when evaluating what to put in the value attribute.
I created this helper for the extremely common id case:
public static MvcHtmlString HiddenIdFor<TModel, TValue>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression)
{
var value = ModelMetadata.FromLambdaExpression(expression, html.ViewData).Model.ToString();
var builder = new TagBuilder("input");
builder.MergeAttribute("type", "hidden");
builder.MergeAttribute("name", ExpressionHelper.GetExpressionText(expression));
builder.MergeAttribute("value", value);
return MvcHtmlString.Create(builder.ToString(TagRenderMode.SelfClosing));
}
Then you can use this in the template:
#Html.HiddenIdFor(m => m.Id)
#Html.HiddenIdFor(m => m.ProjectId)

Related

Pass a parameter to a field in another page ASPMVC3

I have a person class that has an index file that lists each person.
An Html.ActionLink points to another controller for notes on the person.
One person, many notes, so I want to pass the PersonID as a param and insert it into the new note form. The parameter is not the NoteID, ie the key
#Html.ActionLink("Note", "Create", "Note", new { id = item.PersonID }, null)
The PersonID is in the url that passes to the note form.
How do I get the PersonID into the Note form?
Many thanks,
Harry
In your Create action method in the Note controller, you will receive the ID as a paramter to the action method if you include a parameter for it in the function:
public ActionResult Create(string id)
{
}
From there it is up to you to pass the id into the view you are using to construct the Create Note page. If you have a Note model object and one of the properties is the PersonID, you might want to new up a new Note object, set the PersonID to the value passed in the parameter, and then pass the new Note object to the view page as the model:
return View(newNote);
Hope that is what you are looking for.
You can knockout it using the same name as u declared ID
Or.. just handle it on another Controller using
the same name. ID:
[HttpGet]
public ViewResult Create(int id)
{
return View(id); <--
}
Then with razor-engine it comes with #Model

MVC3 DropDownListFor not setting selected property

In MVC3, when using DropDownListFor is it necessary for the first parameter to be a string? I have the following setup:
#Html.DropDownListFor(m => m.MyListItemId, Model.MyListItems,
new Dictionary<string, object>
{
{ "style", "width:120px" },
{ "data-type", "myList" }
})
where m.MyId is an int on my viewmodel. I'm having an issue where when I change the selected item in my drop down list, and inspect the rendered html, the "selected" property is not set to the newly selected item. This is a problem as i'm using jquery clone function to copy that row and i need the list with the new selected item to be copied to my new row. Ideas?
Update - Changing the property on the viewmodel to a string makes no difference.
Is this a bug with mvc dropdownlistfor? I've read quite a few posts on similar issues, but can't seem to find a solution that works in this instance. This is how my list is setup in my code:
var myListItems = _myRepository.GetAll();
model.MyListItems = new SelectList(myListItems, "Id", "Name", lineItem.myListItemId);
model.MyListItemId = lineItem.myListItemId;
where lineItem is passed into this method
No, the selected value property does not need to be a string, it can be an int. As long as the value is convertible to a string, it should work (so selected value type could be a Guid, int, bool, etc).
I have sometimes found issues when my route for the page has a route parameter with the same name as the selected value model property. For example, consider this:
route: "/establishments/{establishmentId}/edit"
Model:
public class MyViewModel
{
public int EstablishmentId { get; set; }
public SelectListItem[] Establishments { get; set; }
}
View:
#Html.DropDownListFor(m => m.EstablishmentId, Model.Establishments)
With this code, the selected value of the drop down list would always be whatever establishmentId is in the route. So if the route were /establishments/12/edit, then value 12 would be selected in the dropdown. It doesn't matter that the route parameter and model property capitalization doesn't match.
I figured this out by downloading the MVC source code, making my own copy of the DropDownListFor (named MyDropDownListFor), then stepping through the code to see what happened. If you are still having trouble with MVC3, I suggest you do the same. You need to figure out whether this is an issue with the server code, or your jquery clone stuff.

Mvc3 Adding user roles in tightly bound dropdownbox

I have a register model thats tighly bound to a register view. The register model has a selectlistitem poperty. Im having trouble populating a dropdownbox with the select list items.
var users = Roles.GetAllRoles();
model.UserRoles = users.Select(m => new SelectListItem()
{Value = m.ToString(),Text = m.ToString()})
#Html.DropDownListFor(r=>r.ToString(),Model.UserRoles,"Select Role")
You need to bind the selected value to a property of your model, like so:
(in your Model):
public string ActiveRole { get; set; }
...
(in your View):
#Html.DropDownListFor(m => m.ActiveRole, Model.UserRoles,"Select Role")
The first argument of the DropDownListFor method is the property which holds the value of the list. If the value in the list will be the id of the role this has to have the argument x => x.RoleId where x is your model and RoleId is the property in the model. You are binding the dropdownlist to the string representation of the viewmodel, the result of which is garbage.
The second argument is an IEnumerable<SelectListItem> which holds the options for the list. This object has the following values: Value is the value that is send back to the server, this should be what identifies the role, often the id of the role. Text is the text that is displayed in the list, it is what the user sees. Selected is a boolean that indicates which item in the list is selected. If it is false in all of the SelectListItems, the top one is selected, which is standard HTML behaviour.
In your case, you are placing the SelectListItems in the model, which is an unnecessary step, but should work. The model binder won't understand the first argument though, so you need to fix that one.

Where does DropDownListFor get the model from?

You can create a dropdownlist using
#Html.DropDownListFor(model => model.SelectedId, model.DropDownItems);
but where does DropDownListFor get the value to pass into the model => model.SelectedId lambda from?
SelectedId is just the integer of the field in the model.
the model is passed in from the controller using
return View(myModel);
And the model can be defined in the view at the top
#model mymodelnamespace.RoomBookingInsert
So the dropdownbox's value is set as the SelectedId field in your model.
A proper field name for example would be RoomNo if that clarifies it better.
#Html.DropDownListFor(model => model.RoomNo, model.Rooms);
As long as model.DropDownItems (or model.Rooms in my example) Contains a item with the value of that attribute, it will set it as the selected item in the list as default.
Edit:
If you are using viewbag, rather than the model, instead use DropDownList. (each input has a for extension for use with models)
#Html.DropDownList("RoomNo",
new SelectList(ViewBag.Rooms as System.Collections.IEnumerable ?? Enumerable.Empty<SelectListItem>(), "RoomNo", "RoomName"),
new { #value = #ViewBag.RoomNo})
This above, creates an input form with id RoomNo,
and uses a SelectList that will default to empty if null. The fields that defined the values and text shown are set at the end of the select list parameters.
The last bit sets the default value to the contents of #ViewBag.RoomNo.
You can also create the select list in the controller, and then simply set the dropdown options to the viewbag.myselectList entity.

DropDownList Client Side Validation is validating when it should not be. (MVC3, Razor)

I am still learning MVC3 and Razor, so this is perhaps a simple question.
On a view I have a DropDownList whose sole purpose is to help filter (via AJAX) a second drop down list:
#Html.DropDownList("Segments", "-- select segment --")
There is a Segments property of the ViewModel that is defined as:
public IEnumerable<SelectListItem> Segments { get; set; }
There is JavaScript that handles the change event for this DropDownList and populates another DropDownList with appropriate values. That other DropDownList is defined like this:
#Html.DropDownListFor(m => m.fafhProdRecId, Enumerable.Empty<SelectListItem>(), "-- select product recommendation --")
This all works fine until I submit. When I submit, I get a validation error on the Segments drop down list!
Now -- there should be absolutely NO validation on the segments DropDownList -- there shouldn't be any client side validation on EITHER drop down list, for that matter.
But when I try to submit, I get the validation error message back:
The value '1' is invalid.
I have no idea why this is happening.
I have no idea how to decorate the Segments property to say that it is NOT required.
I have no idea how to tell the unobtrusive javascript validator that it is, in fact, being quite obtrusive.
In your ViewModel class add [Bind(Exclude = "Segments")]
From: Using Data Annotations for Model Validation
make sure that your Model has fafhProdRecId as nullable, I imagine it's declared as:
public int fafhProdRecId { get; set; }
change this to:
public int? fafhProdRecId { get; set; }
hopefully, that should resolve the issue as this effectively makes the model field nullable (assuming the db field IS nullable too of course).

Resources