DropDownListFor & Navigation Properties - asp.net-mvc-3

I'm running into an issue trying to use #Html.DropDownListFor().
I have a model with a navigation property on it:
public class Thing {
...
public virtual Vendor Vendor { get; set; }
}
In the controller I'm grabbing the vendor list to throw into the ViewBag:
public ActionResult Create() {
ViewBag.Vendors = Vendor.GetVendors(SessionHelper.CurrentUser.Unit_Id);
return View();
}
The html item in the view looks like this:
#Html.DropDownListFor(model => model.Vendor, new SelectList(ViewBag.Vendors, "Id", "Name"), "---- Select vendor ----")
#Html.ValidationMessageFor(model => model.Vendor)
The dropdown list is being rendered, and everything seems fine until I submit the form. The HttpPost Create method is returning false on the ModelState.IsValid and throwing a Model Error: The parameter conversion from type 'System.String' to type '...Models.Vendor' failed because no type converter can convert between these types.
If I let the page post through, I end up with a server error:
Exception Details: System.ArgumentNullException: Value cannot be null.
Parameter name: items
After searching high and low I haven't been able to find a reason that the #Html.DropDownListFor() isn't properly auto-binding a Vendor object to the navigation property.
Any help would be greatly appreciated.
EDIT:
I ended up having to explicitly set the ForeignKey attributes so that I could directly access "Vendor_Id" then I changed the DropDownListFor to point to "Vendor_Id" instead of the navigation property. That seems to work.

I have found that the best way to do this is as follows. Change the controller to create the SelectListItems.
public ActionResult Create() {
ViewBag.Vendors = Vendor.GetVendors(SessionHelper.CurrentUser.Unit_Id)
.Select(option => new SelectListItem
{
Text = (option == null ? "None" : option.Name),
Value = option.Id.ToString()
});
return View();
}
Then modify the view as follows:
#Html.DropDownListFor(model => model.Vendor, (IEnumerable<SelectListItem>)ViewBag.Vendors, "---- Select vendor ----")
#Html.ValidationMessageFor(model => model.Vendor)
You have to cast the ViewBag.Vendors as (IEnumerable).
This keeps the views nice and neat. You could also move the code that gets the SelectListItems to your repo and put it in a method called something like GetVendorsList().
public IEnumerable<SelectListItem> GetVendorsList(int unitId){
return Vendor.GetVendors(unitId)
.Select(option => new SelectListItem
{
Text = (option == null ? "None" : option.Name),
Value = option.Id.ToString()
});
}
This would separate concerns nicely and keep your controller tidy.
Good luck

I have replied similar question in following stackoverflow question. The answer is good for this question too.
Validation for Navigation Properties in MVC (4) and EF (4)
This approach doesn't publish the SelectList in controller. I don't think publishing SelectList in controller is good idea, because this means we are taking care of view part in controller, which is clearly not the separation of concerns.

Related

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.

A `ViewModel` for each page (`Create.cshtml` and `Edit.cshtml`)?

Questions
There are actually two related questions:
Should I create a ViewModel for each page?
If you do not have problems in creating a single ViewModel class for the two pages (Create.cshtml and Edit.cshtml) how can I validate the ViewModel in different ways (depending on the page that is being used)
Source
ViewModel
public class ProjectViewModel
{
public string Name { get; set; }
public string Url { get; set; }
public string Description { get; set; }
}
Edit.cshtml
#using BindSolution.ViewModel.Project
#model ProjectViewModel
#{
ViewBag.Title = Model.Name;
}
#Html.EditorForModel()
Create.cshtml
#using BindSolution.ViewModel.Project
#model ProjectViewModel
#{
ViewBag.Title = "New Project";
}
#Html.EditorForModel()
ProjectValidator.cs
public class ProjectValidator : AbstractValidator<ProjectViewModel>
{
private readonly IProjectService _projectService;
public ProjectValidator(IProjectService projectService)
{
_projectService = projectService;
RuleFor(p => p.Name)
.NotEmpty().WithMessage("required field")
/*The validation should be made only if the page is Create.cshtml. That is, if you are creating a new project.*/
.When(p => p.??) //Problem Here!!
.Must(n => !_projectService.Exist(n)).WithMessage("name already exists");
RuleFor(p => p.Url)
.NotEmpty().WithMessage("required field");
}
}
Note that if the user is editing an existing project, validation of the property name should not be done again.
ProjectController.cs > Edit method
[HttpPost]
public ActionResult Edit(Guid projectID, ProjectViewModel model)
{
var project = _projectService.Repository.Get(projectID);
if (ModelState.IsValid && TryUpdateModel(project))
{
_projectService.Repository.Attach(project);
if (_projectImageWrap.Create(project) && _projectService.Repository.Save() > 0)
return AjaxRedirect("Index");
}
return View(model);
}
Notes
If I create a ViewModel for each page, there is a duplication of code since pages have the same properties.
Add a property on the ViewModel indicating what page it is being displayed does not solve my problem as to instantiate the ViewModel, I use AutoMapper.
To validate the data, I use FluentValidator.
Thank you all for your help!
My understanding is that there isn't a 1:1 correlation between ViewModels and Views. Oftentimes you will have a View that will not require a ViewModel to go alongside with it.
You will want to create a ViewModel if and only if you need a Model absolutely paralleled and tailored to a specific View. This will not be the case 100% of the time.
When the functionality / use case /validation is different between the pages I use different models. If its the exact same besides the presence of an ID or something similar I use the same model, and its also possible to just use the same view if the differences are pretty minor.
Since your validation is different, if I were doing it I would create two different models so that I could use the out of the box DataAnnotations, with your validation though it may not be required. You could also on the edit model have a readonly property for name since its not editable any longer.
For me the same object must have the same validation on every time, in main to ensure the consistence of the object, independently if it was created or edited.
i think that you should create only one validation, and edit your "exists" method to pass to verify if it is a new object or the current object in repository.
Personally, I don't have a problem with 2 view models, especially if (as Paul Tyng suggested) you use a base class for the fields that are common to edit and create scenarios.
However, if you really only want a single view model then you would either need to:
add a flag to the view model and use the When() method in your validator. Note though that this will not generate the appropriate client-side only validation
define a second validator and invoke the appropriate one from the controller (i.e. instead of the "automatic" validation)
Provide another view Edit.cshtml which will allow the user to edit the data for a selected item.
Create another view Query.cshtml which based on the ItemName will allow the users to query the Inventory table.
Perform the calculation for the total profit (numbersold times (saleprice-purchasecost). Display the total profit.
(BONUS) Create another view Sell.cshtml that will indicate the sale of an item. Adding one to NumberSold and subtract one from NumberInventory for the selected record.

prepopulate Html.TextBoxFor in asp.net mvc 3

I'n new at this, so apologies if this isn't explanatory enough. I want to prepopulate a field in a form in asp.net mvc 3. This works;
#Html.TextBox("CompName", null, new { #value = ViewBag.CompName })
But when I want to prepopulate it with a value and send that value to my model, like this;
#Html.TextBoxFor(model => model.Comps.CompName, null, new { #value = ViewBag.CompName })
It won't work. Any ideas?
Thanks in advance!
So, I would suggest is to move to using viewmodels rather than the ViewBag. I made a folder in my project called ViewModels and then under there make some subfolders as appropriate where I put my various viewmodels.
If you create a viewmodel class like so:
public class MyViewModel
{
public string CompName { get; set; }
}
then in your controller action you can create one of those and populate it, maybe from some existing model pulled from a database. By setting that CompName property in the viewmodel, it'll have that value in your view. And then your view can look something like this:
#model MyNamespace.ViewModels.MyViewModel
#Html.EditorFor(model => model.CompName)
or #Html.TextBoxFor would work too.
Then back in your controller action on the post, you've got something like this:
[HttpPost]
public ActionResult MyAction(MyViewModel viewModel)
{
...
// do whatever you want with viewModel.CompName here, like persist it back
// to the DB
...
}
Might be that you use something like automapper to map your models and viewmodels but you could certainly do that manually as well, though the whole lefthand/righthand thing gets quite tedious.
Makes things much easier if you do it this way and isn't much work at all.
Update
But, if you really want to pass that value in view the ViewBag, you could do this:
In your controller action:
ViewBag.CompName = "Some Name";
Then in your view:
#Html.TextBoxFor(model =>model.Comps.CompName, new {#Value = ViewBag.CompName})
And that'll pre-populate the textbox with "Some Name".
I'd still go with the viewmodel approach, but this seems to work well enough. Hope that helps!
From your controller, if you pass a model initialized with default values using one of the View(...) method overloads that accepts a model object, these values will be used by the view when rendered. You won't need to use the #value = ... syntax.

Razor: #Html.HiddenFor() need to turn off validation

Could you help me, please.
I have a class:
public class Product
{
...
// NOT REQUIRED!
public virtual Category Category{ get; set; }
}
But when in a view I create
#Html.HiddenFor(model => model.Category.Id), or
#Html.Hidden("model.Category.Id", model => model.Category.Id)
razor adds validation attribute to this.
How to turn it off? (in model, in view)
How to turn off validation event if a property has the attribute [Required]?
I found out that this is not a razor problem, it is somewhere in MVC.
Even if I manage to pass "Category.Id" value = "" to the server, TryModelUpdate() will fail - it requires "Category.Id" to be set, but it's not required in my model.
Why is it so??!
I solved the same issue with an crutch like this:
#{ Html.EnableUnobtrusiveJavaScript(false); }
#Html.HiddenFor(t => t.Prop1)
#Html.HiddenFor(t => t.Prop2)
...
#{ Html.EnableUnobtrusiveJavaScript(true); }
Setup a hidden like:
#Html.Hidden("CategoryIdHidden", model => model.Category.Id)
And process the posted hidden value separate from the model binding stuff... I think the validation is UI specific, and not model specific, so it wouldn't validate the category ID.
Or, supply in the hidden a default value of "0". A value of "" probably won't evaluate correctly if the category.ID is of type int, hence its null, hence it errors.
HTH.

How to post the selected value of a selectlist to the controller using a view model?

This question has been asked in various forms but none of the answers seem to fit my situation. I am simply trying to retrieve the selected value of a dropdown list in my controller.
Here is my code:
ViewModel.cs
public class ViewModel
{
public ViewModel() {}
public ViewModel(Contact contact, IEnumerable<State> states)
{
this.Contact = contact;
this.States = new SelectList(states, "Id", "Name", contact.StateId);
}
public Contact Contact {get;set;}
public SelectList States {get;set;}
}
Controller.cs
[HttpPost]
public ActionResult Edit(ViewModel viewModel)
{
_contactService.UpdateContact(viewModel.Contact);
return RedirectToAction("Item", new {id = viewModel.Contact.Id});
}
View.cshtml
<button type="submit" onclick="javascript:document.update.submit()"><span>Update</span></button>//aesthic usage.
#{using (Html.BeginForm("Edit", "Controller", FormMethod.Post, new { name = "update" }))
{
#Html.HiddenFor(m => m.Contact.Id)
#Html.LabelFor(m => m.Contact.Name, "Name:")
#Html.TextBoxFor(m => m.Contact.Name)
<label for="state">State:</label>
#Html.DropDownList("state", Model.States)
}
}
Everything works as expected except that no values from the dropdownlist are passed in my posted viewModel to the controller. The edit page and all fields load correctly. The dropdowns bind correctly and have their selected values displayed properly. However, when I post I only get a "Contact" object passed to the controller. The "States" SelectList object is null.
I tried mapping a "StateId" property in my viewModel contstructor but that did not work either. What am I doing wrong?
Thanks.
I hate answering my own questions but based on the multiple issues I had coupled with the myriad of answers available I thought I would summarize my findings.
First off thanks for Filip, his answer did not exactly fix my problem but it led me in the right direction. +1
If you are creating a form for viewing and editing that requires a drop down list, here are some suggestions and gotchas. I will start with a list of parameters that I needed to fit my needs.
Strongly typed views in my view are preferable. Minimize magic strings.
View models should contain as little logic and extraneous elements as possible. There only job should be to facilitate a collection of data objects.
The drop down list should display the selected value.
The selected value should map easily back to the view model on form submit.
This may sound like an obvious and easily obtainable list but for someone new to MVC, it is not. I will revise my code from above with comments. Here is what I did.
ViewModel.cs
public class ViewModel
{
public ViewModel() {}
public ViewModel(Contact contact, IList<State> states)
{
//no need to pass in a SelectList or IEnumerable, just what your service or repository spits out
this.Contact = contact;
this.States = states;
}
public Contact Contact {get;set;}
public IList<State> States {get;set;}
}
Controller.cs //nothing really different than above
public ActionResult Edit(int id)
{
var contact = _contactService.GetContactById(id);
var states = _stateService.GetAllStates();
return View(new ViewModel(contact, states));
}
public ActionResult Edit(ViewModel viewModel)
{
_contactService.UpdateContact(viewModel.Contact);
return RedirectToAction("Edit", new {id = viewModel.Contact.Id });
}
View//thanks goes to Artirto at this post
#{using (Html.BeginForm("Edit", "Controller", FormMethod.Post))
{
#Html.HiddenFor(m => m.Contact.Id)
#Html.DropDownListFor(m => m.Contact.StateId, new SelectList(Model.States, "Id", "Name", #Model.Contact.StateId))
<input type="submit" value="Save" />
}
}
Try using #Html.DropDownListFor instead, if "state" is a part of your model you can use it like this:
#Html.DropDownListFor(m => m.Contact.StateId, Model.States, "-- Please select a State --") where m.State holds the selected value.
Also not to confuse the IEnumerable with the Model, I would put that in the ViewBag / ViewData.
It would look something like this instead:
#Html.DropDownListFor(m => m.Contact.StateId, (IEnumerable<string>)ViewBag.States, "-- Please select a State --")
And in your action that returns this view you will need to initialize the State enumerable to the ViewBag.States property.

Resources