MVC3 Razor model binder and inherited collections - asp.net-mvc-3

I hope I'm not missing something incredibly obvious here but is there any reason why model binder is always having trouble binding a view model that inherits from a collection?
Lets say I want to show a paged list and display a combo box and add button above it (dealing with simple lists). Involved classes would look like:
public class PagedList<T> : List<T>
{
public int TotalCount { get; set; }
}
And then a view model that looks like:
public class MyViewModel : PagedList<ConcreteModel>
{
public IEnumerable<ChildModel> List { get; set; }
public int? SelectedChildModelId { get; set; }
}
So in the view (Razor):
#model MyViewModel
#using (Html.BeginForm())
{
#Html.DropDownListFor(model => model.SelectedChildModelId, new SelectList(Model.List, "ChildModelId", "DisplayName"))
}
And the controller HttpPost action:
public ActionResult(MyViewModel viewModel)
{
...
}
The above will cause viewModel in ActionResult to be null. Is there a logical explanation for it? From what I can tell it's specific only to view models that inherit from collections.
I know I can get around it with custom binder but the properties involved are primitive types and there isn't even any generics or inheritance.
I've reworked the view models to have the collection inherited type as properties and that fixes the issue. However I'm still scratching my head over why the binder breaks down on it. Any constructive thoughts appreciated.

To answer my own question: All my models that have anything to do with collections no longer inherit from generic list or similar but instead have a property of the required collection type. This works much better because when rendering you can use
#Html.EditorFor(m => m.CollectionProperty)
And create a custom editor under Views/Shared/EditorTemplates for contained type. It also works beautifully with model binder since all individual items from collection get a index and the binder is able to auto bind it when submitted.
Lesson learned: if you plan to use models in views, don't inherit from collection types.

Sometimes model binding to a collection works better if the data in the form post is formatted differently.
I use a plugin called postify.
http://www.nickriggs.com/posts/post-complex-javascript-objects-to-asp-net-mvc-controllers/

Related

MVC OrderBy EditorFor IEnumerable

I have just registered, and this is my first post, so please bear with me if the question is not the best. I have had a look about and can't find an answer that suits my requirements; this is possibly because it's not possible to achieve what I want.
I have a partial view which pulls through an IEnumerable list of EditorFor fields from a viewmodel:
#model DocumentViewModelContainer
#Html.EditorFor(m => m.Document.Metadata)
The DocumentViewModelContainer has the following code:
public class DocumentViewModelContainer
{
public DocumentViewModel Document
{
get;
set;
}
The DocumentViewModel has the following code:
public class DocumentViewModel
{
public IEnumerable<DocumentMetadataFieldViewModel> Metadata
{
get;
set;
}
}
There's a ton of other objects in both view models that I've left out as being irrelevant in this question. The DocumentMetadataFieldViewModel is made up of several fields of standard types (int, strings etc.)
What I'm trying to achieve is adding an OrderBy to this list pulled back by ordering by an object in the bottom view model, such as follows:
#model DocumentViewModelContainer
#Html.EditorFor(m => m.Document.Metadata.OrderBy(i => i.InstanceFieldOrder))
However this gives the error:
System.InvalidOperationException : Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.
I'm not only very new to MVC, but to C# in general; this project has had me learning the language on the fly, so please play nice :)
Thanks,
Mark
You should do this ordering in your controller action which is responsible to retrieve your view models and pass them to the view.
You could always perform the following horror in your view:
#model DocumentViewModelContainer
#{
Model.Document.Metadata = Document.Metadata.OrderBy(i => i.InstanceFieldOrder).ToList();
}
#Html.EditorFor(m => m.Document.Metadata)
but promise me you won't do that.

Passing IEnumerable<Object> to ViewModel - Does Object need to be ViewModel?

Getting my head around MVC today and ran across the best practice of not passing a Model directly to a view. Instead, use a ViewModel.
I researched AutoMapper and plan on using it to map my ViewModels to the respective Models. And I understand that AutoMapper is smart enough to map IEnumerable to IEnumerable without a separate mapping, as long as source and dest are mapped.
But I'm a bit confused about how to handle passing an IEnumerable in my ViewModel to my view. I currently have my page working using a ViewModel that includes IEnumerable but I read that this is just as bad as passing the IEnumerable directly to the view. So do I need a separate ViewModel to hold the object which will be used in an IEnumerable property of the main ViewModel?
So where Activity is the Model in question:
public class ActivityHistoryViewModel
{
public IEnumerable<Activity> activities { get; set; }
}
do I need to create ActivityViewModel and write my ActivityHistoryViewModel like this?
public class ActivityHistoryViewModel
{
public IEnumerable<ActivityViewModel> activities { get; set; }
}
Is there an easier way to do this?
Yes, this is correct. Assuming the only data your model will need is the list, then you don't really need ActivityHistoryViewModel and the view can be typed as such:
#model IEnumerable<ActivityViewModel>
your auto mapper config would look like this:
Mapper.CreateMap<Activity, ActivityViewModel>();
you would map like this:
IEnumerable<Activity> data = GetActivities();
var model = Mapper.Map<IEnumerable<Activity>, IEnumerable<ActivityViewModel>>(data);
return View(model);
And when you define ActivityViewModel you can either create a property-for-property duplicate type, or trim out the excess data you don't need (in my case it would be something like "created date", that is db generated and of no importance to users).
Or if you want to stick with ActivityHistoryViewModel to pass along more than just the list:
view type:
#model ActivityHistoryViewModel
mapping config can remain the same
map like this:
IEnumerable<Activity> data = GetActivities();
var model = new ActivityHistoryViewModel() {
someOtherProperty = "hello world!",
activities = Mapper.Map<IEnumerable<Activity>, IEnumerable<ActivityViewModel>>(data)
};
return View(model);

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.

Reusable editor template with DropDownList for business objects

I'm using MVC3 with Razor views and would like to build reusable DropDownLists for several of my classes, but after much searching I have not found an example that performs exactly how I need it...
For this example I have two classes like this:-
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
public Group Group { get; set; }
}
public class Group
{
public int ID { get; set; }
public string Name { get; set; }
}
I have a working Controller/View for Person. The view has a DropDownListFor control:
#model Person
...
#Html.DropDownListFor(o => o.Group.ID, (ViewData["groups"] as SelectList))
The view uses the Person class directly, not an intermediary model, as I haven't found a compelling reason to abstract one from the other at this stage.
The above works fine... in the controller I grab the value from Group.ID in the Person returned from the view, look it up, and set Person.Group to the result. Works, but not ideal.
I've found a binder here: MVC DropDownList values posted to model aren't bound that will work this out for me, but I haven't got that working yet... as it only really seems useful if I can reuse.
What I'd like to do is have something like this in a template:-
#model Group
#Html.DropDownListFor(o => o.Group.ID, (ViewData["groups"] as SelectList))
And use it in a view like this:-
#Html.EditorFor(o => o.Group)
However the above doesn't seem to work... the above EditorFor line inserts editors for the whole class (e.g. a textbox for Group.Description as well)... instead of inserting a DropDownList with my groups listed
I have the above template in a file called Group.cshtml under Views/Shared/EditorTemplates
If this worked, then whenever a class has a property of type Group, this DropDownList editor would be used by default (or at least if specified by some attribute)
Thanks in advance for any advice provided...
You can create a drop down list user control to handle this. Under your Shared folder create a folder called EditorTemplates and place your user control there. MVC, by convention, looks in the Shared/EditorTemplates for any editor templates. You can override where it looks for the editor templates but I won't go in to that here.
Once you have your user control created, you'll need to decorate the appropriate property with the "UIHint" attribute to tell the engine what editor it should use for that property.
Following would be a sample implementation.
In the Shared/EditorTemplates folder your user control (_GroupsDropDown.cshtml in this case) would look like:
#model Group
#Html.DropDownListFor(o => o.ID, (ViewData["groups"] as SelectList))
Modify the Group property in the Person to add the UIHint attribute as follows:
**[UIHint("_GroupsDropDown")]**
public Group Group { get; set; }
In your controller you would need
ViewData["groups"] = new SelectList(<YourGroupList>, "ID", "Name");
Once you have the above code you can use the EditorFor syntax like you desire.
Hope this helps.

Using one Partial View Multiple times on the same Parent View

I am using MVC3 razor. I have a scenario where I have to use a partial view multiple times on the same parent view. The problem I am having is that when the Parent View gets rendered, it generates same names and ids of the input controls within those partial views. Since my partial views are binded to different models, when the view is posted back on "Save" it crashes. Any idea how can i make the control id/names unique, probably some how prefix them ?
Awaiting
Nabeel
Personally I prefer using editor templates, as they take care of this. For example you could have the following view model:
public class MyViewModel
{
public ChildViewModel Child1 { get; set; }
public ChildViewModel Child2 { get; set; }
}
public class ChildViewModel
{
public string Foo { get; set; }
}
and the following controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel
{
Child1 = new ChildViewModel(),
Child2 = new ChildViewModel(),
};
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
and inside the Index.cshtml view:
#model MyViewModel
#using (Html.BeginForm())
{
<h3>Child1</h3>
#Html.EditorFor(x => x.Child1)
<h3>Child2</h3>
#Html.EditorFor(x => x.Child2)
<input type="submit" value="OK" />
}
and the last part is the editor template (~/Views/Home/EditorTemplates/ChildViewModel.cshtml):
#model ChildViewModel
#Html.LabelFor(x => x.Foo)
#Html.EditorFor(x => x.Foo)
Using the EditorFor you can include the template for different properties of your main view model and correct names/ids will be generated. In addition to this you will get your view model properly populated in the POST action.
There is an alternative:
Add a prefix to the PartialView
Bind the model, removing the prefix
For 1, set the prefix in your View:
ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "prefix";
For 2, you can recover the data with UpdateModel, like this:
UpdateModel(producto, "prefix");
This is not very advisable because your action doesn't receive the data as a parameter, but updates the model later. This has several inconvenients: 1) it's not clear what your action needs by looking at its signature 2) it's not easy to provide the input to the action for unit testing it 3) the action is vulnerable to overflow parameters (parameters provided by the user that shouldn't be there and are mapped to the model).
However, for 2 there is an alternative: register a custom Model Binder that allows you to do remove the prefix. And the custom Model Binder must know about it.
A good solution is in this SO Q&A: How to handle MVC model binding prefix with same form repeated for each row of a collection? But it has a little flaw: if you add a hidden field with the name "__prefix" in a partial view, and you render it several times as a partial view, this ID will be repeated for several different elements in the page, which is not allowed, and can provoke some trouble. And one of the most important reasons to provide a prefix is precisely rendering the same "edit" view as partial views for several instances of an entity. I.e. this would happen in a page like gmail, where you can edit several emails at once.
There are several possible solutions for this problem.
One of them is providing the prefix as a query string or routedata value, and not as a form field, which avoid the Id conflicts, and can be found by the model binder. (It can always have the same name).
Another solution is to use a hidden field, with a fixed pattern, but which is different for every rendered view. The prefix could follow this pattern for uniqueness: "PP$ActionControllerId" like "PP$EditProduct23", which is unique for each rendered view, and can be easily found between the request parameters looking for one that starts with "PP$".
And a final solution would be to create the prefix only in the view, and not providing it in any kind of request parameter. The Model binder would have to look for the prefix examining the names of the request parameters, until it finds one whose prefix follow the pattern.
Of course, the custom ModelBinder must be adapted to work tieh the chosen convention.

Resources