Im making a form in ASP.Net MVC 3 C#.
This particular page of the form contains the user's employment history.
The user can have more than one employment history. So the view model contains a list.
Now it would be easy to so this (and it works):
int i = 0;
foreach (FrgCandidatePortal_2.Models.tblCandidateEmploymentHistory item in Model.empHistList)
{
<div>
<label>
Employment History #Html.Encode(i + 1)</label></h4>
<label>
Company Name</label>
<div>
#Html.EditorFor(c => c.empHistList[i].LJCOMP)
#Html.ValidationMessageFor(c => c.empHistList[i].LJCOMP)
</div>
...
</div>
i++;
}
BUT i want to be able to add an empty one using a button and ajax.
SO i figure "ill make it a partial view".
QUESTION 1: is a partial view the correct way to handle a recurring form element that needs add remove and save functionality?
Now I've made it a partial view and this is my view model for the page:
public class EmploymentHistoryViewModel : ViewModels
{
private List<EmploymentHistory_Partial> _EmploymentHistory_Partial_List;
public List<EmploymentHistory_Partial> EmploymentHistory_Partial_List
{
get
{
if (_EmploymentHistory_Partial_List.Count == 0)
{
int i = 0;
foreach (tblCandidateEmploymentHistory item in dataModel.candidateModel.tblEmpHistList)
{
_EmploymentHistory_Partial_List.Add(new EmploymentHistory_Partial(i, item));
i++;
}
return _EmploymentHistory_Partial_List;
}
else
{
return _EmploymentHistory_Partial_List;
}
}
set {
//foreach emphist partial fill emp hist
_EmploymentHistory_Partial_List.Clear();
dataModel.candidateModel.tblEmpHistList = value.Select(c=>c.empHist).ToList();
}
Basically my view model is a list of _EmploymentHistory_Partial's.
That displays data correctly when i use this code in my employment history view:
int i = 0;
foreach (FrgCandidatePortal_2.Models.EmploymentHistory_Partial item in Model.EmploymentHistory_Partial_List)
{
Html.RenderPartial("_EmploymentHistory_Partial",new FrgCandidatePortal_2.Models.EmploymentHistory_Partial(i,Model.EmploymentHistory_Partial_List[i].empHist));
}
BUT IT DOESNT SUBMIT
QUESTION 2: Why don't the values submit to the model on post?
(Im assuming its something to do with mvc naming conventions in the html document)
UPDATE
PROGRESS!
by editing the html of one of the fields (so that ID and NAME equaled EmploymentHistory_Partial_List[0].empHist.LJCOMP instead of empHist.LJCOMP) before i submitted when debugging. IT WORKED. It got to the set of EmploymentHistory_Partial_List in the view model.
So its naming. What seems to be happening is the partial view isn't inheriting the beginning of the naming from the rest of the form (and quite rightly). Is there any way to make that happen?
You can use EditorTemplates, either through an Html.BeginCollectionItem helper or just by calling Html.EditorFor(Model.EmploymentHistory_Partial_List).
Related
I need to persist changes to a view model that contains a collection, but everytime i post back to the controller, i am losing my model bindings. I am fairly new to MVC so i may be missing something glaring here.
#{ Html.RenderAction("TabList", "TabController", new {Id = Model.Id}); }
I have a main container page that has a render action to a controller to return the first partial view.
[HttpGet]
public ViewResult TabList(Guid orderid)
{
// build the viewmodel
return View("ControlTabList", model);
}
From there iterate over the collection and different render partials based on the object type. (I have simplified the code here, as the items are polymorphic and have some type of downcasting)
#model TabListViewModel
#using (Html.BeginForm("UpdateItem", "TabController", FormMethod.Post, new {Id = "myForm"}))
{
#Html.AntiForgeryToken()
<input type="submit" value="Send" id="submitButton"/>
#for (int i = 0, c = this.Model.Count; i < c; i++)
{
var currentItem = this.Model.ElementAt(i);
#switch (currentItem.Code)
{
case "1":
Html.RenderPartial("Partials/ItemOne", currentItem);
break;
case "2":
Html.RenderPartial("Partials/ItemTwo",currentItem);
break;
default:
Html.RenderPartial("Partials/ItemThree",currentItem);
break;
}
}
}
When I post back to the controller my ViewModel will always be null.
[HttpPost]
public ActionResult UpdateItems(TabListViewModel model)
{
/* i will remove the redirect here, as the model above is always null*
}
Is there a reason why i am losing the bindings? I would like to save the entire collection, instead of individually saving each item in the collection.
There are 2 reasons why your implementation will fail.
First is the use of partials to display each item in the collection. If you inspect the html your generating you will see that the id and name attributes for each currentItem are identical. Duplicate id's are invalid html and duplicate name attributes means that you cannot bind back to a collection. Assuming currentItem has a property string Name, then the correct name would be <input name="Name[0]../>, <input name="Name[1]../> etc. Note the indexers in the name attribute, which allow you to bind to a collection.
Second, your TabListViewModel appears to be collection of a base type where you have added derived types. When you post back, the DefaultModelBinder will only initialize items of the base type since it has no way of knowing which derived type to initialize. From your last question, I assume the base type is ProvinceViewModel and the derived types are QuebecViewModel and OntarioViewModel. Since ProvinceViewModel is abstract, it cant be initialized (no constructor) so your model will always be null. While it is possible to write a custom abstract ModelBinder, as a self confessed newbie, this might be best left until you have a better knowledge of MVC and the model binding process (this article will help you get started)
The easiest way to solve this is with a view model containing collections of each type and use for loops or custom EditorTemplates. For example
View model
public class ProvinceVM
{
public List<QuebecViewModel> QuebecProvinces { get; set; }
public List<OntarioViewModel> OntarioProvinces { get; set; }
}
Then create an EditTemplate for each type
In /Views/Shared/EditorTemplates/QuebecViewModel.cshtml
#model QuebecViewModel
#Html.TextBoxFor(m => m.someProperty)
....
Then in the main view
#model ProvinceVM
#using (Html.BeginForm())
{
#Html.EditorFor(m => m.QuebecProvinces)
// Ditto for OntarioProvinces, or you can use a `for` loop as follows
for(int i = 0; i < Model.OntarioProvinces.Count; i++)
{
#Html.TextBoxFor(m => m.OntarioProvinces[i].someProperty)
....
}
}
Note that both options will generate controls such as
<input name="QuebecProvinces[0].someProperty" ..../>
<input name="QuebecProvinces[1].someProperty" ..../>
which will be correctly bound when you post back to
public ActionResult UpdateItem(ProvinceVM model) // suggest you use a more appropriate name (at least pluralize it)
There seems to be way too much logic in the view. MVC was created to make use of Soc - separation of concerns ( however the view itself it not directly an observable object from the model ) . This enables a developer to write clear and precise code for each part of the system. It however, because of its flexibility as a framework , makes the decision of this up to the developer.
The idea is for clean views, light controllers and heavy classes.
You seem to be running in circles a bit here. From your code it seems to display a view that then returns a view that renders a partialview based on a parameter that is sent to it by the controller (which is building the viewmodel for the partialview ) which is received by the render action id property.
I think there are some clear considerations that need to be implemented for the codes maintainability
First off. It seems as though you want to achieve the display of a partialview. This partialview is determined based on a parameter, the Model.Id . You do not say how this parameter is being injecting into the RenderAction but that should not matter.
#{ Html.RenderAction("TabList", "TabController", new {Id = Model.Id}); }
This code above does not need to exist. It is calling itself. And if that is not the case ( i might not fully understand why you have that there ) then that should be replaced with the call to the partial view, With the main view that the code is sitting on being injected with the ModelId parameter . In this use case that is the View TabList itself.
[HttpGet]
// Model id is passed into the controller in which ever fashion it was
// used to pass into the RenderAction method
public ViewResult TabList(int modelId = 3) // allows for a default on 3
{
// build the viewmodel
return View();
}
"Build the viewmodel" is where the main code happens, this is the stuff that determines what is going to be displayed, and so because it is not directly a display element it needs to sit in the controller. Think of it like this. When the view gets its model, everything the view needs to display should already be in the model. And If you are using polymorphism then there is no reason to be writing switch cases inside of for loops in a view . Just no. That should be sent off to its corresponding interfaces
So to build the viewmodel could be something like this
//build viewmodel
TabListViewModel model = new TabListViewModel{
PartialStuff = dbcontext.entity.FirstOrDefault(_ => _.ModelId == modelId),
}
So now you have a viewmodel with objects that has been filtered based on the modelId , and now just return that model.
return View(model);
The View then declares that model
#model TabListViewModel
And you have one partial view that receives only the objects it needs based on the model
#Render.PartialView("_ItemStuff",Model.PartialStuff)
The partial view then has its model defined as "Partialstuff" and the form objects are created by that model . This allows for a strongly typed model that will pass values back to the controller.
#model PartialStuff
#using (Html.BeginForm("UpdateItem", "TabController", FormMethod.Post, new{Id = "myForm"}))
{
#Html.AntiForgeryToken()
<input type="submit" value="Send" id="submitButton"/>
// Add values with the model directly attached
#Html.TextBoxFor(_ => _.stuffFromTheModel)
I updated the following code using the EditorTemplate to get rid of the heavy switch statements i was using to iterate over the polymorphic collection of derived types.
#for (int i = 0, c = this.Model.Count; i < c; i++)
{
var currentItem = this.Model.ElementAt(i);
#Html.EditorFor(model => currentItem)
}
I then added the following prefix in all of my EditorTemplates to prevent duplicate name and ids of the controls.
#{
ViewData.TemplateInfo.HtmlFieldPrefix = Model.ProvinceCode;
}
I created a custom ModelBinder to intercept and bind the form data to create a ViewModel i can use.
[HttpPost]
public ActionResult UpdateItem([ModelBinder(typeof(ProvinceCustomModelBinder))]ProvinceVM model)
I have a single page ASP.NET MVC 4 application which should display charts. Each chart (dotnetHighcharts) is contained in a single partial view with its own model.
Now I want to be able to display multiple charts on a single page, each with its own model, rendered in a partial view.
My controller looks like this:
public class ReportController : Controller
{
[HttpGet]
public ActionResult Display(Guid? id)
{
var viewModel = PrepareViewModel(id);
return View(viewModel);
}
[HttpPost]
public ActionResult DisplayChart(Guid? id)
{
var viewModel = PrepareViewModel(id);
return PartialView("Partial_ChartView", viewModel);
}
}
The partial view looks like this:
#model MobileReports.Models.ReportViewModel
#Model.Chart
The main view has this code:
#model IEnumerable<MobileReports.Models.ReportViewModel>
#{
ViewBag.Title = "Display";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div id="chartContainer" style="float:left">
#{Html.RenderPartial("Partial_ChartView");}
</div>
<div id="secondChartContainer" style="float:right">
#{Html.RenderPartial("Partial_ChartView");}
</div>
The Model itself contains only a rendered chart and a few infos about it.
My problem is, I don't know if I am doing it right. The mainview has to know which charts are already displayed on the page, so that I dont display the same chart twice. In the initial load, I somehow have to tell the partialviews which charts are already being displayed on the page. How can I do that?
Also how do I keep multiple models on the main view?
-edit-
The controller action DisplayChart(Guid? id) is used inside the partialView from a javascript function:
function displayNextReport() {
// try to find a report that is not displayed already
var diff = $(availableReports).not(displayedCharts).get();
if (diff.length > 0) {
// remove old chart data
if ($("#chart_container").highcharts() !== undefined) {
$("#chart_container").highcharts().destroy();
}
// load the new report
$("#chart_container").load('#Url.Action("DisplayChart", "Report")', { id: diff[0] });
}
}
Note: This does not work right now. I need to pass an argument that tells the DisplayChart() method which chart to load next. For this, I need to know which charts are currently diplayed.
My code is most likely very wrong and messed up. I think it is best to just describe what I want to achieve:
I want to have a View that displays multiple partial views of the same type. Each partial view has to know what the other partial view are currently displaying. Thats all I want to do..
I'm working on an ASP.NET MVC3 project, and I have a problem with RadioButtons name generated by a for loop in a PartialView.
First, here is my code, I'll explain the exact problem just after :
Model FileUploadModel
public class FileUploadModel
{
public HttpPostedFileBase File { get; set; }
public bool IsMainFile { get; set; }
}
Model MyModel
public class MyModel
{
// Some properties
public List<FileUploadModel> Files { get; set; }
}
How the PartialView _UploadFiles is called
#Html.Partial("_UploadFiles", Model) // Model is a MyModel model
PartialView _UploadFiles - HERE IS THE PROBLEM
#model MyModel
#{
var nbFiles = (Model.Files != null ? Model.Files.Count : 0);
const int NB_DEFAULT_UPLOAD_FIELDS = 3;
}
#for (int i = 0; i < NB_DEFAULT_UPLOAD_FIELDS; i++)
{
#Html.TextBoxFor(m => m.Files[nbFiles].File, new { type = "file", name = "Files", id = "Files" })
#Html.RadioButtonFor(m => m.Files[nbFiles].IsMainFile, "Main ?", new { id = "rb" + i + "_File_IsMain" })
}
So this is the last snippet the problem. I'd like to create 3 upload fields with an associated RadioButton indicating if the file is the "main file". However, with the above snippet, the displayed view is OK, but when I validate my form, my posted model only have the first picture uploaded (and the corresponding boolean IsMainFile , the others are just ignored). That means the List Files contains only the first TextBox and first RadioButton data
So I tried with #Html.RadioButtonFor(m => m.Files[i].IsMainFile, but the RadioButtons have a different name so user can check all RadioButtons, which is NOT the desired behavior. MVC doesn't authorize me to override the name of the RadioButtons, so I can't give them my own name.
How can I generate these fields is order to only ONE Radiobutton can be checked AND my MyModel.Files property contains all chosen files ?
Thank you
I think your best bet here is to use editor templates.
First, you create a view named FileUploadModel, and place it a folder named EditorTemplates which should exist under your controller's Views folder. Note that the name of the view must match the name of your view model's class.
Its contents would look something like:
#model FileUploadModel
#Html.TextBoxFor(m => m.File, new { type = "file" })
#Html.RadioButtonFor(m => m.IsMainFile, true)
Then, back in your "_UploadFiles" view, your markup for the input field and radio button would change to:
#model MyModel
using (Html.BeginForm(...
#Html.EditorFor(m => m.Files)
...
Note that this will automatically iterate through and apply that editor template for each FileUploadModel object, and automatically name them as required. This of course assumes that the Files property in your model is populated.
Your action that accepts the post should accept type MyModel as its sole parameter, and everything should bind up automatically at post.
There are ways to avoid editor templates and build and name those fields programmatically, but this is really the preferred way, and much cleaner.
More specific to your issue... (In other words, I forgot that the code above won't group radio buttons.)
For the grouping of radio buttons to work, you should set the attribute name as follows:
#Html.RadioButtonFor(m => m.IsMainFile, true, new { Name = "IsMainFile" })
Case matters here. Note that Name must be capitalized for this to work. I'm not sure why this is the case.
The problem is that once you change that attribute, this field will no longer automatically bind in the post. This is unfortunate but understandably is as designed since, if you look at the request, I believe you will see that only the selected radio button value has been posted.
So, you could change the value of the radio button to something recognizable since just that one instance of IsMainFile will show up in the request, if I'm correct.
Perhaps you can use this specific knowledge to tweak your existing code.
I have an ASP.NET MVC application that displays a list of items. In my view page I loop over the items and render each item with partial view, like so:
#foreach(var item in Model.items)
{
<li>
#Html.Partial("ItemView", item)
</li>
}
In the item view, I wrap each item with a form that has a 'Delete' button, like this:
#using(Html.BeginForm(...))
{
#Html.HiddenFor(m=>m.Id)
<label>#Model.Name (#Model.Id)</label>
<input type="submit" value="Delete"/>
}
The items are rendered properly, the resulting page has a nice list of all the items with their proper names and IDs displayed.
EDIT: The same happens with #Hidden, apparently, contrary to what I wrote before.
In addition, this only happens the second time the form is rendered (that is, after one of the Delete buttons is clicked), the first time everything is working properly. My action methods looks like this:
public ActionResult AllItems()
{
var model = new AllItemsModel();
return PartialView(model);
}
public ActionResult Delete(DeleteModel model)
{
.... Perform the delete ...
return PartialView("AllItems", new AllItemsModel());
}
Why is this happening?
I suspect that this happens because you already have an Id parameter in your RouteData:
public ActionResult SomeAction(int id)
{
var model = ...
return View(model);
}
and you have requested the page with /somecontroller/someaction/123. The HiddenFor helper now uses the Id from the route values and not the id of the item. Try renaming the property on your item view model to something different than id. For example ItemId.
Another possibility is that the problem occurs only after the postback and not when the page is initially rendered. Showing your POST action might help in exploring this possibility further.
UPDATE:
Alright, now that you have shown your POST action things are much more clear:
public ActionResult Delete(DeleteModel model)
{
.... Perform the delete ...
return PartialView("AllItems", new AllItemsModel());
}
you are basically creating a new view model here and passing it to the partial view. But HTML helpers always use the value from the ModelState when binding. And only after that the value from your view model. So if you intend to modify properties on your model inside your POST action make sure that you have removed this value from the ModelState first. In your example since you have completely scratched the entire view model (by creating a new AllItemsModel()) you could clear the entire ModelState:
public ActionResult Delete(DeleteModel model)
{
.... Perform the delete ...
// Clear the modelstate otherwise the view will use the values that were initially posted
// and not the values from your view model
ModelState.Clear();
return PartialView("AllItems", new AllItemsModel());
}
This behavior is by design and applies to all HTML helpers, not only the HiddenFor helper.
Working on this project and I am stuck again. I have a view that I need to display a menu along the side. The menu is pulled from a table. Along the side of this, I have a question that is pulled from another table. Inside of this, there will be suggestions that are yet pulled from another table. With the question, I have a couple of inputs, radio buttons, and a text box for feedback which will end up in a different table. All of this has to take place with the ID from the main table so that when the information is saved in the results table it knows what the main record it belongs too. The page can be brought up with a /Audit/302 but when you change the submenu to the next question how do you get that question and maintain the ID. For Every question, there will be an entry for that ID. I have made relationships this is what it looks like.
August 02 2011
I have added a ViewModel that pulls a list of checkboxes from a table called Score (OK, Concern, N/A)
The page that i have is rendered with an ID it pulls a record from AuditSchedules and retrieves some info. In the Layout i also have a side menu (partial) that renders a list of actionlinks that will select different sections and a MainQuestion (partial). We work up to this point. Since i have added the viewModel to populate the checkboxes and save the text into a table with the ID of the AuditSchedule and MainQuestion. My issue is i am calling for id to select the ids from the table for the Scores and it is grabbing the id from the AuditSchedule. Giving me a no data found.
Here is my Controller:
//get
public ActionResult _Forms(int score)
{
AuditFormEdit viewModel = new AuditFormEdit();
viewModel.ScoreInstance = _db.Scores.Single(r => r.ScoreID == score);
viewModel.InitializeScoreCheckBoxHelperList(_db.Scores.ToList());
return View(viewModel);
}
//post
[HttpPost]
public ActionResult _Forms(int score, AuditFormEdit viewModel)
{
if (ModelState.IsValid)
{
viewModel.PopulateCheckBoxsToScores();
_db.Entry(viewModel.ScoreInstance).State = System.Data.EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("/");
}
else
{
return View(viewModel);
}
}
Here is my viewModel:
public class AuditFormEdit
{
public Models.Score ScoreInstance { get; set; }
public List<ScoreCardCheckBoxHelper> ScoreCardCheckBoxHelperList { get; set; }
public void InitializeScoreCheckBoxHelperList(List<Models.Score> ScoreList)
{
if (this.ScoreCardCheckBoxHelperList == null)
this.ScoreCardCheckBoxHelperList = new List<ScoreCardCheckBoxHelper>();
if (ScoreList != null
&& this.ScoreInstance != null)
{
this.ScoreCardCheckBoxHelperList.Clear();
ScoreCardCheckBoxHelper spacerProductCheckBoxHelper;
string spacerTypes =
string.IsNullOrEmpty(this.ScoreInstance.ScoreName) ?
string.Empty : this.ScoreInstance.ScoreName;
foreach (Models.Score scoreType in ScoreList)
{
spacerProductCheckBoxHelper = new ScoreCardCheckBoxHelper(scoreType);
if (spacerTypes.Contains(scoreType.ScoreName))
spacerProductCheckBoxHelper.Checked = true;
this.ScoreCardCheckBoxHelperList.Add(spacerProductCheckBoxHelper);
}
}
}
public void PopulateCheckBoxsToScores()
{
this.ScoreInstance.ScoreName = string.Empty;
var scoreType = this.ScoreCardCheckBoxHelperList.Where(x => x.Checked)
.Select<ScoreCardCheckBoxHelper, string>(x => x.ScoreName)
.AsEnumerable();
this.ScoreInstance.ScoreName = string.Join(", ", scoreType);
}
public class ScoreCardCheckBoxHelper : Models.Score
{
public bool Checked { get; set; }
public ScoreCardCheckBoxHelper() : base() { }
public ScoreCardCheckBoxHelper(Models.Score scoreCard)
{
this.ScoreID = scoreCard.ScoreID;
this.ScoreName = scoreCard.ScoreName;
}
}
}
Here is the link and controller that pulls the page up this is the Body of the layout:
public ActionResult Audit(int id)
{
var auditToDetail = _db.AuditSchedules.Single(r => r.AuditScheduleID == id);
return View(auditToDetail);
}
the link looks like this:
/AuditSchedule/Audit/257
Let me know if you need anything else.
I have used a code first approach and all tables have models with the relationships called out.
thank you
Your question is pretty general - but here are some things to think about.
First, in MVC3 you typically want to strongly type your views to your models. In your case you would want a single view for each model. You don't want "multiple multiple models in a view", but rather multiple views rendered together on a single page - all linked by some common data.
One way to accomplish this would be set write a "child actions" (use:[ChildActionOnly] attribute) in your controller for populating each of your individual models sending them to their respective indiviudal views. You can use the [Html.RenderAction][3] function within any view to render any other view. The easiest way to organize this is often with layouts 4 - You could render all your views using Html.RenderAction(), and then render your main form as the primary view (displayed in the layout with #RenderBody()).
To populate each of your models you will likely need to know some data (like an id), these can be stored in the ViewBag or ViewData, and passed in as objects in the Html.RenderAction function. (If you are using razor the call in your view might look something like:#{Html.RenderAction("actionName", new {object1 = "xyz", object2 ="abc"});} Just to make this clear, the logical flow will look like this
Populate the ViewBag with the data you will need to render your sub-views,
Populate your model for the main form and you call the main form's view (your primary view).
Your primary view defines a layout view, which in-turn calls Html.RenderAction, using the ViewBag data to set the appropriate objects.
The layout also renders the primary view as the "body" (#RenderBody()).
The end result of this is views that can be [strongly typed][7] to individual views, and individual controller actions for each model. This is a good thing, because it makes your views reusable anywhere, and it isolates the logic for populating each model (because each model has its own action in the controller). If you make subsequent changes to a model, it will be easy to know which Action/Model/View needs to be modified.
Below are some resources to get you started in understanding your options for rendering multiple models on a single page. I apologize for not lacing this post with nice links to helpful items, but as a new answerer, I was limited to two... Google will have to be your guide for the others.
Hope this helps - Best of Luck!
Resources:
Html.RenderAction and Html.Action - http://haacked.com blog
When to use Html.RenderPartial and Html.RenderAction in ASP.NET MVC Razor Views - http://www.arrangeactassert.com