I have the following view:
#model dynamic
<form class="form-horizontal" id="formDynamicItem" action="/DynamicItem/SaveItem" method="post">
#Html.AntiForgeryToken()
<div class="col-xs-12 buttonBar">
<button type="submit" value="Save" name="submitButton" class="btn">Save</button>
<button type="submit" value="Cancel" name="submitButton" class="btn">Cancel</button>
</div>
<div class="col-lg-6">
<div class="ibox ">
<div class="ibox-content">
#{
foreach (var obj in Model)
{
var kvpObj = (KeyValuePair<string, object>)obj;
var entityProp = (EntityAttributeProperties)kvpObj.Value;
<div class="form-group">
#if (entityProp.IsHiddenField)
{
<input type="hidden" class="form-control" data-val="true" id="#kvpObj.Key" name="#kvpObj.Key" value="#entityProp.Value" />
}
else if (entityProp.IsFormField)
{
var isReadOnly = entityProp.IsReadonly ? "readonly" : "";
IHtmlString validationRules = Html.Raw(string.Empty);
if (entityProp.ValidationRules != null)
{
validationRules = entityProp.ValidationRules;
}
#Html.Label(entityProp.Name, new { #class = labelClass })
<div class="#controlClass">
#switch (#entityProp.Type)
{
//... many cases
default:
<input type="text" class="form-control" id="#kvpObj.Key" name="#kvpObj.Key" value="#entityProp.Value" #isReadOnly #validationRules />
break;
}
</div>
}
</div>
}
}
</div>
</div>
</div>
</form>
#section Scripts {
<script>
$("#formDynamicItem").validate();
</script>
}
And in the controller I get my values using FormCollection:
public ActionResult SaveItem(FormCollection form)
{
...
newValue = typeConverter.ConvertFromString(form[entityAttribute.Name]);
...
}
}
My question is the following:
How can I establish Server-side validation on such dynamic model? Can I use FormCollection somehow? Possibly build dynamic view model somehow? If anyone has experience in this please consider giving a suggestion(answer).
Update: Making Detail page with ViewModel insted of dynamic model
So, After much refactoring I appear to be again stuck with server-side validation:
So now I have this ViewModel:
public class DynamicItemViewModel
{
public Guid Id { get; set; }
public List<EntityAttributeProperties> Properties { get; set; }
}
this detail page:
#model ExactDistillation.Models.DynamicItem.DynamicItemViewModel
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
#using (Html.BeginForm("SaveItem", "DynamicItem", FormMethod.Post, new { #class = "form-horizontal", #id = "formDynamicItem" }))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(x => x.Id)
<div class="col-xs-12 buttonBar">
<button type="submit" value="Save" name="submitButton" class="btn btn-primary pull-right">Save</button>
<button type="submit" value="Cancel" name="submitButton" class="btn btn-default pull-right cancel">Cancel</button>
</div>
<div class="col-lg-6">
<div class="ibox float-e-margins">
<div class="ibox-title text-webwonders">
<h5>Item</h5>
</div>
<div class="ibox-content">
#{
for (int i = 0; i < Model.Properties.Count; i++)
{
#Html.EditorFor(m => Model.Properties[i], "EntityAttributeProperties", "Properties[" + i + "]")
}
}
</div>
</div>
</div>
}
</div>
</div>
And this is how I define EntityAttributeProperties page:
#model EntityAttributeProperties
<div class="form-group">
#if (Model.IsHiddenField)
{
#Html.HiddenFor(x => x.Value)
}
else if (Model.IsFormField)
{
#Html.Label(Model.Name, new { #class = "col-sm-5 col-md-4 col-lg-3" })
<div class="col-sm-7 col-md-8 col-lg-9">
#switch (Model.Type)
{
--- Many cases
default:
#Html.DynamicTextBoxFor(m => m.Value, null, Model.IsReadonly, Model.ValidationRules)
break;
}
</div>
}
</div>
EntityAttributesProperties looks the following way:
public class EntityAttributeProperties
{
public string Name { get; set; }
public string DisplayName { get; set; }
public object Value { get; set; }
public EntityAttributeDataTypeEnum Type { get; set; }
public short Order { get; set; }
public bool IsFormField { get; set; }
public bool IsReadonly { get; set; }
public bool IsHiddenField { get; set; }
public Dictionary<string,object> ValidationRules { get; set; }
}
So, I am trying to make server-side validation for Model, but I am stuck, because I don't find any elegant solution to my problem, just solutions in which I have to do a lot of hardcoding (which I don't like).
Here is how I receive the form Submit:
public ActionResult SaveItem(DynamicItemViewModel model)
{
List<EntityAttributeExtendedView> entityAttributes = GetItemEntityAttributes();
DataObject dataObject = _dbContext.DataObjectCollection.FirstOrDefault(x => x.Id == model.Id);
if (dataObject != null)
{
JObject json = JObject.Parse(dataObject.Value);
dynamic dynJson = JsonConvert.DeserializeObject(dataObject.Value);
// Possibly loop through all entity attributes and separately make a custom validation ?
// Or somehow create custom Model State for validation ?
}
return View("Detail", model);
}
I will appreciate any suggestion on how to approach the problem with server side validation.
Thanks.
FormCollection is a very raw form of data and cannot be validated easily. I'd suggest you should refactor your view to be able to use ViewModels, else you will have a hard time working with the Data.
I cannot show you the full way but give you some hints:
Create a single View Model class for the View which contains a list of Items (of type EntityAttributeProperties). Let's call it MainViewModel.
Pass this View Model to the view rather then the Dictionary
In your view, use #Html.EditoFor(x => x.Items) to generate the correct HTML. ASP.NET MVC will use the Editor Templates for EntityAttributeProperties type
This is a good moment to create a new view EntityAttributeProperties.cshtml in your View folders EditorTemplates sub-folder
within this item view, you can do all your entityProp.Type switches, but be careful with ID generation, always use #Html.IdFor(...) etc. instead of generating own IDs to keep type safe with your View Model
After some tweaks, your Post Action should be able to receive your view model of Type MainViewModel. If everything went good, the Item's will be filled, even if you used different Controls (hidden fields, text fields, drop-downs...) to populate the Values
From my perspective, only this MVC-safe approach will lead to success in this case
I'm adding another Answer because the Question changed heavily.
A good approach is to use the IValidatableObject Interface. So you add this Interface to your EntityAttributeProperties class and have to override the Method Validate. for simple validation like required fields, you use so called validation Attributes.
Your EntityAttributeProperties class would be decorated like this:
public class EntityAttributeProperties : IValidatableObject
{
public string Name { get; set; }
[Required]
public object Value { get; set; }
...
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
if (... /* some condition, e.g. specific EntityAttributeDataTypeEnum */)
{
// Do some validation
// some other random test
if (.../* something not right... */)
{
results.Add(new ValidationResult("your input was not valid!"));
}
}
return results;
}
}
You might need to also make your DynamicItemViewModel and IValidatableObject and loop through the Items, but sometimes MVC is smart enough to validate sub-items automatically, so you might need this:
public class DynamicItemViewModel : IValidatableObject
{
...
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
return Items.SelectMany(x => x.Validate(validationContext));
}
}
OK now in your Controller, you basically need to check your ModelState. The automatically generated ModelState Property contains all errors.
Related
I have an razor page receiving an PageModel descendant.
For exibition-only pages (dashboards, reports, etc), it works like a charm. I use the base PageModel class to make available to turn all pages reflect my hosting.json configuration, making links inside the app dynamic.
So the hierarchy is: PageModel --> BasePageModel --> ConfigTransmissorFragModel
Now I have this SendingConfiguration model (I will process it in lower level layer):
public class SendingConfiguration
{
// I will edit this one
[DisplayName("Send it to production server?")]
public bool ProductionServer { get; set; }
}
The PageModel is as below:
// BasePageModel will process the urls of the site.
public class ConfigTransmissorFragModel : BasePageModel
{
public SendingConfiguration SendConfig { get; set; }
private void UpdateSendingConfiguration()
{
var Config = InfoExibicao.ConfigAtiva;
ConfigEnvio.ProductionServer = Config.ProductionServer;
}
private InfoConfiguration _InfoExibition;
public InfoConfiguration InfoExibition
{
get { return _InfoExibition; }
set
{
_InfoExibition = value;
}
}
public ConfigTransmissorFragModel() : base(null)
{
SendConfig = new SendingConfiguration();
}
public ConfigTransmissorFragModel(
ConfigDashboard PConfigDash,
InfoConfiguration PInfoConfig
) : base(PConfigDash)
{
ConfigEnvio = new SendingConfiguration();
this.InfoExibition = PInfoConfig;
UpdateSendingConfiguration();
}
}
The Model is generated in controller:
var Fch = FacadeInfoConfig;
var InfoConfig = Fch.ObterInfoConfiguracao(IdTransmissor);
var CfgDash = ConfigDash;
var Modelo = new ConfigTransmissorFragModel(CfgDash, InfoConfig);
Modelo.UrlServer = ConfigHost.WebServiceUrl;
In the razor page I have this header:
#page
#using ESenderWebService.ModeloPagina
#using Microsoft.AspNetCore.Mvc.RazorPages;
#using Negocio.Integracao.Configuracao;
#model ESenderWebService.ModeloPagina.ConfigTransmissorFragModel
#{
//ViewData["Title"] = "Configuracao de Transmissor para envio";
Layout = "~/Pages/Shared/_Layout_Dashboard.cshtml";
}
The form:
<form autocomplete="off" asp-controller="InfoConfig" asp-action="SalvarConfig" method="post">
<div class="form-group">
<fieldset>
<legend style="width: auto; margin-bottom: auto;"> Configurações </legend>
<div>
#Html.CheckBoxFor(m => m.SendConfig.ProductionServer)
#Html.LabelFor(m => m.SendConfig.ProductionServer)
</div>
<div>
<button class="btn btn-primary " name="submit" type="submit">Save</button>
</div>
</fieldset>
</div>
</form>
My problem is: how to receive SendingConfiguration on my post handler?
// This one explodes complaining that HttpContext is missing
[HttpPost()]
public IActionResult SalvarConfig(ConfigTransmissorFragModel PModel)
// This one never reflects what I mark in the form
// It always returns false in PModel.ProductionServer
[HttpPost()]
public IActionResult SalvarConfig([FromForm] SendingConfiguration PModel)
What I'm doing wrong?
I am not sure why the first HttpPost syntax exploding in your case, because its working fine in my sample project.
i am trying to create a ASP Core Application with Razorpages.
Everything is working fine so far, but now i'm stuck with this problem:
I want to create a form with multiple Buttons based on a Objectarray. Each Button has its own Name and should call the same OnPost method. So far so good. I struggle with posting the correct Object with the form.
Here is some Code...
Test.cshtml :
#page
#using APP.Models
#model TestModel
#{
ViewData["Title"] = "Test";
Layout = "~/Pages/Shared/_Layout.cshtml";
var bereiche = Bereich.getBereiche();
}
<div class="grid">
<h1 class="center-text">#ViewData["Title"]</h1>
<form method="post" class="center">
<div class="form-group">
<label asp-for="Model" class="col-form-label"></label>
<div class="button-group-vertical">
#for (var i = 0; i < bereiche.Count; i++)
{
<input type="hidden" asp-for="Model" value="#bereiche[i]" />
<button class="btn btn-info btn-md button-group-vertical-item">#bereiche[i].name</button>
}
</div>
</div>
</form>
</div>
Bereich.cs in namespace Model
public enum Art
{
Mitte, Beladen, Inspection, Kette
}
public class Bereich
{
public string name { get; set; }
public Art art { get; set; }
public static List<Bereich> getBereiche()
{
var bereiche = new List<Bereich>
{
new Bereich() { name="Mitte", art = Art.Mitte},
new Bereich(){name= "Beladen",art = Art.Beladen },
new Bereich(){name = "Qualitätskontrolle",art = Art.Inspection},
new Bereich(){name ="Kette",art= Art.Kette}
};
return bereiche;
}
}
Test.cshtml.cs :
public class TestModel : PageModel
{
[DisplayName("Bereich")]
[BindProperty]
public Bereich Model { get; set; }
public void OnGet()
{
}
public IActionResult OnPost()
{
return Page(); // BREAKPOINT HERE -> MODEL always "art=Mitte", "name=null" :/
}
}
Does anyone have a clue what i am doin wrong ?
In order to post an object back, you could use one form per object, with named fields corrosponding to your object property names.
Try this instead:
#for (var i = 0; i < bereiche.Count; i++)
{
<form method="post" class="center">
<input name="art" type="hidden" value="#bereiche[i].art" />
<button name="name" value="#bereiche[i].name" class="btn btn-info">#bereiche[i].name</button>
</form>
}
And also if you want your button to hold a value, you need to put the value in a value tag like i did in the example.
When I submit, my model is empty on post.
Model
public QuizModel()
{
Questions = new List<QuizQuestionModel>();
}
public QuizModel(string quizName)
{
QuizName = quizName;
Score = 0;
IntranetEntities db = new IntranetEntities();
Quiz quiz = db.Quizs.Where(x => x.Name.Equals(quizName, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
if (quiz != null)
{
IQueryable<Quiz_Question> questions = db.Quiz_Question.Where(x => x.QuizID.Equals(quiz.ID)).OrderBy(x => x.QuestionNo);
Questions = new List<QuizQuestionModel>();
foreach (Quiz_Question question in questions)
{
QuizQuestionModel q = new QuizQuestionModel();
q.ID = question.ID;
q.Question = question.Question;
q.UserAnswer = null;
q.SystemAnswer = question.Answer;
Questions.Add(q);
}
}
}
public string QuizName { get; set; }
public List<QuizQuestionModel> Questions { get; set; }
public int Score { get; set; }
Controller
[HttpPost]
public ActionResult OSHAQuiz(Models.QuizModel model)
{
if (ModelState.IsValid)
{
bool passed = false;
model.Score = model.Questions.Where(x => x.UserAnswer.Equals(x.SystemAnswer, StringComparison.InvariantCultureIgnoreCase)).Count();
if (!model.Score.Equals(0))
{
double percent = model.Score / model.Questions.Count();
if (percent >= .8)
{
passed = true;
}
}
if (passed)
{
return View("/Views/Quiz/Passed.cshtml");
}
else
{
return View("/Views/Quiz/Failed.cshtml");
}
}
else
{
return View("/Views/Quiz/Quiz.cshtml", model);
}
}
View
#model PAL.Intranet.Models.QuizModel
<script>
$(document).ready(function () {
$("input:checked").removeAttr("checked");
});
</script>
<div class="grid">
<h2>OSHA Quiz</h2>
<hr />
<div class="align-center">
#using (Html.BeginForm("OSHAQuiz", "Quiz", FormMethod.Post, new { id = "formShowLoading" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<div class="row cell">
<div class="example bg-grayLighter" data-text="Directions">
<ul class="simple-list">
<li class="align-left">When you have made your selection for all 20 statements, click on the button 'Submit.'</li>
<li class="align-left">Mark 'True' or 'False' for each statement.</li>
<li class="align-left">You must score 80% (16 correct) to pass.</li>
<li class="align-left">You must fill in your full name to receive credit.</li>
</ul>
</div>
</div>
<div class="row cell">
<div class="row cell">
<div class="panel" data-role="panel">
<div class="heading">
<span class="title">Questions</span>
</div>
<div class="content">
<ul class="numeric-list">
#foreach (var question in Model.Questions)
{
<li>
<table class="table hovered" style="width: 100%;">
<tr>
<td align="left">#question</td>
<td align="right" width="150px">
<div class="align-center">
<label class="align-right input-control radio small-check">
#Html.RadioButtonFor(model => question.UserAnswer, true, new { Name = question.GroupName })
<span class="check"></span>
<span class="caption">True</span>
</label>
<label class="align-right input-control radio small-check">
#Html.RadioButtonFor(model => question.UserAnswer, false, new { Name = question.GroupName })
<span class="check"></span>
<span class="caption">False</span>
</label>
</div>
</td>
</tr>
</table>
</li>
}
</ul>
</div>
</div>
</div>
</div>
<div class="row cell">
<input type="submit" value="Submit" class="button info small-button" />
<input type="reset" value="Reset" class="button primary small-button" />
</div>
}
</div>
</div>
UPDATE
if I use HiddenFor on QuizName, it does come back over on post but the rest of the model is empty.
When iterating over a collection that you want to post back to your model, you can't use foreach; you must use a regular for statement with indexing in order for Razor to generate the correct field names.
#for (var i = 0; i < Model.Questions.Count(); i++)
{
...
#Html.RadioButtonFor(m => m.Questions[i].UserAnswer)
}
Then, your fields will have name attributes in the form of Questions[0].UserAnswer, which the modelbinder will recognize and bind appropriately to your model. As you have it now, with the foreach, the field name is being generated as question.UserAnswer, which the modelbinder has no idea what to do with and discards.
Also, FWIW, accessing your context from within your model entity is a hugely bad idea, and even worse if you're not injecting it. Move that logic out of your entity and utilize a utility class or service instead. Also, look into dependency injection, as your context is one of those things that you want one and only one instance of per request. If you start instantiating multiple instances of the same context, you will have problems.
The problem is model binding is going to attempt to bind your form values to properties on your model. It will not use the constructor on your model that takes the quiz name, it will use the default constructor to instantiate the QuizModel object.
I would consider refactoring this model to remove your EntityFramework dependency and find a new way to populate those values.
You should also call the Dispose() method on IDisposable objects when you're done using them.
My suggestion for how to solve this problem would be to use the QuizModel you currently have to help render your view (i.e. Your quiz questions and possible answer for each question).
Create a seperate ViewModel for quiz submission
public class QuizSubmission
{
public string QuizName { get;set; }
public List<QuizQuestionResponse> Responses { get;set; }
}
public class QuizQuestionResponse
{
public int QuestionId { get;set; }
public int AnswerId { get;set; }
}
In your controller action, you should be binding to a QuizSubmission model.
[HttpPost]
public ActionResult OSHAQuiz(Models.QuizSubmission model)
{
Then you'll be able to perform any actions you need for that quiz submission (ie. data access, validation ).
You'll also need to update your view so that your Html input elements have the correct name attributes that model binding can correctly bind each question and response pair to a QuizQuestionResponse item in your Responses list.
Been searching around but couldn't find a direct solution to what I'm trying to achieve.
I've tried to include as much as needed but it's a very large project so hopefully you'll get the gist.
Overview:
I have a view model that has several lists of objects within it. I am using two partial views for control over each of the list of objects, one for gathering the list of objects (which is held in a session), and the other for adding a list of said object into the list.
Update:
As per comment - what I am looking to do is as follows - in the Index, fill out the existingIp model info, which is displayed through the addToListPartialView, then it will post to the ListPartialView to update the list through the session, handled backend in the controller, which will in turn display on the Index - the question ultimately is, how do I achieve this?
Problem:
The issue I'm having is once I've added an object, through a partial view, to the object list, another partial view, how do I then pass this back to the main view?
Code:
Controller
public ActionResult AddExistingIp([Bind(Include = "Subnet, Cidr, Mask")]ExistingIp existingIp)
{
if(Session["pa_ipv4Session"] != null)
{
pa_ipv4 pa_ipv4 = (pa_ipv4)Session["pa_ipv4Session"];
if(pa_ipv4.ExistingIps == null)
{
pa_ipv4.ExistingIps = new List<ExistingIp>();
}
pa_ipv4.ExistingIps.Add(existingIp);
ViewBag.pa_ipv4 = pa_ipv4.ExistingIps;
return View("ExistingIpView", ViewBag.pa_ipv4);
}
else
{
pa_ipv4 pa_ipv4 = new pa_ipv4();
Session["pa_ipv4Session"] = pa_ipv4;
pa_ipv4.ExistingIps = new List<ExistingIp>();
pa_ipv4.ExistingIps.Add(existingIp);
ViewBag.pa_ipv4 = pa_ipv4.ExistingIps;
return View("ExistingIpView", ViewBag.pa_ipv4);
}
Index:
#model ViewModel
<div id="ExistingIpList">
#{Html.RenderPartial("ExistingIpView");}
</div>
<div id="addExisting">
#{Html.RenderPartial("AddExistingIp");}
</div>
List Partial
#model IEnumerable<ExistingIp>
#if (Model != null)
{
foreach (var ei in Model)
{
<div class="ui-grid-c ui-responsive">
<div class="ui-block-a">
<span>#ei.Subnet</span>
</div>
<div class="ui-block-b">
<span>#ei.Cidr</span>
</div>
<div class="ui-block-c">
<span>#ei.Mask</span>
</div>
<div class="ui-block-d">
#ei.Id
Delete
</div>
</div>
}
}
Add to list partial:
#using (Html.BeginForm("AddExistingIp", "PA_IPV4"))
{
<div class="ui-grid-c ui-responsive">
<div class="ui-block-a">
<span>
#Html.EditorFor(m => m.Subnet)
#Html.ValidationMessageFor(m => m.Subnet)
</span>
</div>
<div class="ui-block-b">
<span>
#Html.EditorFor(m => m.Cidr)
#Html.ValidationMessageFor(m => m.Cidr)
</span>
</div>
<div class="ui-block-c">
<span>
#Html.EditorFor(m => m.Mask)
#Html.ValidationMessageFor(m => m.Mask)
</span>
</div>
<div class="ui-block-d">
<span>
#Html.EditorFor(m => m.Id)
#Html.ValidationMessageFor(m => m.Id)
</span>
</div>
</div>
<div data-role="main" class="ui-content">
<div data-role="controlgroup" data-type="horizontal">
<input type="submit" id="addExistingIp" cssclass="ui-btn ui-corner-all ui-shadow" value="Add" />
</div>
</div>
}
ViewModel:
public Contact ContactDetails { get; set; }
[Required]
public bool ExistingAddress { get; set; }
public List<ExistingIp> ExistingIps { get; set; }
[Required]
[DataType(DataType.MultilineText)]
public string ExistingNotes { get; set; }
You can modify the AddExistingIp to just store the data. And to make a RedirectToAction Index. There you will take the data from Session and pass it to the Model.
[HttpPost]
public ActionResult AddExistingIp([Bind(Include = "Subnet, Cidr, Mask")]ExistingIp existingIp)
{
if(Session["pa_ipv4Session"] != null)
{
pa_ipv4 pa_ipv4 = (pa_ipv4)Session["pa_ipv4Session"];
if(pa_ipv4.ExistingIps == null)
{
pa_ipv4.ExistingIps = new List<ExistingIp>();
}
pa_ipv4.ExistingIps.Add(existingIp);
}
else
{
pa_ipv4 pa_ipv4 = new pa_ipv4();
Session["pa_ipv4Session"] = pa_ipv4;
pa_ipv4.ExistingIps = new List<ExistingIp>();
pa_ipv4.ExistingIps.Add(existingIp);
}
return RedirectToAction("Index");
}
The Index Action will look similar with this, where you take data from Session and use it in your Model
public ActionResult Index()
{
var viewModel = new ViewModel();
// take data from Session
pa_ipv4 pa_ipv4 = Session["pa_ipv4Session"] as (pa_ipv4);
// some verification
// add the list from Session to model
viewModel.ExistingIps = pa_ipv4.ExistingIps;
return View(viewModel);
}
Also, I think your Index View you should at ExistingIpView you should pass the Model to display.
#model ViewModel
<div id="ExistingIpList">
#{Html.RenderPartial("ExistingIpView", Model.ExistingIps);}
</div>
<div id="addExisting">
#{Html.RenderPartial("AddExistingIp");}
</div>
In a View i have the next structure ( control of Subject*s for each *Group):
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#for (int i = 0; i < ViewBag.AllGroups.Count; i++)
{
<h4>#ViewBag.AllGroups[i].Code</h4>
<select id="e-#i" multiple="multiple">
#foreach (Subject subject in ViewBag.AllSubjects)
{
<option value="#subject.Name">#subject.Name</option>
}
</select>
}
<input type="submit" value="Generate" class="btn btn-default" />
</div>
}
The question is that how can I retreive this data (I want to receive (1)list of Groups and and I want to get a (2)list of all selected Subjects for each group in my list(1)) in my Controller?
Thank you in advance.
Recommended way is to use strongly typed View Model Object
public class GroupViewModel
{
public string Code { get;set; }
public List<Subject> AllSubjects { get; set; }
}
Pass List as the Model to the Razor view in the controller.
return new View(new List<GroupViewModel>()); // populated one.
Use this list in the View.
#model IList<GroupViewModel>
#for (int i = 0; i < Model.Count; i++)
{
<h4>Model[i].Code</h4>
<select id="e-#i" multiple="multiple">
#foreach (Subject subject in Model[i].AllSubjects)
{
<option value="#subject.Name">#subject.Name</option>
}
</select>
}
There is nothing especial to deal with this situation, except that you have missed the tag name of the select element.
To be exact, all html elements such as select you have used here, should have a name not id (id="e-#i") and all elements are serialized based their names and sent to server. On the other side, at server-side, you should get the posted values which are in a csv formatted (due to multiple ability added you have added to select element)
Solved my problem by simplifying the task. What i had to to: I created new ViewModel for this thing. I replaced tag <select></select> with #Html.ListBoxFor(m => m.Subjects, Model.SubjectItems). I had to create a SubjectItems list in my ViewModel.
Here's the code (Sorry for tons of code: I just want to make everything clear):
My View:
#using System
#using System.Linq
#using TimeTable.GenericRepository
#model TimeTable.Models.GroupViewModel
#{
//it's better to move the next line to a Controller later
ViewBag.GroupId = new SelectList(new GroupRepository().Get().ToList(), "Id", "Code", Model.GroupId);
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_LayoutBootstrap.cshtml";
}
<h2>Index</h2>
<hr />
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary(true)
<div class="form-group">
#Html.LabelFor(model => model.GroupId, "Group is: ", new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("GroupId", String.Empty)
#Html.ValidationMessageFor(model => model.GroupId)
</div>
</div>
#Html.ListBoxFor(m => m.Subjects, Model.SubjectItems, new { #id = "e", #style = "width:80%; " })
<br /><br />
<input type="submit" value="Generate" class="btn btn-default" />
</div>
}
#* ReSharper disable once Razor.SectionNotResolved *#
#section Scripts {
#Styles.Render("~/Content/select2")
#Scripts.Render("~/bundles/select2")
<script type="text/javascript">
$(function () { $("#e").select2(); });
</script>
}
My Controller:
public ActionResult Generate()
{
return View(new GroupViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Generate(GroupViewModel gvm)
{
var subjects= gvm.Subjects; // <== selected subjects are here
return View();
}
My Model:
public class GroupViewModel
{
public int GroupId{ get; set; }
public Group Group {
get { return new GroupRepository().GetById(GroupId); }
}
public object Subjects { get; set; }
public IEnumerable<SelectListItem> SubjectItems
{
get
{
var items = new SelectList(new SubjectRepository().Get().ToList(), "Id", "Name");
return items;
}
}
}
P.S. Select2 is a custom replacement for selectBoxes, it's not necessary, but I like it: http://ivaynberg.github.io/select2/