Given the following class, what is your opinion on the best way to handle create/edit where Attributes.Count can be any number.
public class Product {
public int Id {get;set;}
public string Name {get;set;}
public IList<Attribute> Attributes {get;set;}
}
public class Attribute {
public string Name {get;set;}
public string Value {get;set;}
}
The user should be able to edit both the Product details (Name) and Attribute details (Name/Value) in the same view, including adding and deleting new attributes.
Handling changes in the model is easy, what's the best way to handle the UI and ActionMethod side of things?
Look at Steve Sanderson’s blog post Editing a variable length list, ASP.NET MVC 2-style.
Controller
Your action method receives your native domain model Product and stays pretty simple:
public ActionResult Edit(Product model)
View
Edit.aspx
<!-- Your Product inputs -->
<!-- ... -->
<!-- Attributes collection edit -->
<% foreach (Attribute attr in Model.Attributes)
{
Html.RenderPartial("AttributeEditRow", attr);
} %>
AttributeEditRow.ascx
Pay your attention to helper extension Html.BeginCollectionItem(string)
<% using(Html.BeginCollectionItem("Attributes")) { %>
<!-- Your Attribute inputs -->
<% } %>
Adding and editing of new attributes is possible too. See the post.
Use the FormCollection and iterate through the key/value pairs. Presumably you can use a naming scheme that will allow you to determine which key/value pairs belong to your attribute set.
[AcceptVerbs( HttpVerb.POST )]
public ActionResult Whatever( FormCollection form )
{
....
}
Use a custom Model Binder, and write the Action methods as you would normally:
ActionResult Edit(
int id,
[ModelBinder(typeof(ProductModelBinder))] Product product
) ...
In your ProductModelBinder, you iterate over the Form Collection values and bind to a Product entity. This keeps the Controller interface intuitive, and can help testing.
class ProductModelBinder : IModelBinder ...
Depends on the experience you are looking to create for the user. I have implemented something similar for tagging content. In the model, Tags are represented as IList, but the UI shows a comma delimited list in a single text field. I then handle merging the items in the list into a string to populate the text field, and I split the input to put items back into the IList in the model.
In my DAL, I then deal with converting the List into LINQ entities, handle inserts and deletes, etc.
It isn't the most straight forward code, but it isn't too difficult to manage and it gives the user an expected interface.
I'm sure there are other ways to handle it but I would focus on what would work best for the user and then work out the mapping details based on that.
Andrew,
I'm thinking something a little more difficult than tags. In this simple case a name / value pair .. color: Red; size: 10; material: cotton.
I think anything that could be used on that could extend to more complex. I.e. Adding a category and adding all its items on the same page. It's relatively easy to add another line using some jQuery, but what's the consensus on sending the info to the ActionMethod?
You can't code:
public ActionResult Whatever(stirng attr1Name, string attr2Name, string attr3Name ...
Also I don't think accepting this would work either:
public ActionResult Whatever(ILIst<Attribute> attributes, string productName ...
Related
I have a view model like this:
public class EditVM
{
public Media.Domain.Entities.Movie Movie { get; set; }
public IEnumerable<Genre> Genres { get; set; }
}
Movie is the real entity I wish to edit. Genres is simply present to populate a drop down. I would prefer that when I call:
#Html.TextBoxFor(m => m.Movie.Title)
inside my strongly typed view that the input control have a name = "Title" instead of "Movie.Title"
I do not wish to split my view into partial views or lose my strongly typed view by using ViewData or the like.
Is there a way to express to the View that I do not wish to have the Movie. prefix? I noticed that you can set:
ViewData.TemplateInfo.HtmlFieldPrefix = "x";
in the controller, but unfortunately it seems only to allow adding an additional prefix. Setting it to "" does nothing.
Is there any work around for this? Or am I stuck with the unfortunate prefix that isn't really necessary in this case if I wish to keep strongly typed views and lambdas?
Thanks for any help.
Update:
Here's the controller actions to maybe make things a bit clearer.
public ActionResult Edit(int? id)
{
var vm = new EditVM
{
Movie = id.HasValue ? _movieSvc.Find(id.Value) : new Movie(),
Genres = AppData.ListGenres()
};
return View(vm);
}
[HttpPost]
public void Edit([Bind(Prefix = "Movie")]Movie m)
{
_movieSvc.AddOrUpdateMovie(m); //Exceptions handled elsewhere
}
No, in order to do what you want you would have to rewrite the Html helpers, and then you would have to write your own model binder. Seems like a lot of work for minimal gain.
The only choice is a Partial view in which you pass the Movie object as the model. However, this would require you to write your own model binder to have it be recognized.
The reason you have to do m.Movie.Title is so that the ID has the correct name, so the model binder can recognize it as a member of your model.
Based on your update:
Your options are:
Use non-strongly typed helpers.
Use a partial view.
Rewrite the stronly typed helpers
Don't use the helpers at all, and write the values to the HTML
Personally, i'd just use 1 or 2, probably 2.
EDIT:
Based on your update above. Change your code to this (note, Genres does not get posted back to the server, so m.Genres will just be null on postback):
[HttpPost]
public void Edit(EditVM m)
{
_movieSvc.AddOrUpdateMovie(m.Movie); //Exceptions handled elsewhere
}
EDIT:
I did just think of an alternative to this. You could simply do this:
#{ var Movie = Model.Movie; }
#Html.TextBoxFor(m => Movie.Title)
However, if there was a validation error, you would have to recreate your EditVM.
I have a view model like this
I think that you might have some misunderstanding about what a view model is. A view model shouldn't contain any reference to your domain models which is what those Movie and Genre classes seem to be. I mean creating a new class that you suffix with VM and in which you stuff all your domain models as properties is not really a view model. A view model is a class that is specifically designed to meet the requirements of your view.
A much more correct view model would looks like this:
public class EditVM
{
public string MovieTitle { get; set; }
public IEnumerable<GenreViewModel> Genres { get; set; }
}
and in your view you would have:
#Html.EditorFor(x => x.MovieTitle)
#Html.EditorFor(x => x.Genres)
Another option is to either use the TextBox(string name, object value) overload instead of the TextBoxFor:
#Html.TextBox("Title", Model.Movie.Title)
You could also specify the input tag HTML instead of using a helper.
Another option is to take EditVM as your postback parameter. This is what I would do. My post action parameter is always the same type of the .cshtml model. Yes there will be properties like lists that are null, but you just ignore those. It also allows you to gracefully handle post errors as well because if there is an error you'll need to return an instance of that view model anyhow, and have the values they submitted included. I usually have private methods or DB layer that handles retrieving the various lists that go into the ViewModel, since those will be empty on postback and will need to be repopulated, while not touching the properties that were in the post.
With your post method as it is now, if you need to return the same view, you've gotta create a new EditVM and then copy any posted values into it, and still populate the lists. With my method, you eliminate one of those mapping steps. If you are posting more than one thing, are you going to have umpteen different parameters on your post action? Just let them all come naturally into a single parameter typed to the EditVM of the View. While maybe having those null properties in the VM during the postback feels icky, you get a nice predictable consistency between View and postback IMO. You don't have to spend alot of time thinking about what combination of parameters on your post method will get you all the pieces of data from the form.
I have a controller with typical create methods (one for GET, one for POST). the POST takes a strongly-typed parameter:
[HttpPost] public ActionResult Create(Quiz entity)
however, when the callback is made, the properties of my entity are null... if I redefine it like this:
[HttpPost] public ActionResult Create(Quiz entity, FormCollection form)
I can see that the values are there e.g. form["Component"] contains "1". I've not had this problem in the past and I can't figure out why this class would be different.
thoughts anyone?
The easiest way to get the default model binder to instantiate Quiz for you on postback is to use the Html form helpers in you view. So, for example, if your Quiz class looked like this:
public class Quiz
{
public int Id { get; set; }
public string Name { get; set; }
}
The following code in your view would ensure the values are present on postback:
#Html.HiddenFor(mod => mod.Id)
#Html.TextBoxFor(mod => mod.Name)
Keep in mind that values which need to be posted back but not shown in the view (like identifiers) need to be added to the view with Html.HiddenFor.
Here's a more comprehensive list of Html form helper functions.
I FIGURED IT OUT!!
so, in my model (see comments on #ataddeini's thread below) you can see I have a Component... to represent components I used a couple of listboxes, the second (Components) dependent on the contents of the first (Products). In generating the second list I used
#Html.DropDownListFor(x => x.Component, ...)
which (as shown in one of the above links) generates a form field called "Component"... and therein lies the problem. What I needed to have done is bind it to the the Id of the component instead!
#Html.DropDowListFor(x => x.Component.Id, ...)
hurray!
Given the following model which has a name, url, and an arbitrary list of keywords (I want the user to add a series of keywords) ...
public class Picture
{
public Picture()
{
keywords = new List<string>();
}
public string name {get;set:}
public string url {get;set;}
public List<string> keywords{get;set;}
}
... and the following action in my controller ...
[HttpPost]
public ActionResult Edit(FormCollection fc)
{
if (ModelState.IsValid)
{
// do stuff
}
return View(ModelManager.Picture);
}
In the FormCollection I have the following field
fc["keywords"] = "keyword1,keyword2,keyword3"
And I then create a Picture object based on the form collection.
However, I would prefer to use a strongly-typed action such as
[HttpPost]
public ActionResult Edit(Picture p)
But in this approach, my p.keywords property is always empty. Is there some way to help the framework recreate my p.keywords property before it hits my controller's action method?
I thought an Editor Template might work here, but I don't think there is a way to model bind a nested IEnumerable view model member. Your fastest bet may be handling it directly with FormCollection and some string parsing magic. Otherwise, if you have to strongly-type this, maybe a custom model binder like this could help if you can control your keyword element id's:
public class PictureKeywordBinder : IModelBinder
{
public object GetValue(ControllerContext controllerContext,
string modelName, Type modelType,
ModelStateDictionary modelState)
{
Picture picture = new Picture();
//set name, url, other paramaters here
foreach(var item in Request.Form.Keys)
{
if (item.StartsWith("keyword"))
{
picture.keywords.Add(Request.Form[item]);
}
}
//add any errors to model here
return picture;
}
}
Maybe the keyword id's could be setup in a partial view passed the sub model from your parent view:
<% Html.RenderPartial("PictureKeywords", Model.keywords);
Are your keywords seperate text boxes? If so, create an inputs like this and they will be populated by the model binder.
<input name="keywords[0]" type="text">
<input name="keywords[1]" type="text">
<input name="keywords[2]" type="text">
The way I got around this, is to use a hidden input to store the csv string of items, in your case, keywords.
I then hooked into the form submit event (using jQuery) and appended the inputs to form the csv string, which is then stored in the hidden input. This hidden input was strongly typed to a property on my model.
It's a little clunky, but if you have a dynamic number of possible keywords then this works quite well (except if JS is disabled of course)
In what way you are expecting the user to add more keywords? In the form comma separated values(CSV) or by dynamically adding textboxes?
Based on your requirement, i have two solutions with me.
Short question - how do you define your view models?
Here are some of the options:
Pass the actual model into the view.
Create a view model with a reference to the model (like Model.Product)
Create a view model with the properties needed from the model, and set those from the model.
Probably a lot more.
All with their own advantages and disadvantages.
What is your experience - good and bad? And do you use the same model for GET/POST?
Thanks for your input!
Basically - it's all about separating responsibilities.
More you separate them - more verbose, complex but easier to understand it gets.
Model:
public class foo{
string Name{get;set}
Bar Bar {get;set;}
string SomethingThatIsUneccessaryInViews {get;set;}
}
public class bar{
string Name {get;set;}
}
public class fizz{
string Name{get;set;}
}
Presenter (i admit - still haven't got idea of MVP completely):
public someSpecificViewPresenter{
fizz fizz{get;set;}
foo foo{get;set;}
necessaryThingsForWhatever[] necessaryThingsForWhatever{get;set;}
public void someLogicIfNeeded(){...}
}
magic object2object mapping & flattening, viewmodel modelmetadata configuration goes here...
ViewModel (NB=>POCOS with container props only. No logic should go here.):
public class fooViewModel{
string Name {get;set;}
string BarName {get;set;}
}
public class fizzViewModel{
string Name {get;set;}
}
public class someSpecificView{
fooViewModel foo {get;set;}
fizzViewModel fizz {get;set;}
whateverViewModel whatever {get;set;}
}
and here goes "das happy ending"...
<use viewdata="someSpecificView m" />
<p>
Our foo:<br/>
${Html.DisplayFor(x=>x.foo)}
</p>
<p>
Our fizz:<br/>
${Html.DisplayFor(x=>x.fizz)}
</p>
${Html.UberPaging(m.whatever.Paging)}
And yes, i use same model for GET/POST. See this for more why/ifs.
But lately - I'm looking for other solutions. CQRS buzz catch my eye.
In my projects, it's a mix really.
If I want to display a form with details of Customer X, I just pass a DAL Customer object to my view. It's really no use to create a seperate ViewModel for it, map all its properties, and then display them. It's a waste of time imho.
Sometimes though, models are a bit more complex. They're the result of multiple queries, have some added data to them, so in these cases, I create a custom ViewModel, and add the necessary data from my model to it. In your case, it would be option 2, or sometimes 3. I prefer that over passing my model and having to add an additional 10 items in my ViewData.
I grabbed the T4 templates from SubSonic 3. These were modified and I added some new ones. I can run one of them and it generates 3 separate view models for each table. Then I can modify as needed.
Why three?
FormModel - contains on the data necessary for displaying in a form for editing or creation. Foreign keys get converted to SelectLists. DateTime fields get split into date and time components.
PostModel - this is the object returned from the Form Post. DropDownLists are posted as Int or equivalent type. Only the necessary members are in the model.
DisplayModel - used for non-editing display of the data.
I always generated these in a subfolder named Generated. As I hand tweek them I move them to the Models folder. It doesn't completely automate the process, but it generates a lot of code I would otherwise generate by hand.
I have a question regarding how to get the white- and black-listing feature of the MVC controller's UpdateModel/TryUpdateModel to work on individual properties of child objects. For example, lets say I have a questionnaire collecting details about the person filling out the form and about his or her company.
My [simplified] form fields would be named, for example:
YourName
YourEmail
Company.Name
Company.Phone
Now in my model, lets say I don't want Company.ID or Company.IsPremiumMember to be tampered with, so I'd like to exclude them from the model-binding. I have tried a combination of whitelisting, blacklisting, and both in order to get this to work. I have not had any success. Here is what I am running into:
When I explicitly include in my whitelist the same four fieldnames I wrote above, the entire Company does not get bound (i.e., questionnaire.Company is left null) unless I also include "Company" in my whitelist. But then this has the undesirable effect of binding the ENTIRE company, and not just the two properties I want.
So, I then tried to include Company.ID and Company.IsPremiumMember in my blacklist, but this seems to be trumped by the whitelist and does not filter out these properties "after the fact" I suppose.
I know that there are other ways to express the "bindability", such as via the [Bind] attribute on members, but this is not ideal as I would like to have the same model classes used in other situations with different binding rules, such as allowing an admin to set whatever properties she would like.
I expect an obvious answer is that I should write my own model binder, and I've already starting trying to look into how to perhaps do this, but I was really hoping to use an "out-of-the-box" solution for what (in my opinion) seems like a very common scenario. Another idea I'm pondering is to fabricate my own ValueProvider dictionary to hand to the UpdateModel method, but again, something I'd rather avoid if there is an easier way.
Thanks for any help!
-Mike
Addendum #1
Here are the fields I present on my form:
YourName
YourEmail
Company.Name
Company.Phone
And here is what a black hat sends my way:
YourName=Joe+Smith&YourEmail=joe#example.com&Company.Name=ACME+Corp&Company.Phone=555-555-5555&Company.CreditLimit=10000000
(be sure you notice the extra parameter tacked on there at the end!)
And here is the problem:
As I originally posted, it doesn't seem possible (using the default model binder) to prevent CreditLimit from being set---it's either the entire Company or nothing---without some big workaround. Am I wrong?
Addendum #2
I'm pretty much convinced now that the simple objective I have is not possible "out of the box." My solution has been to walk through the posted form fields and construct my own ValueProvider dictionary, thus whitelisting the fields I want to allow, and handing that to UpdateModel.
Addendum #3
I still have not yet checked out AutoMapper, but with something like that at hand, the solution of creating some ViewModels/DTOs to handle this type of complex whitelisting---plus the ability to easily attach the same server-side validation (FluentValidation) I'm already using on my domain objects---seems a viable solution. Thank you everyone!
In general, the best way to go is to create view-models, models built specifically for your views. These models are not domain objects. They are data-transfer objects, built to transfer data from your controller actions to your view templates. You can use a tool like AutoMapper painlessly to create/update a domain model object from your view-model object or to create/update a view-model object from your domain model.
I have worked around this problem by making the action accepting two objects (the parent and the child object)
Example:
Suppose we have the following model:
public class Employee
{
public string Name { get; set; }
public int Age { get; set; }
public Company Comapany { get; set; }
}
public class Company
{
public int Phone { get; set; }
}
You build your form like this:
<form action="Home/Create" method="post">
<label for="Employee.Name">Name</label>
<%=Html.TextBox("Employee.Name") %><br />
<label for="Employee.Name">Age</label>
<%=Html.TextBox("Employee.Age") %><br />
<label for="Employee.Name">Comapany Phone</label>
<%=Html.TextBox("Company.Phone") %><br />
<input type="submit" value="Send" />
</form>
Then build a "Create" action that accept two objects one of type Employee and the other of type Comapny and assign the Company object to the Employee.Company property inside the action:
public ActionResult Create(Employee employee,Company company)
{
employee.Comapany = company;
UpdateModel(employee);
return View();
}
I hope this help you.
Edit:
public ActionResult Create(Employee employee,Company company)
{
employee.Comapany = company;
UpdateModel(employee,new[] {"Name","Email","Phone"});
return View();
}
Have you tried using the following?:
public ActionResult Create([Bind(Exclude="PropertyToExclude1, PropertyToExclude2")] Employee employee)
{
//action code here
}
or use Include instead of Exclude to List what fields can be bound rather than which can't