My target is, to modify a model in more than one view.
Since sometimes my models have many properties I want to modify them in more than one view. Something like:
first page edits 2 properties, second page edits 3 other properties,...
the model looks like this:
public class LoadViewModel
{
public int CurrentPage { get; set; } = -1;
public PageViewModel PageViewModel { get; set; }
}
public class PageViewModel
{
public string Param1 { get; set; }
public string Param2 { get; set; }
public int Param3 { get; set; }
}
my view on the Index-page looks like this:
#model LoadViewModel
#using(Ajax.BeginForm("Load", "Home", new AjaxOptions {UpdateTargetId = "page"}, new {lvm = Model}))
{
<div id="page"></div>
<input type="submit"/>
}
and this is my action:
public ActionResult Load(LoadViewModel lvm = null)
{
if (lvm == null) lvm = new LoadViewModel();
lvm.CurrentPage += 1;
TempData["CurrentPage"] = TempData["CurrentPage"] == null ? 0 : (int)TempData["CurrentPage"] + 1;
if (!partialViewDict.ContainsKey((int) TempData["CurrentPage"]))
TempData["CurrentPage"] = 0;
return PartialView(partialViewDict[(int)TempData["CurrentPage"]], lvm);
}
the pages are just partials that are mapped:
private Dictionary<int, string> partialViewDict = new Dictionary<int, string>
{
{0, "Pages/_Page1"},
{1, "Pages/_Page2"},
{2, "Pages/_Page3"},
};
and designed like this:
#using WebApplication1.Controllers
#model LoadViewModel
#{
TempData["CurrentPage"] = 0;
}
#Html.DisplayNameFor(m => m.PageViewModel.Param1)
#Html.EditorFor(m => m.PageViewModel.Param1)
this is working. When switching to Page2 the model is correctly set, but when hitting the submit the value of Param1 (that I set in Page1) is resetted to null and only the values I set in the current partial are correct.
This is Page2:
#using WebApplication1.Controllers
#model LoadViewModel
#{
TempData["CurrentPage"] = 1;
}
#Html.DisplayNameFor(m => m.PageViewModel.Param2)
#Html.EditorFor(m => m.PageViewModel.Param2)
When I add a #Html.HiddenFor(m => m.PageViewModel.Param1) into the partial, the value is still set. But I don't want the values to be resetted. I don't want to add an #Html.HiddenFor for all properties a set in a previous view. How can I prevent that the values are resetted when hitting submit without adding #Html.HiddenFor for all not listed attributes? Or is there any other possibility to catch my target?
There's two pieces to this. First, the post itself, and getting that to validate. For that, each step should have its own view model, containing only the properties it's supposed to modify. This allows you to add all the validation you need without causing other steps to fail. In the end, you'll combine the data from all of these into your entity class or whatever.
Which brings us to the second piece. You need some way of persisting the data from each step. The only data that will exist after a POST is the data that was posted and anything in the session (which includes TempData). You could always create a bunch of hidden fields to store the data from the previous steps, but that can get a little arduous. Most likely, you'll just want to use the session.
TempData is basically a specialized instance of Session, so which you use doesn't really matter. With TempData, you'll need to remember call TempData.Keep() for each of the keys you've set for each step or you'll lose the previous steps on the next request. Session will keep them around for the life of the session, but you should remember to remove the keys at the end with Session.Remove().
Do you use #using (Html.BeginForm()) in your .cshtml?
Unfortunately this is MVC. MVC is stateless, which means if you don't render it then you loose it :(
If you use model binding and scaffolding, then you can save some time and work but at the end it will be the same solution.
Related
I have a model like
public class Model
{
public int Value { get; set; }
public List<OtherModel> List { get; set; }
}
public class OtherModel
{
public int Value1 { get; set; }
public int Value2 { get; set; }
public bool IsPropTrue { get; set; }
}
I am using Model in a View where I'm looping through the List to show data in a table.
Depending on whether one of the properties (IsPropTrue) in OtherModel is true or false, I want to use the HiddenFor Html helper and send the data to the HttpPost controller.
#model Model
#foreach (var item in Model.List)
{
if (item.IsPropTrue)
{
#Html.HiddenFor(model=> item.Value1)
#Html.HiddenFor(model=> item.Value2)
}
}
I think it doesn't work because I should in some way add these properties to the OtherModel, which is inside the Model; But the way I have it now, I am adding properties to Model.
you can do it like this :
#model Model
#foreach (var item in Model.List)
{
if (item.IsPropTrue)
{
#Html.HiddenFor(model => model.List[Model.List.IndexOf(item)].Value1)
#Html.HiddenFor(model => model.List[Model.List.IndexOf(item)].Value2)
}
}
this way the binding system will bind the hidden fields with your List OtherModel in the Model
if you want send an array to server based on the Model you have to use indexer in #Html.HiddenFor .
#model WebApplication1.Models.MyModel
<form>
#if (Model != null && Model.List != null)
{
for (int i = 0; i < Model.List.Count; i++)
{
if (Model.List[i].IsPropTrue)
{
#Html.HiddenFor(model => Model.List[i].Value1)
#Html.HiddenFor(model => Model.List[i].Value2)
}
}
}
<button type="submit">submit</button>
</form>
if you want know reason of using indexer on model i recommend How does MVC 4 List Model Binding work?
Consider if it the responsibility of the view or the controller action to make the decisions - you can send everything back to the action to do the decision making.
In your Views/Shared folder, create a controller called EditorTemplates
In this folder, add a partial view called OtherModel
In this view, set the model to OtherModel and set the Layout=null
Add the three OtherModel fields in EditorFor (and HiddenFor if not displaying isPropTrue). This partial view displays just one instance of your list.
In your main view, use the above editor model like so. MVC will take care of all rendering and postback of the Model State for your complete list of items. We like one-liners...
#Html.EditorFor(model => model.OtherModel)
When the data is subsequently posted back to an action, Model State has wrapped up all of your displayed items into a list again, so you can check the isPropTrue value for each item on the server.
The only issue with MVC is that is you pass an empty list out to a view, you get a null value back, so just replace this with an empty list when null is returned
EDIT: I think I need some sort of view model, but I'm unsure how to handle this relationship.
I'm trying to understand MVC 4 and EF Code First and I'm trying to map many to many relationships.
I have two classes;
public class Asset
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public virtual ICollection<Category> Categories { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public virtual ICollection<Asset> Assets { get; set; }
}
So I'm trying to allow each Asset to have multiple Categories and each Category may have multiple Assets.
On my create method I have;
public ActionResult Create()
{
var model = new Asset();
model.Categories = _db.Categories.ToList();
return View(model);
}
In my view, the only way I can show these categories is to say; (Note the capital M in Model. I can't use the lower case model as used elsewhere in the view)
#model MyProject.Models.Asset
#using (Html.BeginForm("Create", "Assets", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<div>
#foreach (var item in Model.Categories)
{
<p>#item.CategoryName</p>
}
</div>
<div class="form-group">
#Html.LabelFor(model => model.Title, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Title, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Title, "", new { #class = "text-danger" })
</div>
</div>
}
When the initial create is called, I can see my asset and it has categories. On the return create method however, its null. I can't work out why. I understand I'm not doing anything to edit these categories in the View, I can't get that far. What I don't understand though is why my model leaves with categories, but comes back with none.
My create return (here my assets categories is null)
// POST: Assets/Create
[HttpPost]
public ActionResult Create(Asset model)
{
if (!ModelState.IsValid)
{
//error, return to view.
return View();
}
try
{
//do stuff
}
catch
{
return View();
}
}
Ultimately when creating an Asset, I want to be able to list all the categories and allow some selection as to which categories this new asset will belong. If someone could help me work that out, you're my hero. But if I could just understand why what's coming back isn't what I sent out, that would be a start.
In my view, the only way I can show these categories is to say; (Note the capital M in Model. I can't use the lower case model as used elsewhere in the view)
I've always hated that Microsoft uses the model => model.* convention through its generated views and tutorials and articles online; it only leads to confusion. In your view Model is an actual object instance, namely an instance of what you defined as the "model" for the view. The lowercase model you see used in things like Html.EditorFor is actually a parameter to a lambda expression. It can be called anything. For example, Html.EditorFor(x => x.Foo) and even Html.EditorFor(supercalifragilisticexpialidocious => supercalifragilisticexpialidocious.Foo) would work just as well. Although the value that gets passed into this parameter is usually the Model object instance, Model and model are totally different concepts.
When the initial create is called, I can see my asset and it has categories. On the return create method however, its null. I can't work out why. I understand I'm not doing anything to edit these categories in the View, I can't get that far. What I don't understand though is why my model leaves with categories, but comes back with none.
That is why. You're not doing anything to edit these categories in the view. There's no fields for them to be posted along with the form data, and as a result, the class instantiated by the modelbinder in your action does not contain any data for categories. This is key. The class instance that goes into the view is not the same class instance that comes back after something like a post. Each is a unique thing. The post action has no knowledge of anything that came before it; it simply has whatever data was posted. Assuming the action takes a parameter of a particular class, the modelbinder will attempt to new up an instance of that class and bind the posted data to appropriate properties on that class. It doesn't care what was sent to the view originally; it doesn't even care what class it's working with.
Ultimately when creating an Asset, I want to be able to list all the categories and allow some selection as to which categories this new asset will belong. If someone could help me work that out, you're my hero. But if I could just understand why what's coming back isn't what I sent out, that would be a start.
This is the fun part. First, you absolutely must use a view model for this. In case you're not familiar with view models, they're simply classes that are used as a model for a view, hence the name. What you're passing around here, Asset, is technically an "entity", which is a class that is used for data transfer, usually to/from a database. While an entity could be used as a model for a view, as you've done here, it's not really suited for that.
There's a clear conflict of interest, as the needs of a class representing some table schema in a database are vastly different from the needs of a class representing data for a UI layer. That's where view models come in. In the most traditional sense, a view model simply represents the data that will need to be displayed and/or edited in one or more views. It may have many properties in common with a particular entity or it may only have a subset of those properties or even completely different properties. It is the job of your application to "map" from your entity to your view model and vice-versa, so that the logic for saving an entity to a persistence store can be completely abstracted from the logic for how the user interacts with that data.
The reason a view model is so important for your purposes here is that form elements in HTML have certain limitations. They can only work with data that can be represented as a string: things like ints, bools, actual strings, etc. They are particularly unsuited for working with complex objects, like your Category class. In other words, it would be perfectly achievable to post back a list of integer ids, representing Categorys, but it is entirely implausible to post back complete Category instances that have been chosen by a user.
Since your entity expects a list of categories and your view will only feasibly be capable of posting a list of ints, there's a fundamental disconnect. Using a view model provides a way to bridge the gap. Plus, it allows you to have other properties, like a list of category choices to populate your select list with, that would be totally inappropriate to put on your entity class.
For your scenario, you'd need a view model like:
public class AssetViewModel
{
// any other asset properties you need to edit
public List<int> SelectedCategoryIds { get; set; }
public IEnumerable<SelectListItem> CategoryChoices { get; set; }
}
This then allows you to create a multiselect list in your view using:
#Html.ListBoxFor(m => m.SelectedCategoryIds, Model.CategoryChoices)
Now, to populate your view model with data from your entity. In a create view, the entity doesn't exist yet, so you don't need to do any mapping. The only thing you need to do is populate your CategoryChoices property so the select list in the view has some data. However, based on the above discussion about data needing to be posted back or else it will be null, since the actual contents of the select list will never be posted, you'll need to populate this in each of your create and edit actions, both for GET and POST. As a result, it's best to factor this logic out into a private method on your controller that each action can call:
private void PopulateCategoryChoices(AssetViewModel model)
{
model.CategoryChoices = db.Categories.Select(m => new SelectListItem
{
Value = m.Id,
Text = m.Name
};
}
Then, in your create GET action, you'll just new up your view model and populate your category choices:
public ActionResult Create()
{
var model = new AssetViewModel();
PopulateCategoryChoices(model);
return View(model);
}
In the post version, you'll now need to map the posted data onto your Asset entity:
[HttpPost]
public ActionResult Create(AssetViewModel model)
{
if (ModelState.IsValid)
{
var asset = new Asset
{
Title = model.Title,
Description = model.Description,
// etc.
Categories = db.Categories.Where(m => model.SelectedCategoryIds.Contains(m.Id))
}
db.Assets.Add(asset);
db.SaveChanges();
return RedirectToAction("Index");
}
PopulateCategoryChoices(model);
return View(model);
}
The edit GET action is similar to the create version, only this time, you have an existing entity that will need to be mapped onto an instance of your view model:
var asset = db.Assets.Find(id);
if (asset == null)
{
return new HttpNotFoundResult();
}
var model = new AssetViewModel
{
Title = asset.Title,
Description = asset.Description,
// etc.
SelectedCategoryIds = asset.Categories.Select(m => m.Id).ToList()
};
Likewise, the edit POST action is similar to the create version, but you're going to map from your view model on to an existing asset instead of creating a new asset. Additionally, because you have a many to many relationship, you have to take extra care when saving the categories.
// map data
asset.Title = model.Title;
asset.Description = model.Description;
//etc.
// You might be tempted to do the following:
// asset.Categories = db.Categories.Where(m => model.SelectedCategoryIds.Contains(m.Id));
// Instead you must first, remove any categories that the user deselected:
asset.Categories.Where(m => !model.SelectedCategoryIds.Contains(m.Id))
.ToList().ForEach(m => asset.Categories.Remove(m));
// Then you need to add any newly selected categories
var existingCategories = asset.Categories.Select(m => m.Id).ToList();
db.Categories.Where(m => model.SelectedCategoryIds.Except(existingCategories).Contains(m.Id))
.ToList().ForEach(m => asset.Categories.Add(m));
The extra footwork here is necessary to prevent saving the same relationship twice, resulting in an integrity error. By default Entity Framework creates a join table for many to many relationships that consists of a composite primary key composed of the foreign keys to each side of the relationship.
The reason why your categories is null is because you are not binding it on the POST. They are not in fields during the POST.
Try this and see if they are filled out:
#for (int i = 0; i < Model.Categories; i++)
{
#Html.TextBoxFor(model => model.Categories[i].CategoryId)
#Html.TextBoxFor(model => model.Categories[i].CategoryName)
}
1.
return View();
you are not passing back the model in your Create Method that is why you don't see the Model is NULL.
// POST: Assets/Create
[HttpPost]
public ActionResult Create(Asset model)
{
if (!ModelState.IsValid)
{
//error, return to view.
return View(model);
// If you don't pass back the model to you view you will see model is NULL
}
try
{
//do stuff
}
catch
{
return View(model);
}
}
Cateogries Will always be null in your case, as you can't post back a List like you are doing in your case.
Try Displaying them in a loop, Then the MVC model Binder will be able to bind your list to Model:
#for (int i = 0; i < Model.Categories; i++)
{
#Html.HiddenFor(model => model.Categories[i].CategoryId)
}
If you want to save SelectedCategories you will have to use MultiSelect
I'm just picking up .net MVC and I've come across something that I can't work out. I'm obviously missing some basic principle but would love some help.
I have a ViewModel with two IEnumerables that I want to use to create dropdownlistfors. My GET works fine, the lists are populated as expected.
Now I'm posting the ViewModel back to a POST method, not to do anything useful but just to try and understand how mvc works. I expected that I would simply be able to re-populate the dropdownlistfors from the model that was posted back - but I get a null reference exception.
Other values, such as partyid, in the ViewModel survive the POST so i'm confused.
I can get it to work if I repopulate the lists but that seems wrong.
Can someone give me a pointer?
My ViewModel
public class DemoViewModel
{
//properties
public IEnumerable<tbl_server_lookup> servers { get; set; }
public int serverId { get; set; }
public IEnumerable<tbl_site_lookup> sites { get; set; }
public int siteId { get; set; }
public int partyid { get; set; }
public string message { get; set; }
public DemoViewModel()
{
}
}
My Controller
// GET: /Demos/Test/
[HttpGet]
public ActionResult Test()
{
DemoViewModel demo = new DemoViewModel();
using (var dbContext = new ADAPI.Models.db_ad_apiEntities2())
{
var serverList = dbContext.tbl_server_lookup.Where(s => s.server_name != null);
demo.servers = serverList.ToList();
var siteList = dbContext.tbl_site_lookup.Where(w => w.site_name != null);
demo.sites = siteList.ToList();
}
demo.message = "Enter the user id you would like to look up in the box below.";
return View(demo);
}
//
//POST: /Demos/Test/
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Test(DemoViewModel demo)
{
//It works if I uncomment this block...
/*using (var dbContext = new ADAPI.Models.db_ad_apiEntities2())
{
var myQuery = dbContext.tbl_server_lookup.Where(s => s.server_name != null);
demo.servers = myQuery.ToList();
var siteList = dbContext.tbl_site_lookup.Where(w => w.site_name != null);
demo.sites = siteList.ToList();
}*/
demo.message = "the user id you posted is: " + demo.partyid + ". The Server you selected is: ";// +demo.serverId;
return View(demo);
}
My View
#model ADAPI.ViewModels.DemoViewModel
<h2>Demos</h2>
<h3>#Model.message</h3>
#using (Html.BeginForm("Test","Demos"))
{
#Html.AntiForgeryToken()
<div class="">
<h4>Party ID</h4>
#Html.ValidationSummary(true)
<!-- input box for party id-->
#Html.TextBoxFor(model => model.partyid)
<!-- dropdown list of server types eg live vs test-->
#Html.DropDownListFor(model => model.serverId, new SelectList(Model.servers, "server_Id","server_name"))
#Html.DropDownListFor(model => model.siteId, new SelectList(Model.sites, "site_short_name","site_name"))
<input type="submit" value="Try" />
</div>
}
The Error
In MVC, model on the views are loaded in the controller action, they are not posted back along with the post action.
If you are used to ASPX's viewstate, there is no such thing in MVC, you need to load what you need for every view in the current action.
Dropdown lists are rendered into html as tag and returned to the server as plain single value.
You have to rebind/repopulate them on the server, wchich is annoying in scenarios like validation, where the same model should be returned to the client.
There is no support for that in the framework - you have to do it on your own.
One more thing - if you absolutely have to return the list items and want them back on the server, you can serialize tham and hide in some hidden field. But it's ugly and unsecure since anyone can change its value.
Hi I have simple model which looks like this:
public class HomeModel
{
public HomeModel()
{
Buildings = new List<Building>();
}
public List<Building> Buildings { get; set; }
public int SelectedBuildingId { get; set; }
}
and on view i display combo like this:
#Html.DropDownListFor(model => model.SelectedBuildingId, new SelectList(Model.Buildings, "Id", "Name"), "Choose Building... ")
and now when I click submit button, then the buildings list dissapears
so I tryid to keep it with hidden field
#Html.HiddenFor(model => model.Buildings)
but it doesn't work, any help ?
#Html.HiddenFor(model => model.Buildings) -> this is working just for primitive types like int string and so on.
I will not recommend you to serialize a list into the view but if this is necessary for you,you can do it like this:
#for (int i = 0; i < Model.Buildings.Count; i++)
{
#Html.HiddenFor(c => c.Buildings[i])
}
And now after post you will have full list in your view.
If your view is strongly typed to your HomeModel, then you can return your model in your postback event and return the model again to your view:
[HttpPost]
public ActionResult GetHomePostResult(HomeModel model)
{
//do whatever with your model data
return View("YourHomeGetView", model);
}
You don't need the list again... just load it again in the Post action.
EDIT:
If you insist in having it posted along with the important data. You should serialize that list to a string, and then rebuild the list in the server after the post.
Remember you are in MVC not plain ASP.NET... so each action is somehow atomic. For the case of dropdownlists, this is the normal behavior: reload the list in the server (independently from where you load it).
1)
and now when I click submit button, then the buildings list dissapears so I tryid to keep it with hidden field
#Html.HiddenFor(model => model.Buildings)
but it doesn't work, any help ?
Just Inspect element and see what is the name rendered for this hidden field, Make sure you have the same name as in your Model (only then the values get binded). If you see a different name in element, change the syntax to
#Html.HiddenFor(model => model.Buildings, name {Name = Buildings })
and on post back the values will be binded in the model
**OR**
2) If you are retrieving the values from DB using some Id or something. Pass this Id to the view. And keep this in hidden field. On post back catch this value in controller with proper parameter name (Name must be same as the hidden field element in view). Use this Id to retrieve the DropDown again and continue...
I am working on ASP.NET MVC 4 application. I use EF 5 and Code First. I have two entities with 1:N relation:
public class Menu
{
//some properties
public virtual ICollection<Document> Documents { get; set; }
}
and:
public class Document
{
//some properties..
public int MenuID { get; set; }
public virtual Menu Menu { get; set; }
}
I have Edit view with [HttpPost] and [HttpGet] methods. When I pass the model through the GET action like this :
Menu model = unitOfWork.MenuRepository.GetById(Id);
if (model != null)
{
return View(model);
}
everything is right, I can see that model contains 1 Documents but then in my razor view if I simply try:
#Html.HiddenFor(m => m.Documents)
then when I submit the form to the Post action I can see that the Documents property is null.
How can keep Documents persistent?
Anything with a collection will not get rendered as you are expecting. you need to create say, a displaytemplate which expects a collection and then render our the properties of the documents in a HiddenFor or if you dont want a display template then do the same but on the view in question.
for example, this is what you need to do:
#for(int counter = 0; counter < Model.Documents; counter++)
{
#Html.HiddenFor(m => Model.Documents[counter].Id)
#Html.HiddenFor(m => Model.Documents[counter].Title)
// and so on
}
so now when you postback, it has all the properties it needs for the engine to bind to and pass it to your controller method and serve it up.
Have you looked through the html, that is generated ? Take a look )
This answer and this one provide workarounds.