ASP MVC3 - passing collection item into partial view - asp.net-mvc

I have a view model that I've created with a collection (a List) of a separate model. In our database, we have two tables: BankListMaster and BankListAgentId. The primary key of the "Master" table serves as the foreign key for the Agent Id table.
As the Master/Agent ID tables have a one-to-many relationship, the view model I've created contains a List object of BankListAgentId's. On our edit page, I want to be able to both display any and all agent Ids associated with the particular bank, as well as give the user the ability to add or remove them.
I'm currently working through Steve Sanderson's blog post about editing a variable length list. However, it doesn't seem to cover this particular scenario when pulling existing items from the database.
My question is can you pass a specific collection item to a partial view, and if so how would you code that into the partial view correctly? The following code below states that
The name 'item' does not exist in the current context
However, I've also tried using a regular for loop with an index and this syntax in the partial view:
model => model.Fixed[i].AgentId
but that just tells me the name i does not exist in the current context. The view will not render using either method.
Below is the code from the view
#model Monet.ViewModel.BankListViewModel
#using (Html.BeginForm())
{
<fieldset>
<legend>Stat(s) Fixed</legend>
<table>
<th>State Code</th>
<th>Agent ID</th>
<th></th>
#foreach(var item in Model.Fixed)
{
#Html.Partial("FixedPartialView", item)
}
</table>
</fieldset>
}
Here is the partial view
#model Monet.ViewModel.BankListViewModel
<td>
#Html.DropDownListFor(item.StateCode,
(SelectList)ViewBag.StateCodeList, item.StateCode)
</td>
<td>
#Html.EditorFor(item.AgentId)
#Html.ValidationMessageFor(model => model.Fixed[i].AgentId)
<br />
Delete
</td>
And here is the view model. It currently initialized the Fixed/Variable agent Id lists to 10, however that is just a work-around to get this page up and running. In the end the hope is to allow the lists to be as large or small as needed.
public class BankListViewModel
{
public int ID { get; set; }
public string BankName { get; set; }
public string LastChangeOperator { get; set; }
public Nullable<System.DateTime> LastChangeDate { get; set; }
public List<BankListAgentId> Fixed { get; set; }
public List<BankListAgentId> Variable { get; set; }
public List<BankListAttachments> Attachments { get; set; }
public BankListViewModel()
{
//Initialize Fixed and Variable stat Lists
Fixed = new List<BankListAgentId>();
Variable = new List<BankListAgentId>();
Models.BankListAgentId agentId = new BankListAgentId();
for (int i = 0; i < 5; i++)
{
Fixed.Add(agentId);
Variable.Add(agentId);
}
//Initialize attachment Lists
Attachments = new List<BankListAttachments>();
Attachments.Add(new BankListAttachments());
}
}

The problem is with your partial view. In your main view, in the loop, you're passing a BankListAgentId object. However, your partial view's Model type is #model Monet.ViewModel.BankListViewModel.
Furthermore, you are trying to access a variable called item in your partial view, when none exist. Instead of using item to access your data, use Model like in any other view. Each view (even a partial one) has it's own Model type.
Your partial view should look something like this:
#model Monet.ViewModel.BankListAgentId
<td>
#Html.DropDownListFor(model => model.StateCode,
(SelectList)ViewBag.StateCodeList, Model.StateCode)
</td>
<td>
#Html.EditorFor(model => model.AgentId)
#Html.ValidationMessageFor(model => model.AgentId)
<br />
Delete
</td>

The model you are passing into the Partial View is a BankListAgentId--- because you are looping over a collection of them when you are creating the partial view.

You're doing everything right so far. You're looping through your list, call the partial for each list item and passing the item into the partial. It seems that the part you're missing is that when you pass the item into the partial, the item becomes the model for the partial. So you interact with it like you would in any other views, i.e. #Model.BankName, #Html.DisplayFor(m => m.BankName), etc.

Related

Model Not Submitting / Binding Correctly on POST

Scenario:
I have a table with rows that are populated from a ViewModel. There are checkboxes for each row that allow the user to check 1 or more of the rows and then choose from actions in a dropdown menu to make edits to properties on the selected rows.
Everything works fine to this point, and I can get the ViewModel to pass correctly and then use it and all it's properties in a POST Action method. I could make the changes based on the option the user picked.
However, since some of the options in the dropdown would make fairly substantial and irreversible changes, I am calling a new View with a GET and populating a new table with just the selected rows, and asking the user to confirm they want to make the changes. Everything is still good up to this point. The new View populates as expected with only the rows that were selected in the previous View.
Problem:
After the user confirms their intent, an Action method is called with POST. The ViewModel that correctly populated the current View is making its way into the controller correctly. I get the ViewModel, but not with the same properties as the one that populated the View.
ViewModel
public class ProjectIndexViewModel
{
public List<ProjectDetailsViewModel> Projects { get; set; }
public string FlagFormEditProjects { get; set; }
public string FlagFormNewProjectStatus { get; set; }
}
The List<ProjectDetailsViewModel> Projects is what is used to populate the rows in the table, and Projects are what are not binding correctly in the POST Action methods in the controller.
Initial View where the checkboxes are selected. Note the example of one of the javascript functions that is called when one of the dropdown options is selected, which is what submits the form.
#using (Html.BeginForm("EditProjectsTable", "Project", FormMethod.Get, new { name = "formEditProjects", id = "formEditProjects" }))
{
#Html.HiddenFor(item => item.FlagFormEditProjects)
#Html.HiddenFor(item => item.FlagFormNewProjectStatus)
....
<table>
<thead>
....
</thead>
<tbody>
#for (int i = 0; i < Model.Projects.Count; i++)
{
<tr>
<td>#Html.DisplayFor(x => x.Projects[i].ProjectNumber)</td>
<td>#Html.DisplayFor(x => x.Projects[i].ProjectWorkType)</td>
.... // more display properties
<td>
#Html.CheckBoxFor(x => x.Projects[i].Selected, new { #class = "big-checkbox" })
#Html.HiddenFor(x => x.Projects[i].ProjectModelId)
</td>
</tr>
}
</tbody>
</table>
}
function submitFormRemoveProjects() {
$("#FlagFormEditProjects").attr({
"value": "RemoveProjects"
});
$('#formEditProjects').submit();
}
Action method that returns the "confirmation" View (works fine)
[HttpGet]
[Authorize(Roles = "Sys Admin, Account Admin, User")]
public async Task<ActionResult> EditProjectsTable([Bind(Include = "Projects,FlagFormEditProjects,FlagformNewProjectStatus")]ProjectIndexViewModel projectIndexViewModel)
{
// Repopulate the Projects collection of ProjectIndexViewModel to
// include only those that have been selected
return View(projectIndexViewModel);
}
View that is returned from Action method above (works fine) Note that the Action method that gets called is set dynamically with the actionName variable in the Html.BeginForm call.
#using (Html.BeginForm(actionName, "Project", FormMethod.Post))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model.FlagFormNewProjectStatus)
....
<table>
<thead>
....
</thead>
<tbody>
#for (int i = 0; i < Model.Projects.Count; i++)
{
<tr>
<td>#Html.HiddenFor(x => x.Projects[i].ProjectModelId)</td>
<td>#Html.DisplayFor(x => x.Projects[i].ProjectNumber)</td>
<td>#Html.DisplayFor(x => x.Projects[i].ProjectWorkType)</td>
.... // more display properties
</tr>
}
</tbody>
</table>
<input type="submit" value="Delete Permanently" />
}
An example of one of the Controller Action methods that is called from this View, and that does not have the same Project that was in the View. Somehow, it has the same number of Projects that were originally selected, but if only one was selected, it has the Project with the lowest Model Id. I'm not sure how else to describe what's happening. But in summary, the correct ViewModel is not making it's way into the POST method example shown below.
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Sys Admin, Account Admin")]
public async Task<ActionResult> DeleteConfirmedMultipleProjects([Bind(Include = "Projects")] ProjectIndexViewModel projectIndexViewModel)
{
if (ModelState.IsValid)
{
// Remove Projects from db and save changes
return RedirectToAction("../Project/Index");
}
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Please help!
The issue is that when you submit to the EditProjectsTable() method from the first view, the values of all form controls are added to ModelState.
Repopulating your collection of ProjectDetailsViewModel does not update ModelState, and when you return the view, the DisplayFor() methods will display the correct values because DisplayFor() uses the values of the model, however your
#Html.HiddenFor(x => x.Projects[i].ProjectModelId)
will use the values from ModelState, as do all the HtmlHelper methods that generate form controls (except PasswordFor()).
One way to solve this is to call ModelState.Clear() before you return the view in the EditProjectsTable() method. The HiddenFor() method will now use the value of the model because there is no ModelState value.
[HttpGet]
[Authorize(Roles = "Sys Admin, Account Admin, User")]
public async Task<ActionResult> EditProjectsTable(ProjectIndexViewModel projectIndexViewModel)
{
// Repopulate the Projects collection of ProjectIndexViewModel to
// include only those that have been selected
ModelState.Clear(); // add this
return View(projectIndexViewModel);
}
For a explanation of why this is the default behavior, refer the second part of this answer.
Side note: Your using a view model, so there is no point including a [Bind] attribute in your methods.
I think your problems comed from this part:
#Html.CheckBoxFor(x => x.Projects[i].Selected, new { #class = "big-checkbox" })
#Html.HiddenFor(x => x.Projects[i].ProjectModelId)
I had this error before and what I did is adding a Boolean property to ProjectDetailsViewModel like IsSelected.
then you should have :
#Html.CheckBoxFor(x => x.Projects[i].IsSelected, new { #class = "big-checkbox" })
Then on method you should add:
foreach (var project in ProjectIndexViewModel.Projects )
{
if (project.IsSelected==true)
"put your logic here"
}

Model send from PartialView to controller

I start work in asp.net-mvc and I have problem to send model from partialview to controller.
So first this is the way I create partialview
#Html.Partial("Weather", ShopB2B.Controllers.HomeController.GetWeather())
GetWeather() is controller metod that initializes first data to model. Model looks like this
public class Weather_m
{
public IEnumerable<SelectListItem> City_dropdown { get; set; }
public string Temperature { get; set; }
}
It is nesesery to DropDownListFor, and partialview looks like this
#model ShopB2B.Models.Weather_m
#using (#Html.BeginForm("GetWeatherNew", "Home", new { weather = Model }, FormMethod.Post))
{
<table>
<tr>
<td>#Html.DropDownListFor(x => x.City_dropdown, Model.Miasta_dropdown)</td>
</tr>
<tr>
<td>#Html.LabelFor(x => x.Temperature, Model.Temperatura)</td>
<td><<input type="submit" value="Send" class="submitLink" style=" height: 40px;" /></td>
</tr>
</table>
}
And here is problem because I want send this model to controller and then check which field is selected, add something, and send this model again to partialview. Any idea, how to do it?????
You really should not be getting the data for your ViewModel type on view rendering.
Your data is type of ShopB2B.Models.Weather_m. Your strongly typed partial view expects this, this is all good. But instead of getting your ShopB2B.Models.Weather_m instentiated with ShopB2B.Controllers.HomeController.GetWeather(), you should create a ViewModel and return it to your strongly typed view, say MyViewModel. This should wrap an instance of ShopB2B.Models.Weather_m. So in your main view, your view would be strongly typed for:
#model ShopB2B.Models.MyViewModel
and you render your partial view like
#Html.Partial("Weather", Model.MyWeather_m)
I usually wrap the partial view inside the form as well, like:
#using (#Html.BeginForm("GetWeatherNew", "Home", new { weather = Model }, FormMethod.Post))
{
#Html.Partial("Weather", Model.MyWeather_m)
}
Hope this helps.
You should define the property bind to Dropdown appropriately. Since, you have defined the city_dropdown defined as IEnumarable so model binding will fails while sending from data to server due to data type mismatch (at client side City_dropdown will be generated as string for select control). In this case, you should change the property of Model as follows.
public class Weather_m
{
public string City_dropdown { get; set; }
public string Temperature { get; set; }
}
And
#Html.DropDownListFor(x => x.City_dropdown, Model.Miasta_dropdown)

Implementing pagin / sorting / filtering in View with POST Form

I have an application in MVC 4 / C# / Visual Studio 2010 SP1. It is to create UI for storing collections of books. The information I want to store are: a name of a collection, a date on creation, and the list of books. A number of books is to be added from the database that stores all the books. Actually, another view is to edit books themselves.
So far, I have designed my View such that it shows form fields for name of collection and date on creation. But underneath I included list of all books to be selected.
Selecting them in the edit / create view means they are added to collection. I thought I could implement paging / sorting / filtering for the list of books as the number may become too large to show it on one page. My idea is to add PartialView with a list of books. The PartialView can be invoked by jQuery by .post() that is trggered by events like clicking on a page number, table column etc. The PartialView would store a page no., a sort criterium, filter criteria in some hidden fields and based on their values it would generate portion of the list. Hidden fields would be updated from the model but would also pass paging / sorting back to action.
I run into problem of how to put everything together in POST form. I would like a user to click page numbers while the previously selected books would still be selected. I don't know how to refresh a PartialView and keep books' state. I hope it is possible. If not, what would you recommend?
Thanks
Below is my application.
The model of a book:
// Entity
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public DateTime DatePublished { get; set; }
}
ViewModels:
// BookToSelect view model
public class BookToSelect : Book
{
public bool Isselected { get; set; }
public static IList<BookToSelect> MapBooksToBooksToSelect(IList<Book> list, bool isselected = false)
{
return list.Select(x => new BookToSelect() { //...})
}
public static IList<Book> MapBooksToSelectToBooks(IList<BookToSelect> list)
{
return list.Select(x => new Book() { //... })
}
}
// List of books view model
public class ListOfBooks
{
public IList<BookToSelect> Books { get; set; }
public DateTime DayOnCreationThe { get; set; }
public string CollectionName { get; set; }
public static IList<Book> GetListOfBooks()
{
return new List<Book>() {
// ... set of new Books() { },
};
}
}
Controller / Action:
public class TestCollectionController : Controller
{
[HttpGet, ActionName("Edit")]
public ActionResult Edit_GET()
{
ListOfBooks ViewModel = new ListOfBooks();
ViewModel.Books = ListOfBooks.GetListOfBooksToSelect();
ViewModel.DayOnCreation = DateTime.Today;
ViewModel.CollectionName = "List of random books";
return View(ViewModel);
}
[HttpPost, ActionName("Edit")]
public ActionResult Edit_POST(ListOfBooks ViewModel)
{
return View(ViewModel);
}
}
and View:
#using MvcDbContext.ViewModels
#model ListOfBooks
#{
ViewBag.Title = Model.CollectionName;
}
<h2>#Model.CollectionName</h2>
#using (Html.BeginForm())
{
#Html.EditorFor(m => m.CollectionName)
#Html.EditorFor(m => m.DayOnCreation)
<table>
<tr>
<th class="display-label">#Html.DisplayNameFor(m => m.Books.FirstOrDefault().Isselected)</th>
<th class="display-label">#Html.DisplayNameFor(m => m.Books.FirstOrDefault().Title)</th>
<th class="display-label">#Html.DisplayNameFor(m => m.Books.FirstOrDefault().Author)</th>
<th class="display-label">#Html.DisplayNameFor(m => m.Books.FirstOrDefault().DatePublished)</th>
</tr>
#for (int i = 0; i < Model.Books.Count(); i++)
{
<tr>
#Html.EditorFor(m => m.Books[i])
</tr>
}
<tr>
<td colspan="3"><input type="submit" name="SaveButton" value="Save" /></td>
</tr>
</table>
}
As you've already determined, if you switch out the HTML with the next page, all the inputs, included their state, are replaced as well. As a result, the only way to handle this is to offload the state into an input outside of the replacement.
The simplest way to handle this would most likely be creating a hidden input that will consist of a comma-delimited string of ids of selected items. Just add some JS that will watch the checkboxes or whatever and add or remove items from this hidden input. You can then just post this string back and use Split to turn it into a list of ids that you can use to query the appropriate books and add them to the collection on the entity.

PartialView displays property names instead of property values

I'm learning MVC, (so please do not get mad if this is very easy problem) and trying to use PartialView that will be commonly used among different pages to display some constant information, but it seems to me that I'm doing something wrong here.
This is hierarchy of my app:
Controller
-AccountController.cs
Models
-CompanyModel.cs
Views
Shared
-Company.cshtml
I have a Login view, which is the first that gets loaded.
In Login action I initialize the Model to be used and referenced in my PartialView.
Here is the Login action defined in my AccountController
public ActionResult Login()
{
StringBuilder sb = new StringBuilder();
CompanyModel cm = new CompanyModel();
cm.CompanyName = ConfigurationManager.AppSettings["CompanyName"];
cm.AppName = " - " + ConfigurationManager.AppSettings["AppName"];
cm.AppVersion = " " + ConfigurationManager.AppSettings["AppVersion"];
sb.Append("Web Security Administration Portal");
ViewBag.Message = sb.ToString();
return View();
}
This is the model:
public class CompanyModel
{
public string CompanyName { get; set; }
public string AppName { get; set; }
public string AppVersion { get; set; }
}
This is the partial view named "Company":
#model WebSecurityAdmin.Models.CompanyModel
<table>
<tr>
<td>#Html.DisplayNameFor(model => model.CompanyName)
#Html.DisplayNameFor(model => model.AppName)
#Html.DisplayNameFor(model => model.AppVersion)
</td>
</tr>
</table>
This is one of the views, let's say Default view where I want to embed my partial view:
#section featured {
<section class="featured">
<div class="content-wrapper">
<hgroup class="title">
<h1>#Html.Partial("Company")</h1>
</hgroup>
</div>
</section>
}
When I display Default view I get property names of CompanyModel displayed instead of their values:
CompanyName AppName AppVersion
Why is that and please explain what I'm doing wrong.
#Html.DisplayNameFor() is what's wrong. That displays either the name of the property, or the name you've given it through an attribute. Instead of that, just have your row like this:
<td>
#model.CompanyName
#model.AppName
#model.AppVersion
</td>
Edited: Adding # before model
Another option is to use the helper #Html.DisplayFor(m => m.CompanyName) as depending on the type of property, especially ones more complex than a string or number, will utilize display templates and add large blocks of HTML in a very succinct manner that keeps your Razor view more readable.

ASP.net MVC - Display Template for a collection

I have the following model in MVC:
public class ParentModel
{
public string Property1 { get; set; }
public string Property2 { get; set; }
public IEnumerable<ChildModel> Children { get; set; }
}
When I want to display all of the children for the parent model I can do:
#Html.DisplayFor(m => m.Children)
I can then create a ChildModel.cshtml display template and the DisplayFor will automatically iterate over the list.
What if I want to create a custom template for IEnumerable?
#model IEnumerable<ChildModel>
<table>
<tr>
<th>Property 1</th>
<th>Property 2</th>
</tr>
...
</table>
How can I create a Display Template that has a model type of IEnumerable<ChildModel> and then call #Html.DisplayFor(m => m.Children) without it complaining about the model type being wrong?
Like this:
#Html.DisplayFor(m => m.Children, "YourTemplateName")
or like this:
[UIHint("YourTemplateName")]
public IEnumerable<ChildModel> Children { get; set; }
where obviously you would have ~/Views/Shared/DisplayTemplates/YourTemplateName.cshtml:
#model IEnumerable<ChildModel>
<table>
<tr>
<th>Property 1</th>
<th>Property 2</th>
</tr>
...
</table>
This is in reply to Maslow's comment. This is my first ever contribution to SO, so I don't have enough reputation to comment - hence the reply as an answer.
You can set the 'TemplateHint' property in the ModelMetadataProvider. This would auto hookup any IEnumerable to a template you specify. I just tried it in my project. Code below -
protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor)
{
var metaData = base.CreateMetadataFromPrototype(prototype, modelAccessor);
var type = metaData.ModelType;
if (type.IsEnum)
{
metaData.TemplateHint = "Enum";
}
else if (type.IsAssignableFrom(typeof(IEnumerable<object>)))
{
metaData.TemplateHint = "Collection";
}
return metaData;
}
You basically override the 'CreateMetadataFromPrototype' method of the 'CachedDataAnnotationsModelMetadataProvider' and register your derived type as the preferred ModelMetadataProvider.
In your template, you cannot directly access the ModelMetadata of the elements in your collection. I used the following code to access the ModelMetadata for the elements in my collection -
#model IEnumerable<object>
#{
var modelType = Model.GetType().GenericTypeArguments[0];
var modelMetaData = ModelMetadataProviders.Current.GetMetadataForType(null, modelType.UnderlyingSystemType);
var propertiesToShow = modelMetaData.Properties.Where(p => p.ShowForDisplay);
var propertiesOfModel = modelType.GetProperties();
var tableData = propertiesOfModel.Zip(propertiesToShow, (columnName, columnValue) => new { columnName.Name, columnValue.PropertyName });
}
In my view, I simply call #Html.DisplayForModel() and the template gets loaded. There is no need to specify 'UIHint' on models.
I hope this was of some value.
In my question about not getting output from views, I actually have an example of how to template a model with a collection of child models and have them all render.
ASP.NET Display Templates - No output
Essentially, you need to create a model that subclasses List<T> or Collection<T> and use this:
#model ChildModelCollection
#foreach (var child in Model)
{
Html.DisplayFor(m => child);
}
In your template for the collection model to iterate and render the children. Each child needs to strongly-typed, so you may want to create your own model types for the items, too, and have templates for those.
So for the OP question:
public class ChildModelCollection : Collection<ChildModel> { }
Will make a strongly-typed model that's a collection that can be resolved to a template like any other.
The actual "valid answer" is -IMHO- not correctly answering the question. I think the OP is searching for a way to have a list template that triggers without specifying the UIHint.
Magic stuff almost does the job
Some magic loads the correct view for a specified type.
Some more magic loads the same view for a collection of a specified type.
There should be some magic that iterates the same view for a collection of a specified type.
Change the actual behavior?
Open your favorite disassembler. The magic occurs in System.Web.Mvc.Html.TemplateHelpers.ExecuteTemplate. As you can see, there are no extensibility points to change the behavior. Maybe a pull request to MVC can help...
Go with the actual magic
I came up with something that works. Create a display template ~/Views/Shared/DisplayTemplates/MyModel.cshtml.
Declare the model as type object.
If the object is a collection, iterate and render the template again. If it's not a collection, then show the object.
#model object
#if (Model is IList<MyModel>)
{
var models = (IList<MyModel>)Model;
<ul>
#foreach (var item in models)
{
#Html.Partial("DisplayTemplates/MyModel", item)
}
</ul>
} else {
var item = (MyModel)Model;
<li>#item.Name</li>
}
}
Now DisplayFor works without UIHint.

Resources