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.
Related
When using Razor to render a form for a complex Model that has sub-models, we'd usually use Partial Views to render the sub-models.
Simple example Model:
public class BlogPost
{
public string Content { get; set; }
public List<Comment> Comments { get; set; }
}
public class Comment
{
public string Content { get; set; }
}
BlogPost.cshtml:
#model BlogPost
#Html.TextAreaFor(x => x.Content)
#for (int i = 0; i < Model.Comments.Count; i++)
{
#Html.Partial('Comment', Model.Comments[i])
}
Comment.cshtml:
#model Comment
#Html.TextAreaFor(x => x.Content)
Now for the issue:
Say we want to send the values of all fields to a controller action that takes BlogPost as a parameter. The fields are going to be posted back to the controller like so:
Content=The+content+of+the+BlogPost&Content=The+first+Comment&Content=The+second+Comment
But what we need to have MVC map them correctly to the BlogPost view model, we need this naming convention:
Content=The+content+of+the+BlogPost&Comments[0].Content=The+first+Comment&Comments[1].Content=The+second+Comment
How can this be achieved in a clean way? We can only think of two ways which both seem to compromise the design:
Either we pass the BlogPost as a model to the partial view such that we can define the text area like so: #Html.TextAreaFor(x => x.Comments[i].Content). But this means we couple the partial view for comments to the parent view model - you could think of a scenario where the same partial view should be used in a different context, which is not possible if the partial view depends on the parent view model. Futhermore, the i would have to be passed to the partial view somehow.
Or we fall back to explicitely defining the name of every single field with strings: #Html.TextArea(ViewBag.Prefix + ".Content").
Is there any way to tell the partial view to apply a certain prefix to all field names?
chiccodoro,
if you create and EditorFor template of type Comment, mvc will handle all of this beautifully for you. However, that will only work well in a scenario where the rows are already present in the DB. Exampel from SO:
Submiting Parent & Children in razor
If you need to create new rows on the fly, then you'll have to use a little bit of trickery to allow the fields to operate as required. I used an article from steven sandersons website which allows you to add collection items at runtime and still retains unobtrusive validation etc. see this SO question and related article ref:
Editing a Variable Length List, ASP.NET MVC 3 Style with Table
This question already has answers here:
Closed 10 years ago.
I know my question is stupid, but I dont know solution of my problem and can understand similar questions on stackoverflow.
I doing simple blog.
And when I go to one post in this blog I must see text of post and comments for him. They there are in my datebase, but I dont know how display both.
Please help me
You can create a custom ViewModel for this particular View. Something like this:
public class BlogReaderViewModel
{
// various fields which exist on either the post or the comments
}
Then you'd bind to that ViewModel for the View. The Controller action would get the Models it needs and build an instance of the ViewModel to pass to the View.
Another option would be to use a Tuple. It's a generic class which acts as a strongly-typed container for multiple other types. So the View's Model would be something like this:
Tuple<Post, Comments>
From an overall design perspective, my biggest recommendation would be to consider how your Models relate to one another and find your "aggregate root." In the case of a blog post with comments, it sounds like the post should be the aggregate root. The Model itself should have the comments within it. Something like this:
public class BlogPost
{
public string Title { get; set; }
public string Body { get; set; }
public IEnumerable<Comment> Comments { get; set; }
}
The idea is that the aggregate root is the parent object and internally knows about its child objects. You shouldn't have to manually compose those hierarchies of objects every time you want to use them.
You have to create a ViewModel to represent this View or the data that this view need, for example:
public class OrderViewModel {
public int Id { get; set; }
public DateTime DateOrder { get; set; }
public decimal Total { get; set; }
public string CustomerName { get; set; }
public List<Item> Items { get; set; }
// other properties
}
And you shoul use this ViewModel to type your view, for sample (using razor):
#model Models.ViewModels.OrderViewModel
It depends on the relationship of the comments in the model. Usually comments should be a child collection of post. So in the view you should be able to render the comments with something like this (Razor):
#foreach (var comment in Model.Comments) {
// comments display goes here
}
Be sure when you pass the model to the view from the controller that you don't produce an inefficient query. Make sure that the query gets the comments with the blog, depending on how you are getting your model in the DB. If you are using EF that would be the "Include" directive, e.g.
.Include(p => p.Comment);
One option is to Create a composite model that represents both groups of data required to render the view, and pass the off each sub model to editor templates on the view itself.
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.
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/
I'm writing an MVC2 app using DataAnnotations. I have a following Model:
public class FooModel
{
[ScaffoldColumn("false")]
public long FooId { get; set; }
[UIHint("BarTemplate")]
public DateTime? Bar { get; set;}
}
I want to create a custom display template for Bar. I have created following template:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<DateTime?>" %>
<div class="display-label">
<span><%: Html.LabelForModel() %></span>
</div>
<div class="display-field">
<span><%: Html.DisplayForModel()%></span>
<%: Html.ActionLink("Some link", "Action", new { id = ??FooId?? }) %>
</div>
Now, my problem is that inside template for Bar I want to access another property from my model. I don't want to create a separate template for FooModel because than I will have to hardcode all other FooModel properties.
After a brief investigation with a debugger I can see that:
this.ViewData.ModelMetadata.ContainerType
is FooModel (as expected)
this.ViewData.TemplateInfo has a
non-public property VisitedObjects
(of type
System.Collections.Generic.HashSet<object>)
which contains two elements:
FooModel and DateTime?.
How can I get access to my FooModel? I don't want to hack my way around using Reflection.
Update:
I've accepted mootinator's answer as it looks to me as the best solution that allows type-safety. I've also upvoted Tx3's answer, as mootinator's answer builds upon it. Nevertheless, I think that there should be a better support form MVC in those kind of scenarios, which I believe are quite common in real world but missing from sample apps.
Maybe you could create new class, let's say UserDateTime and it would contain nullable DateTime and rest of the information you need. Then you would use custom display template for UserDateTime and get access to information you require.
I realize that you might be looking for other kind of solution.
I think you may be better off extracting this functionality to an HtmlHelper call from the Parent View.
Something like RenderSpecialDateTime<TModel>(this HtmlHelper html, Expression<Func<TModel,DateTime?>> getPropertyExpression) would probably do the job.
Otherwise, you will have to do something like what Tx3 suggested. I upvoted his answer, but posted this as an alternative.
Couldn't you use the ViewData dictionary object in the controller and then grab that in the ViewUserControl? It wouldn't be strongly typed but...you could write a helper to do nothing if it's empty, and link to say the example login history page if it had a value.
It would appear that somewhere between MVC 5.0 and 5.2.2 a "Container" property was added on to the ModelMetadata class.
However, because all of the methods in a provider responsible for metadata creation (GetMetadataForProperty, Create etc) do not have container in their signature, the Container property is assigned only in certain cases (GetMetadataForProperties and GetMetadataFromProvider according to reflected code) and in my case was usually null.
So what I ended up doing is overriding the GetMetadataForProperty in a new metadata provider and setting it there:
public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
{
var propMetaData = base.GetMetadataForProperty(modelAccessor, containerType, propertyName);
Object container = modelAccessor.Target.GetType().GetField("container").GetValue(modelAccessor.Target);
propMetaData.Container = container;
return propMetaData;
}
I know this is reflection but it's fairly succinct. It would appear that MS is correcting this oversite so maybe it will be possible to replace the reflection code in the future.
Sorry if this suggestion seems daft, I haven't tried it, but couldn't you do what Tx3 suggested without having to create a bunch of new classes by defining a generic class to reference whatever type of parent you want?
public class FooModel
{
[ScaffoldColumn("false")]
public long FooId { get; set; }
[UIHint("BarTemplate")]
public ParentedDateTime<FooModel> Bar { get; set;}
public FooModel()
{
Bar = new ParentedDateTime<FooModel>(this);
}
}
public class ParentedDateTime<T>
{
public T Parent {get; set;}
public DateTime? Babar {get; set; }
public ParentedDateTime(T parent)
{
Parent = parent;
}
}
You could expand that to encapsulate any old type with a <Parent, Child> typed generic, even.
That would also give you the benefit that your strongly typed template would be for
Inherits="System.Web.Mvc.ViewUserControl<ParentedDateTime<FooType>> thus you would not have to explicity name which template to use anywhere. This is more how things are intended to work.