My domain object Store holds a reference to the object Chain:
public class Store
{
public Chain Chain { get; set; }
}
On creating a new store there is the possibility to set the associated chain. I am realising this by passing the chain objects via ViewBag and use the Html.DropDownListFor for selection:
#Html.DropDownListFor(
x => x.Chain,
new SelectList(ViewBag.Chains, "Id", "Name"))
So far so good, but back in the controller (after submitting) the chain property is null. I figured that I can set the DropDownList to Chain.Id but then I need to load the entity again. Is there a better way to get/keep the correct reference?
Is there a better way to get/keep the correct reference?
No, that's how HTML works. Only the selected value is sent to the server and you need to use this value to load the corresponding entity. Please learn HTML before getting into ASP.NET MVC development. It would help you very much. That's the correct way:
#Html.DropDownListFor(
x => x.Chain.Id,
new SelectList(ViewBag.Chains, "Id", "Name")
)
Well, as a matter of a real fact, to be totally honest with you the most correct way to handle this is to get rid of all ViewBag crap and use a real view model in your application:
#Html.DropDownListFor(
x => x.ChainId,
Model.AvailableChains
)
where AvailableChains will of course be a property of type IEnumerable<SelectListItem> on the view model you prepared for this view. You should always be using a view model and never be passing your domain models to your view if you want to be doing ASP.NET MVC the right way.
And here's how your view model might look like:
public class MyViewModel
{
public int ChainId { get; set; }
public IEnumerable<SelectListItem> AvailableChains { get; set; }
}
Related
In my ViewModel (also in my Domain model), I have kinda dynamic Property Structure where the Profile Elements are a List of the base class ProfileVM and refer to a ProfileDefinitionElement (just to explain the ViewModel without pasting the full thing).
public class OwnProfileVM
{
public OwnProfileVM() {}
public ProfileDefinitionVM ProfileDefinitionVM { get; set; }
public ProfileVM ProfileVM { get; set; }
}
So I bind my Properties using a Linq Single statement:
#Model.ProfileDefinitionVM.ProfileElementDefinitions.Single(p => p.Key == ProfileElementKey.CompanyName.ToString()).Title
This works for showing data. But when posting back like this:
#Html.TextBoxFor(model => ((ProfileElementTextVM)model.ProfileVM.ProfileElements
.Single(p=> p.ProfileElementDefinition.Key == ProfileElementKey.CompanyName.ToString()))
.Text
..the model properties are null.
This is because of the parameterless constructor which builds the OwnProfileVM object without any properties filled in.
After some research I found out that there are two ways to solve this:
"Flatten" the ViewModel. So I would have a fixed Property for every Profile Element. This would work, but the disadvantage would be that I couldn't map the data with the Automapper. I would have to fill the ViewModel to the Model "manually". This would result in more Code in the Controller and a "bigger", but simpler ViewModel. Seen in this article
Find a way to pass the Definition data into the ViewModel Constructor to build the list of Properties before posting back.
Now my questions:
Is the second way even possible and if yes, how would this be done? I havent found a way to do this.
If the first question can be answered with yes, which way would you prefer?
Looks complicated. It may be best to simplify it a bit.
In my experience, model properties are null in the controller because the binder cannot understand how to link the form element name with the associated property. For example, I've seen it with lists where foreach has been used:
(model has a) List<Something> Somethings.....
foreach (Something thing in Model.Somethings)
{
#Html.EditorFor(m => thing)
}
This is rendered in the resulting html as <input name="thing"..... which is useless. The solution here is to use a for loop and access the model's properties via their path rather than copying pointers to instances, such as:
for (int i = 0; i < Model.Somethings.Count; i++)
{
#Html.EditorFor(m => Model.Somethings[i])
}
This is then rendered with the correct <input name="Model.Somethings[i]"..... and will be understood by the model binder.
I expect this issue you're facing here is similar. You need to add the necessary accessors to your properties so that the correct names and ids can be rendered in your view and picked up by the binder.
I'm not sure of the exact definition of your class so this example is not likely to be completely right.
This class includes a this[string index] method which will get and set the element using your property key as the index:
public class ProfileElements : List<ProfileElement>
{
public ProfileElement this[string index]
{
get
{
return base.First(p => p.ProfileElementDefinition.Key == index);
}
set
{
base[index] = value;
}
}
}
And in your view, you could use this like:
#Html.TextBoxFor(model => model.ProfileVM.ProfileElements[ProfileElementKey.CompanyName.ToString()].Text)
Hopefully, this will do what you need.
Maybe this is very simple to do but I can't find the good words when I search on stackoverflow or on google.
I have a model and this model contains a "Country" property that is a integer. When I am in the Edit view, this property is used this way and it work well.
#Html.DropDownListFor(model => model.Country, new SelectList(ViewBag.Countries, "IDCountry", "Name"))
In the Details view, I only want to show the name of the country, but I don't know how! Right now, I'm doing this but it only show the ID and I can't find a way to give him the List so it use it as a datasource or something like that to show "Canada" instead of 42.
#Html.DisplayFor(model => model.Country)
How can this be achieved?
How can this be achieved?
By using a view model of course:
public class CountryViewModel
{
public int CountryId { get; set; }
public string CountryName { get; set; }
}
and then you would populate this view model in the controller action that is supposed to render your Display view:
public ActionResult Display(int id)
{
Country country = ... go and fetch from your db the corresponding country from the id
// Now build a view model:
var model = new CountryViewModel();
model.CountryId = country.Id;
model.CountryName = country.Name;
// and pass the view model to the view for displaying purposes
return View(model);
}
Now your view will be strongly typed to the view model of course:
#model CountryViewModel
#Html.DisplayFor(x => x.CountryName)
So as you can see in ASP.NET MVC you should always be working with view models. Think in terms of what information you need to work with in a given view and the first thing you should do is define a view model. Then it's the responsibility of the controller action that is serving the view to populate the view model. Where the values of this view model are coming from doesn't really matter. Think of the view model as a single point of aggregation of many data sources.
As far as the views are concerned, they should be as dumb as possible. Simply work with what's available in the view model.
I'm using EF5 Code First with :
public class Scenario
{
public int Id { get; set; }
public IList<Client> Clients { get; set; }
}
public class Client
{
public int Id { get; set; }
public string Name {get;set;}
public int VisibilityNumber{ get; set; }
}
I'm directly sending the scenario object to the view (MVC4, without using a viewmodel class - maybe a mistake ?, but a lot less plumbing code). In my view, I use HiddenFor for Scenario.Id, and a for loop to display an EditFor for each client VisibilityNumber.
This is the Controller :
[HttpPost]
public ActionResult Edit(int id, FormCollection formValues)
{
if (ModelState.IsValid)
{
Scenario scen=GetScenarioFromDB(id);
TryUpdateModel(scen,formValues);
if (ModelState.IsValid)
SaveToDb(scen);
}
}
After the TryUpdateModel, for each Clients object (which were correctly loaded from DB) :
VisibilityNumber is correctly set
Id is set to 0, which of course is a bad thing
Name is set to null
After looking at the MVC Source code (DefaultModelBinder/UpdateCollection), I can see that when binding to collections, new items are always created.
If I can't fix that, I think I'm going to use a viewModel, and AutoMapper. I assume that the MVC team wanted to force us to use viewModel, rather than directly send EF object.
You should not get scenario from database in your update. Instead, you should take your bound model, attach it (if edited) or add it (if new) to context and then save changes. It's a common scenario called "disconnected entities" (which you, in fact, do have, because you have model that was disconnected when sent to client, and then got back also disconnected).
I "fixed" DefaultModelBinder/UpdateCollection so that it can work with my use case : when the binding is drilling down in the navigation properties, it uses the current object as model (it's easy, since I'm only doing modifications, no insert or delete) : I can take the DefaultModel source code, put my fix in it, and use it as a custom model binder. It's fun, but a bit dirty and over the top.
But I believe the best way is to use a specific ViewModel, using only the properties which are editable, and use AutoMap to map it to my EF hierarchy. BUT : it has the same problem of creating child objects collection.
In the end, I just did some manual mapping for between my View Model and my EF hierarchy : I'm nearly sure I can do something automatic, which could detect if a child item has been modified or inserted or deleted (since every item has a [key] property, but I just don't have the time budget to implement it.
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 the following code in my view:
<%= Html.ListBoxFor(c => c.Project.Categories,
new MultiSelectList(Model.Categories, "Id", "Name", new List<int> { 1, 2 }))%>
<%= Html.ListBox("MultiSelectList",
new MultiSelectList(Model.Categories, "Id", "Name", new List<int> { 1, 2 }))%>
The only difference is that the first helper is strongly typed (ListBoxFor), and it fails to show the selected items (1,2), even though the items appear in the list, etc. The simpler ListBox is working as expected.
I'm obviously missing something here. I can use the second approach, but this is really bugging me and I'd like to figure it out.
For reference, my model is:
public class ProjectEditModel
{
public Project Project { get; set; }
public IEnumerable<Project> Projects { get; set; }
public IEnumerable<Client> Clients { get; set; }
public IEnumerable<Category> Categories { get; set; }
public IEnumerable<Tag> Tags { get; set; }
public ProjectSlide SelectedSlide { get; set; }
}
Update
I just changed the ListBox name to Project.Categories (matching my model) and it now FAILS to select the item.
<%= Html.ListBox("Project.Categories",
new MultiSelectList(Model.Categories, "Id", "Name", new List<int> { 1, 2 }))%>
I'm obviously not understanding the magic that is happening here.
Update 2
Ok, this is purely naming, for example, this works...
<%= Html.ListBox("Project_Tags",
new MultiSelectList(Model.Tags, "Id", "Name", Model.Project.Tags.Select(t => t.Id)))%>
...because the field name is Project_Tags, not Project.Tags, in fact, anything other than Tags or Project.Tags will work. I don't get why this would cause a problem (other than that it matches the entity name), and I'm not good enough at this to be able to dig in and find out.
I've stumbled across this problem myself, finally I realized that the problem was a naming convention.
You cannot name the ViewBag or ViewData poperty containing the SelectList or MultiSelectList to the same name your property model containing the selected items. At least not if you're using the ListBoxFor or DropDownListFor helper.
Here's an example:
public class Person
{
public List<int> Cars { get; set; }
}
[HttpGet]
public ActionResult Create()
{
//wont work
ViewBag.Cars = new SelectList(carsList, "CarId", "Name");
//will work due to different name than the property.
ViewBag.CarsList = new SelectList(carsList, "CarId", "Name");
return View();
}
//View
#Html.ListBoxFor(model => model.Cars, ViewBag.CarsList as SelectList)
I'm sure theres plenty of other ways doing this, but it solved my problem, hope it will help someone!
I have also been stuck with this exact same issue and encountered the same problem with ListBox and ListBoxFor.
No matter what I do, I cannot get selections to occur on the ListBoxFor. If I change to the ListBox and name it something OTHER than the property name of the data I am binding to, selections occur.
But then because I'm not using ListBoxFor and the data is sitting inside a model class (Model.Departments) for example, I don't get model binding on the way back to my controller and hence the property is null.
EDIT I found a solution posted by someone else here;
Challenges with selecting values in ListBoxFor
Also, you can try to clear ModelState for c.Project.Categories in the controller:
[HttpPost]
public ActionResult Index(ModelType model)
{
ModelState.Remove("Project.Categories");
return View("Index", model);
}
And use the next construction:
<%= Html.ListBoxFor(c => c.Project.Categories,
new MultiSelectList(Model.Categories, "Id", "Name"))%>
Where c.Project.Categories is IEnumerable<int>.
Sorry for my english. Good luck!
The correct answer is that it doesn't work very well. As such I read the MVC code. What you need to do is implement IConvertible and also create a TypeConverter.
So, in my instance I had a Country class, such that people could choose from a list of countries. No joy in selecting it. I was expecting an object equals comparison on the selectedItems against the listitems but no, that's not how it works. Despite the fact that MultiListItem works and correctly gets the selected items, the moment it is bound to your model it's all based on checking that the string represnetation of your object instance matches the string "value" (or name if that is missing) in the list of items in the SelectItemList.
So, implement IConvertible, return the string value from ToString which would match the value in the SelectItemList. e.g in my case CountryCode was serialized into the SelectItem Value property , so in ToString IConvertible I returned CountryCode. Now it all selects correctly.
I will point out the TypeConverter is used on the way in. This time its the inverse. That Countrycode comes back in and "EN" needs converting into Country class instance. That's where the TypeConverter came in. It's also about the time I realised how difficult this approach is to use.
p.s so on your Category class you need to implement IConvertible. If its from the entity framework as my company is then you'll need to use the partial class to implement IConvertible and implement ToString and decorate it with a TypeConverter you wrote too.
Although this isn't an answer to your main question, it is worth noting that when MVC generates names it will turn something like Project.Tags into Project_Tags, replacing periods with underscores.
The reason that it does this is because a period in an element ID would look like an element named Project with a class of Tags to CSS. Clearly a bad thing, hence the translation to underscores to keep behaviour predictable.
In your first example,
<%= Html.ListBoxFor(c => c.Project.Categories,
new MultiSelectList(Model.Categories, "Id", "Name", new List<int> { 1, 2 }))%>
the listbox is attempting to bind to Model.Project.Categories for your strongly typed Model which has been provided to the page (using the lambda notation). I'm not sure what the second parameter in the ListBoxFor is doing though.
What is the Model that is being passed to the page?
Try this
<%= Html.ListBoxFor(c => c.Project.Categories,
new MultiSelectList(
Model.Categories
,"Id"
,"Name"
,Model.Project.Tags.Select(
x => new SelectListItem()
{
Selected = true,
Text = x.TEXT,
Value = x.ID.ToString()
}).ToList())
)
)%>
Html.ListboxFor and Html.Listbox work great when you're NOT binding the list box to its data source. I assume the intended use is this:
// Model
public class ListBoxEditModel
{
public IEnumerable<Category> Categories { get; set; }
public IEnumerable<Category> SelectedCategories { get; set; }
}
In the view:
#Html.ListBoxFor(m => m.SelectedCategories,
new MultiSelectList(Model.Categories, "Id", "Name"))
// or, equivalently
#Html.ListBox("SelectedCategories" ,
new MultiSelectList(Model.Categories, "Id", "Name"))
Note that in this case you don't have to explicitly say which values are selected in the MultiSelectList - the model you're binding to takes precedence, even if you do!