ASP.NET MVC 5 group of radio buttons - c#

I am starting my first ASP.NET MVC project, so I have one simple question.
I have following code:
foreach(var question in Model.GeneralQuestions)
{
<div class = "well">
<h3>
<strong>#question.QuestionString</strong>
</h3>
#foreach (var answer in question.PossibleAnswers)
{
#Html.RadioButtonFor(model => question.QuestionString, answer.Answer)
#Html.Label(answer.Answer)
<br />
}
</div>
}
All questions in Model.GeneralQuestions are unique, so radio buttons should be divided into groups by name attribute (for each question one group of radio buttons). But this code produces only one group, so when I answer second question first one becomes deselected.
What do I need to change?
EDIT
My model looks like:
public class StudentViewModel
{
public Student Student { get; set; }
public List<Question> GeneralQuestions { get; set; }
public List<SubjectQuestions> SubjectQuestions { get; set; }
}
public class Student
{
public int StudentID { get; set; }
public string Index { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public virtual ICollection<Subject> Subjects { get; set; }
}
public class Question
{
public int QuestionID { get; set; }
public string QuestionString { get; set; }
public bool IsAssociatedWithSubject { get; set; }
public virtual ICollection<PossibleAnswer> PossibleAnswers { get; set; }
public virtual ICollection<Results> Results { get; set; }
}
public class SubjectQuestions
{
public Subject Subject { get; set; }
public List<Question> Questions { get; set; }
}
public class Results
{
public int ResultsID { get; set; }
public int QuestionID { get; set; }
public int? SubjectID { get; set; }
public int PossibleAnswerID { get; set; }
public virtual Question Question { get; set; }
public virtual PossibleAnswer PossibleAnswer { get; set; }
public virtual Subject Subject { get; set; }
}
In one instance of StudentViewModel I save one student and all questions that he should answer (both general and related to subjects he is studying) and pass it to view. In view I put all questions in single form and they are all type of radio. So, can anyone help me with grouping of radio buttons and posting back this form correctly?

There are a number of problems with your code including generating duplicate id's (invalid html), generating duplicate name attributes (which is why you're creating only one group, but more importantly this will prevent you from binding to the model when you post back) and you're not actually binding to a valid property anyway.
You will need to create view models to represent what you want to display and edit and generate the radio buttons in a for loop (or using an EditorTemplate) so they are correctly named with indexers.
View models
public class QuestionVM
{
public int ID { get; set; } // for binding
public string Text { get; set; }
[Required]
public int? SelectedAnswer { get; set; } // for binding
public IEnumerable<AnswerVM> PossibleAnswers { get; set; }
}
public class SubjectVM
{
public int? ID { get; set; }
[DisplayFormat(NullDisplayText = "General")]
public string Name { get; set; }
public List<QuestionVM> Questions { get; set; }
}
public class AnswerVM
{
public int ID { get; set; }
public string Text { get; set; }
}
public class StudentVM
{
public int ID { get; set; }
public string Name { get; set; }
// plus any other properties of student that you want to display in the view
public List<SubjectVM> Subjects { get; set; }
}
View
#model YourAssembly.StudentVM
#using(Html.BeginForm())
{
#Html.HiddenFor(m => m.ID)
#Html.DisplayFor(m => m.Name)
for(int i = 0; i < Model.Subjects.Count; i++)
{
#Html.HiddenFor(m => m.Subjects[i].ID)
#Html.DisplayFor(m => m.Subjects[i].Name) // will display "General" if no name
for (int j = 0; j < Model.Subjects[i].Questions.Count; j++)
{
#Html.HiddenFor(m => m.Subjects[i].Questions[j].ID)
#Html.DisplayFor(m => m.Subjects[i].Questions[j].Text)
foreach(var answer in Model.Subjects[i].Questions[j].PossibleAnswers )
{
<div>
#Html.RadioButtonFor(m => m.Subjects[i].Questions[j].SelectedAnswer, answer.ID, new { id = answer.ID})
<label for="#answer.ID">#answer.Text</label>
</div>
}
#Html.ValidationMessageFor(m => m.Subjects[i].Questions[j].SelectedAnswer)
}
}
<input type="submit" value="save" />
}
Controller
public ActionResult Edit(int ID)
{
StudentVM model = new StudentVM();
// populate your view model with values from the database
return View(model);
}
[HttpPost]
public ActionResult Edit(StudentVM model)
{
// save and redirect
}
Note I am a little confused by the database structure implied by your models (for example why do you need separate models for Question and SubjectQuestion when a null value for SubjectID identifies it as a "General" question). I suggest you start by just hard-coding some values in the GET method to see how it works and posts back.
StudentVM model = new StudentVM();
model.ID = 1;
model.Name = "bambiinela";
model.Subjects = new List<SubjectVM>()
{
new SubjectVM()
{
Questions = new List<QuestionVM>()
{
new QuestionVM()
{
ID = 1,
Text = "Question 1",
SelectedAnswer = ?, // set this if you want to preselect an option
PossibleAnswers = new List<AnswerVM>()
{
new AnswerVM()
{
ID = 1,
Text = "Answer A"
},
new AnswerVM()
{
ID = 1,
Text = "Answer B"
}
}
},
new QuestionVM()
{
ID = 2,
Text = "Question 2",
PossibleAnswers = new List<AnswerVM>()
{
// similar to above
}
}
}
},
new SubjectVM()
{
ID = 1,
Name = "Math",
Questions = new List<QuestionVM>()
{
// similar to above
}
}
};
When you post, the model is populated with the ID of the selected answer for each question in each subject. Note the use of DisplayFor() for some properties. These won't post back so you would need to repopulate these properties if you return the view (e.g. ModelState is not valid). Alternatively you can generate a read-only textbox or add a hidden input for those properties. I also suggest you inspect the HTML that is generated, in particular the name attributes which will look something like
<input type="radio" name="Subjects[0].Questions[0].SelectedAnswer" ...
to give you an understanding of how collections are bound to your model on post back

The trick is to use an expression (first parameter to Html.RadioButtonFor) which contains a value that changes per group of radio-buttons. In your case, it would be an index in the list of questions.
Here is some sample code:
#for (int i = 0; i < Model.GeneralQuestions.Count; i++)
{
var question = Model.GeneralQuestions[i];
#Html.Label(question.QuestionString)
<br />
foreach (var answer in question.PossibleAnswers)
{
#Html.RadioButtonFor(model =>
Model.GeneralQuestions[i].SelectedAnswerId, answer.Id)
#Html.Label(answer.Answer)
<br />
}
}
This produces the following HTML:
<label for="Q1">Q1</label>
<br />
<input id="GeneralQuestions_0__SelectedAnswerId"
name="GeneralQuestions[0].SelectedAnswerId" type="radio" value="1" />
<label for="A01">A01</label>
<br />
<input id="GeneralQuestions_0__SelectedAnswerId"
name="GeneralQuestions[0].SelectedAnswerId" type="radio" value="2" />
<label for="A02">A02</label>
<br />
<label for="Q2">Q2</label>
<br />
<input id="GeneralQuestions_1__SelectedAnswerId"
name="GeneralQuestions[1].SelectedAnswerId" type="radio" value="11" />
<label for="A11">A11</label>
<br />
<input id="GeneralQuestions_1__SelectedAnswerId"
name="GeneralQuestions[1].SelectedAnswerId" type="radio" value="12" />
<label for="A12">A12</label>
<br />
And for sake of completeness, here is a reduced version of the models used:
public class StudentViewModel
{
public List<Question> GeneralQuestions { get; set; }
}
public class Question
{
public int QuestionId { get; set; }
public string QuestionString { get; set; }
public ICollection<PossibleAnswer> PossibleAnswers { get; set; }
public int SelectedAnswerId { get; set; }
}
public class PossibleAnswer
{
public int Id { get; set; }
public string Answer { get; set; }
}
and here is the code from the action method:
return View(new StudentViewModel
{
GeneralQuestions =
new List<Question>
{
new Question
{
QuestionString = "Q1",
PossibleAnswers =
new[]
{
new PossibleAnswer {Id = 1, Answer = "A01"},
new PossibleAnswer {Id = 2, Answer = "A02"}
}
},
new Question
{
QuestionString = "Q2",
PossibleAnswers =
new[]
{
new PossibleAnswer {Id = 11, Answer = "A11"},
new PossibleAnswer {Id = 12, Answer = "A12"}
}
},
}
});

Related

MVCCheckBoxList catering for a 1 to Many relationship

I have a class for which has a 1 to many relationship with another class. for this I will use class Car and class Gears. I need to create a form, which registers a car and the user needs to specify a choice of gears.
public class Car
{
public int id { get; set; }
public string desc { get; set; }
public List<Gear> Gears { get; set; }
}
public class Gear
{
public int gid { get; set; }
public int gname { get; set; }
}
using asp.net MVC 5, I have a create form, which I have scaffolded to the Car model, and within the form, I wish to have a checkboxlist of gears,
I also have a ViewModel that I have provided for my checkboxlist which is as below:
public class GearsViewModel
{
public Gear _gear {get; set; }
public bool _isChecked {get; set;}
}
Controller looks like:
Gears fetched from db context will be
"GearR","Gear1","Gear2","Gear3","Gear4","Gear5","Gear6","Gear7"
public action Create()
{
ViewBag.Gears = new SelectList(db.Gears, "gid","gname");
List<GearViewModel> _gears= new List<GearViewModel>();
foreach(Gear G in ViewBag.Gears)
{
_gears.Add(new GearViewModel(G, false));
}
ViewBag.GearsCheckList = _gears.ToList();
return View();
}
Now, this is the part I'm getting stuck at, is how to display and capture details in the CreateView.
I need assistance on how to design the Create form and how I will capture the info.
Firstly, view models should not contain data models when editing. You view models should be (add validation and display attributes as appropriate)
public class CarVM
{
public int? ID { get; set; }
public string Description { get; set; }
public List<GearVM> Gears { get; set; }
}
public class GearVM
{
public int ID { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
and the GET method will be
public ActionResult Create()
{
var gears = db.Gears;
CarVM model = new CarVM
{
Gears = gears.Select(x => new GearVM
{
ID = x.gid,
Name = x.gname
}).ToList()
};
return View(model);
}
and the view will then be
#model CarVM
....
#using (Html.BeginForm())
{
..... // elements for editing ID and Description properties of CarVM
#for (int i = ; i < Model.Gears.Count; i++)
{
<div>
#Html.HiddenFor(m => m.Gears[i].ID)
#Html.HiddenFor(m => m.Gears[i].Name) // include if your want to get this in the POST method as well
#Html.CheckboxFor(m => m.Gears[i].IsSelected)
#Html.LabelFor(m => m.Gears.IsSelected, Model.Gears[i].Name)
</div>
}
<input type="submit" .... />
}
Then in the POST method
public ActionResult Create(CarVM model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// To get the ID's of the selected gears
IEnumerable<int> selected = model.Gears.Where(x => x.IsSelected).Select(x => x.ID);
// Initialize your data models, save and redirect
}

ASP.NET MVC Many-to-many relationship with viewmodel

I'm trying to display a list of books that will show the details for each book. It all works properly, except for the properties which have many-to-many relationships with the Books model.
Here is my Book model (I removed annotations for readability):
public class Book
{
public int BookId { get; set; }
public string title { get; set; }
public Int32 isbn { get; set; }
public string author { get; set; }
public string summary { get; set; }
public string series { get; set; }
public string amazonLink { get; set; }
public string pubLink { get; set; }
public int? GradeLevelId { get; set; } //Foreign Key for GradeLevel
public bool needsEdit { get; set; }
public int? LexileLevelId { get; set; } //Foreign Key for LexileLevel
public DateTime dateAdded { get; set; }
public Book()
{
dateAdded = DateTime.Now;
}
public string comments { get; set; }
public virtual GradeLevel GradeLevel { get; set; }
public virtual LexileLevel LexileLevel { get; set; }
//Navigation Properties
public virtual ICollection<Recommendation> Recommendations { get; set; }
public virtual ICollection<RelevantGenre> RelevantGenres { get; set; }
}
The two navigation properties (Recommendation and RelevantGenre) are for the associative/joining tables, and that's where I'm having issues. To keep things simple, I'm going to focus on the RelevantGenre model. Each book can have more than one Genre, so the RelevantGenre is the join table between Book and Genre.
Here's the Model for those:
public class RelevantGenre
{
//Both are primary keys
[Key]
[Column(Order = 1)]
public int BookId { get; set; } //Foreign Key to Book
[Key]
[Column(Order = 2)]
public int genreId { get; set; } //Foreign Key to Genre
public virtual Book Book { get; set; } //Nav property
public virtual Genre Genre { get; set; } //Nav property
}
public class Genre
{
public int GenreId { get; set; }
public string genreTitle { get; set; }
public int genreOrder { get; set; }
//Navigation Property to RelevantGenre
public ICollection<RelevantGenre> RelevantGenres { get; set; }
}
Here's the Controller:
// GET: Books
public ActionResult Index(string filter, string searchString)
{
var viewModel = new BookListViewModel();
if (String.IsNullOrEmpty(searchString) && String.IsNullOrEmpty(filter))
{
var results = from b in db.Books select b;
var resultsList = (results.ToList());
viewModel.Books = resultsList;
return View(viewModel);
}
else
{
var results = from b in db.Books select b;
//Filtering the book list
switch (filter)
{
case "HR":
results = from b in db.Books
join r in db.Recommendations
on new { b.BookId } equals
new { r.BookId }
where (r.RecommendationTypeId == 1)
select b;
break;
default:
results = from b in db.Books select b;
break;
}
if(!String.IsNullOrEmpty(searchString))
{
//Search query results
var searchResults = from b in db.Books
.Where(model => model.title.Contains(searchString) || model.author.Contains(searchString)
|| model.series.Contains(searchString))
select b;
if (searchResults != null )
{
results = searchResults;
}
else
{
ViewBag.SpanText = "Sorry, no results founds. Please try your search again.";
}
}
var resultsList = (results.ToList());
viewModel.Books = resultsList;
return View(viewModel);
}
}
As you can see, it's returning a viewModel, because I thought that made the most sense for how to return a combination of model data.
Here's the viewmodel:
public class BookListViewModel
{
public List<Book> Books { get; set; }
public int BookId { get; set; }
public string title { get; set; }
public string author { get; set; }
public Int32 isbn { get; set; }
public string series { get; set; }
public int? GradeLevelId { get; set; }
public string gradeLevelName { get; set; }
public int? LexileLevelId { get; set; }
public string lexileLevelName { get; set; }
public Recommendation Recommendation { get; set; }
public int RecommendationTypeId { get; set; }
public string recName { get; set; }
public RelevantGenre RelevantGenre { get; set; }
public int genreId { get; set; }
}
And lastly, here's the view:
#model FavBooks.ViewModels.BookListViewModel
#{
ViewBag.Title = "All Books";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Books</h2>
<p class="details">
#Html.ActionLink("Browse All Books in List Format", "FullList", "Books")
</p>
#foreach (var item in Model.Books)
{
<div class="row fullBorder">
<div class="col-md-2 col-sm-2">
<img src="~/Content/Images/harrypotterbook.png" class="bookThumb" alt="Book Image" />
</div>
<div class="col-md-3 col-xs-3">
<h3>
<a href="#Url.Action("Details", "Books", new { id = item.BookId })" class="darkLink bookTitle">
#Html.DisplayFor(modelItem => item.title)
</a>
</h3>
<h4>By #Html.DisplayFor(modelItem => item.author)</h4>
<p><strong>ISBN:</strong> #Html.DisplayFor(modelItem => item.isbn)</p>
</div>
<div class="col-md-3 col-sm-4 bookMargins">
#{
if (item.series != null)
{
<p><strong>Series:</strong> #Html.DisplayFor(modelItem => item.series) </p>
}
else
{
<p></p>
}
}
<p><strong>Grade Level:</strong> #Html.DisplayFor(modelItem => item.GradeLevel.gradeLevelName )</p>
<p><strong>Lexile Level:</strong> #Html.DisplayFor(modelItem => item.LexileLevel.lexileLevelName)</p>
</div>
<div class="col-md-4 col-sm-3">
#Html.ActionLink("View Book Details", "Details", "Books", new { id = item.BookId }, new { #class="btn btn-default btnBookDetails" })
</div>
</div>
}
You can see that my view displays a list of items from Model.Books using a foreach loop. For each book, I'd like it to also display the RelevantGenres that are connected to the book, but it's not letting me. The GradeLevel and LexileLevel properties connect just fine (those are one-to-many), but it doesn't seem to register any of the many-to-many relationships which are not directly part of the Book model.
I feel like I'm missing something basic here, or maybe there's an issue with my view-model setup. Do you see where I went wrong on this or what I can do to display each book's genres?
EDIT:
Let me get more specific with what I tried.
I saw here that it's possible to use a foreach inside of another foreach to display a loop. But when I try that, it tells me that the "foreach cannot operate on that... because Favbooks.Models.Book does not contain a public definition for GetEnumerator". So I tried changing the #model to an IEnumerable<> and looping through the whole Model (instead of foreach(var item in Model.Books) but then it still wouldn't work. In that situation, it gave me an error saying:
'BookListViewModel' does not contain a definition for 'RelevantGenres' and no extension method 'RelevantGenres' accepting a first argument of type 'BookListViewModel' could be found (are you missing a using directive or an assembly reference?)
Because that wasn't working, I kept the #model with #model FavBooks.ViewModels.BookListViewModel like it was initially, and and tried putting in #Html.DisplayFor(modelItem => item.Genres.genreTitle) but it doesn't recognize Genre or RelevantGenre.
To sum up, the issue is that if I loop through Model.Books, then it won't recognize anything in the viewmodel other than the Books list. But if I loop through the overall Model, then it still won't recognize the RelevantGenres, and now it started giving me another error like this:
The model item passed into the dictionary is of type 'FavBooks.ViewModels.BookListViewModel', but this dictionary requires a model item of type 'System.Collections.Generic.IEnumerable`1[FavBooks.ViewModels.BookListViewModel]'.
I'm sorry if this isn't totally clear. I haven't worked so much with viewmodels before and I see that I must have set it up wrong, but I just don't know how to get this working...
Have you tried .Include()? This will populate all the related data in the navigation property. Refer https://learn.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/reading-related-data-with-the-entity-framework-in-an-asp-net-mvc-application for more detials.
eg:
var BookList = _context.Book.Where(m=>m.BookId==id).Include(m=>m.Recommendations).Include(m=>m.RelevantGenres)
This will query a record of Book whose BookId=id and will populate all the related navigation properties.
There's also .ThenInclude()
A property of public List<Book> Books { get; set; } should be enough as this will store all the data related to your retrieved entity.
and below is example of how you can access the fields of a navigation property.
#model BookListViewModel
foreach(var b in Model.Books)
{
#HtmlDisplayFor(modelItem=>b.title)
foreach(var r in b.RelevantGenres)
{
#HtmlDisplayFor(modelItem=>r.GenereName)
}
}
Myself being fairly new to .netcore mvc, I don't know much either. But I hope this helps to point you in the right direction if not completely solve your issue.

MVC 6 Modelbinding complex dictionaries

I'm trying to post a form containing a dictionary of complex types.
The dictionary looks like this:
public Dictionary<Question, List<QuestionAlternative>> Questions { get; set; }
One question can have many alternatives.
The question itself looks like this:
public class Question
{
public Guid Id { get; set; }
public string Text { get; set; }
public Guid TestId { get; set; }
public Test Test { get; set; }
public ICollection<QuestionAlternative> QuestionAlternatives { get; set; }
public Guid? QuestionTypeId { get; set; }
public QuestionType QuestionType { get; set; }
}
And then there's the QuestionAlternative:
public class QuestionAlternative
{
public Guid Id { get; set; }
public string Alternative { get; set; }
public Guid QuestionId { get; set; }
public Question Question { get; set; }
}
My form looks like this:
<form asp-controller="Answer" asp-action="AnswerMe" method="post">
#foreach (var questionPair in Model.Questions)
{
#Html.Hidden("Questions[" + questionIndex + "].Key.Id", questionPair.Key.Id)
<br />
foreach (var alternative in questionPair.Value)
{
<label for="#alternative.Id">#questionPair.Key.Text</label>
<textarea id="#alternative.Id" name="#("Questions["+ questionIndex + "].Value["+ index +"]")"></textarea>
index++;
}
}
questionIndex++;
}
<input type="submit" class="button" value="Answer" />
</form>
The ActionResult that i'm posting to looks like this:
[AllowAnonymous]
[HttpPost("AnswerMe")]
public IActionResult AnswerMe([FromBody]AnswerViewModel model)
{
foreach(var item in model.Questions) //throws exception
....
}
and my viewmodel:
public class AnswerViewModel
{
public ODELDAL.Entities.Test Test { get; set; }
public Dictionary<Question, List<QuestionAlternative>> Questions { get; set; }
public Guid SelectedRole { get; set; }
}
When I post my form the dictionary named Questions is null.
Is it possible to achieve this with the default modelbinder or do i need to build a custom one.
If so, how would that solution look like?
Thanks in advance
Since a Question already possesses a collection of QuestionAlternative why not use a simple collect (List, array....etc) in your view model...
public class AnswerViewModel {
public ODELDAL.Entities.Test Test { get; set; }
public Question[] Questions { get; set; }
public Guid SelectedRole { get; set; }
}
...and an updated view...
<form asp-controller="Answer" asp-action="AnswerMe" method="post">
#for(int i = 0; i < Model.Questions.Length; i++) {
#Html.HiddenFor(m => m.Questions[i].Id)
<br />
for(int j = 0; j < m.Questions[i].QuestionAlternatives.Count; j++) {
#Html.LabelFor(m => m.Questions[i].QuestionAlternatives[j].Alternative,m.Questions[i].QuestionAlternatives[j].Text)
#Html.TextAreaFor(m => m.Questions[i].QuestionAlternatives[j].Alternative)
}
}
<input type="submit" class="button" value="Answer" />
</form>
The model binder will have an easier task of reconstructing your model using this approach as it will be able to use the expressions to generate the ids for the html tags.
For example, the hidden input will look something like this when generated
<input data-val="true" id="Questions_0__Id" name="Questions[0].Id" value="{Some-Guid-Value_Here}" type="hidden">

Nested lookup in MVC and Entity Framework

I'm running into a problem with a project I'm working on. I'm relatively new to MVC so I'm hoping this is something simple!
I'm trying to perform an edit on an object, but the object in question has a couple of ICollections that it can't seem to bind to.
The class I'm trying to update is called 'Key', and the definition is:
public partial class Key
{
public Key()
{
this.KeyFields = new HashSet<KeyField>();
this.KeyServices = new HashSet<KeyService>();
}
public int ID { get; set; }
public System.Guid APIKey { get; set; }
public string SiteURL { get; set; }
public bool Active { get; set; }
public virtual ICollection<KeyField> KeyFields { get; set; }
public virtual ICollection<KeyService> KeyServices { get; set; }
}
The KeyField and KeyService classes are:
public partial class KeyField
{
public int ID { get; set; }
public int SiteKey { get; set; }
public int Field { get; set; }
public bool Active { get; set; }
public virtual Field Field1 { get; set; }
public virtual Key Key { get; set; }
}
public partial class KeyService
{
public int ID { get; set; }
public int SiteKey { get; set; }
public int Service { get; set; }
public bool Active { get; set; }
public virtual Key Key { get; set; }
public virtual Service Service1 { get; set; }
}
The related Service class is:
public partial class Service
{
public Service()
{
this.KeyServices = new HashSet<KeyService>();
}
public int ID { get; set; }
public string Name { get; set; }
public int Service_View { get; set; }
public virtual ICollection<KeyService> KeyServices { get; set; }
public virtual View View { get; set; }
[NotMapped]
public bool IsSelected { get; set; }
}
and the related Field class is:
public partial class Field
{
public Field()
{
this.KeyFields = new HashSet<KeyField>();
}
public int ID { get; set; }
public string Title { get; set; }
public int Field_View { get; set; }
public virtual View View { get; set; }
public virtual ICollection<KeyField> KeyFields { get; set; }
[NotMapped]
public bool IsSelected { get; set; }
}
The View is using a custom model called 'KeyDetailsModel'. It contains an instance of the 'Key' object and 2 IEnumerables. These are there so that I can output all the fields and services in the database into a list of checkboxes on the View. The fields and services that should be preselected on the View are ICollections in the Key class.
the definition is:
public class KeyDetailsModel
{
public Key Key { get; set; }
public IEnumerable<Field> Fields { get; set; }
public IEnumerable<Service> Services { get; set; }
}
The is the code in the controller that sets up the 'display' page:
public ViewResult KeyDetails(int id)
{
var fieldChannel = new Repo<Field>();
var serviceChannel = new Repo<Service>();
//gets the key information
var key = _keyChannel.GetById(id);
//gets all the fields in the database
var fields = fieldChannel.GetAll();
//gets all the services in the database
var services = serviceChannel.GetAll();
//gets the KeyFields for the key, and sets the IsSelected flag for the related Field
foreach (var f in fields)
{
var selectedField = (from sf in key.KeyFields
where sf.ID == f.ID && sf.SiteKey == id
select sf).FirstOrDefault();
if (selectedField != null)
{
f.IsSelected = true;
}
}
//gets the KeyServices for the key, and sets the IsSelected flag for the related Service
foreach (var f in services)
{
var selectedService = (from ss in key.KeyServices
where ss.ID == f.ID && ss.SiteKey == id
select ss).FirstOrDefault();
if (selectedService != null)
{
f.IsSelected = true;
}
}
//create the model
var KeyDetailsModel = new KeyDetailsModel
{
Fields = fields,
Key = key,
Services = services
};
return View(KeyDetailsModel);
}
This is the method in the controller that will perform the edit:
[HttpPost]
public ActionResult KeyDetails(KeyDetailsModel KeyDetailsModel)
{
if (KeyDetailsModel != null && ModelState.IsValid)
{
return View(KeyDetailsModel);
}
else
{
return View(KeyDetailsModel);
}
}
Finally, the cshtml page is:
#model EdinburghNapier.EAWebLayer.Admin.Models.KeyDetailsModel
#{
ViewBag.Title = "KeyDetails";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Key Details</h2>
#using (Html.BeginForm("KeyDetails", "Key", FormMethod.Post))
{
<fieldset>
<legend>Key</legend>
<div class="display-label">
#Html.DisplayNameFor(model => model.Key.APIKey)
</div>
<div class="display-field">
#Html.EditorFor(model => model.Key.APIKey)
</div>
<div class="display-label">
#Html.DisplayNameFor(model => model.Key.SiteURL)
</div>
<div class="display-field">
#Html.EditorFor(model => model.Key.SiteURL)
</div>
<div class="display-label">
#Html.DisplayNameFor(model => model.Key.Active)
</div>
<div class="display-field">
#Html.EditorFor(model => model.Key.Active)
</div>
<div class="display-field">
<ul>
#foreach (var f in Model.Fields)
{
<li>
<input type="checkbox"
name="Key.KeyFields" value="#f.ID" id="Field_#f.ID"
checked="#f.IsSelected"/>
<label for="#f.ID">#f.Title</label>
</li>
}
</ul>
</div>
<div class="display-field">
<ul>
#foreach (var f in Model.Services)
{
<li>
<input type="checkbox"
name="Key.KeyServices" value="#f.ID" id="Service_#f.ID"
checked="#f.IsSelected"/>
<label for="#f.ID">#f.Name</label>
</li>
}
</ul>
</div>
</fieldset>
#Html.HiddenFor(model => model.Key.ID)
<input type="submit" value="Save"/>
}
What is happening when I submit the form is that the 'KeyFields' and 'KeyServices' properties of the 'Key' class in the 'KeyDetailsModel' are always 0. The edit screen doesn not seem to be able to bind the check box lists to the properties.
One consequence of this is that the status of the ModelState is always false when I submit.
I've been bashing my head against a wall over this for a couple of days now, hope someone can help! I appreciate that I may have supplied too much code and too little explanation - please let me know if that's the case!
You are binding your checkboxes in incorrect way, you should read a bit more about it, you can check some article like this:
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
Your code should be changed, so that each checkbox had an indexer in its name:
<div class="display-field">
<ul>
#for (var i = 0; i < Model.Fields.Count(); i++)
{
<li>
<input type="checkbox"
name="#Html.NameFor(x=>x.Fields[i])" value="#Model.Fields[i].ID" id="#Html.IdFor(x=>x.Fields[i])"
checked="#Model.Fields[i].IsSelected" />
<label for="#Html.NameFor(x=>x.Fields[i])">#Model.Fields[i].Title</label>
</li>
}
</ul>
</div>
<div class="display-field">
<ul>
#for (var i = 0; i < Model.Services.Count(); i++)
{
<li>
<input type="checkbox"
name="#Html.NameFor(x=>x.Services[i])" value="#Model.Services[i].ID" id="#Html.IdFor(x=>x.Services[i])"
checked="#Model.Services[i].IsSelected" />
<label for="#Html.NameFor(x=>x.Services[i])">#Model.Services[i].Name</label>
</li>
}
</ul>
</div>
You will also have to make your Services and Fields properties an array here:
public class KeyDetailsModel
{
public Key Key { get; set; }
public Field[] Fields { get; set; }
public Service[] Services { get; set; }
}
Alternately, you can just use #Html.CheckBoxFor(x=>x.Fields[i].IsSelected inside the cycle, stick to Mvc Helpers, they really make the difference.

Complex ViewModel - View cannot Automap values from model Advice Needed..

I have a complex View. It has data from 4 Models. The models are all static and work as expected. I have created a ViewModel to attempt to show just the data needed for this view. It is made up of Competitors and some complex Classes and Events they participate in.
I have made a complex ViewModel. When I walk through the Controller, I can see all three parts being constructed from the ViewModel. Its all there including data. When I try to map the values using Intellesense in the View, it has no way of knowing this data, or has no mapping from the complex ViewModel. Am I doing this right? I have tried several ways to map these values to the View. I think I need to initialize or map the values to the Models derived from, I just cannot figure out how.
Please advise on how to map these values, data elements to the view.
ViewModel:
Compeditor is an from an actual model direct to the DB
The rest of the data is gathered from multiple tables and passed to view from controller
namespace eManager.Web2.Models
{
public class CompDetailPlus
{
public CompDetailPlus()
{
this.Compeditor = new Compeditor();
}
public virtual Compeditor Compeditor { get; set; }
public virtual IEnumerable<InEventClass> InEventClass { get; set; }
public virtual IEnumerable<AllEventClasses> AllEventClasses { get; set; }
}
public class Compeditor
{
[Key]
public virtual int CompeditorId { get; set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual string MiddleInt { get; set; }
public virtual string StreetAddress { get; set; }
public virtual string City { get; set; }
public virtual string State { get; set; }
public virtual string PostalCode { get; set; }
public virtual string EmailAddress { get; set; }
public virtual string HomePhone { get; set; }
public virtual string CellPhone { get; set; }
public virtual double Height { get; set; }
public virtual double Weight { get; set; }
public virtual int Age { get; set; }
public virtual int Event_CompId { get; set; }
}
public class InEventClass
{
public virtual int EventClassID { get; set; }
public virtual string ClassName { get; set; }
public virtual bool IsSelected { get; set; }
}
//duplicate to simplify how the second list is pulled and then combined with first list
public class AllEventClasses
{
public virtual int EventClassID { get; set; }
public virtual string ClassName { get; set; }
public virtual bool IsSelected { get; set; }
}
}
Controller:
public ActionResult CompeditorDetail(int CompeditorId)
{
//Pull the Competitor detail for the ID passed in
var comp = _db.Compeditors.Single(c => c.CompeditorId == CompeditorId);
//Pull a list of Event-Classes the competitor is already signed up for on current event
var nlist = (from o in _db.Compeditors
join o2 in _db.Event_Class_Compeditors_s on o.CompeditorId equals CompeditorId
where o.CompeditorId.Equals(CompeditorId)
join o3 in _db.Event_Classes on o2.EventClassID equals o3.EventClassID
where o2.EventClassID.Equals(o3.EventClassID)
join o4 in _db.Class_Definitions on o3.ClassID equals o4.Class_Definition_ID
where o3.ClassID.Equals(o4.Class_Definition_ID)
select new InEventClass()
{
ClassName = o4.Class_Name,
EventClassID = o2.EventClassID,
IsSelected = true
}).ToList();
//pull a complete list of Event Classes that are avaiaible
var totallist = (from o in _db.Event_Classes
join o2 in _db.Event_Classes on o.ClassID equals o2.ClassID
where o.ClassID.Equals(o2.ClassID)
join o3 in _db.Class_Definitions on o2.ClassID equals o3.Class_Definition_ID
where o2.ClassID.Equals(o3.Class_Definition_ID)
join o4 in _db.Events on o.EventID equals o4.EventID
where o.EventID.Equals(o4.EventID)
where o4.CurrentEvent.Equals(true)
select new AllEventClasses()
{
ClassName = o3.Class_Name,
EventClassID = o2.EventClassID,
IsSelected = false
}).ToList();
var whatsleft = totallist.Where(eachtotalclass => !(nlist.Any(eachClassIHave => eachClassIHave.EventClassID == eachtotalclass.EventClassID))).ToList();
var model = new CompDetailPlus { AllEventClasses = whatsleft, Compeditor = comp, InEventClass = nlist };
return View(model);
}
View:
(Has to show the Competitor detail and a compound list of Event_Classes they are in)
In the view, I cannot see the values for any data.. all error on run and no good for display.
#model IEnumerable<eManager.Web2.Models.CompDetailPlus>
#{
ViewBag.Title = "Competitor's Detail";
}
<h2>#ViewBag.Title</h2>
<fieldset>
<legend>Compeditor</legend>
<table border="1" >
<tr>
<td>
<div class="display-field">
#Html.HiddenFor(model => model.Compeditor.CompeditorId)
</div>
<b>First Name</b>
<div class="display-field">
#Html.DisplayFor(model => model.Compeditor.FirstName)
</div>
</td>
<td>
<b>Last Name</b>
<div class="display-field">
#Html.DisplayFor(model => model.Compeditor.LastName)
</div>
</td>
#using (Html.BeginForm("CompeditorDetail", "Compeditor", FormMethod.Post))
{
foreach (var item in Model)
{
<input type="checkbox" name="MyID" value="#item.AllEventClasses.IsSelected"/> #item.InEventClass.ClassName <br />
<input type="hidden" name="CompeditorID" value="#item.InEventClass.CompeditorId" />
}
}
</td>
Your View accepts a model of IEnumerable eManager.Web2.Models.CompDetailPlus which would be fine, but your controller is sending a single eManager.Web2.Models.CompDetailPlus object.
Try changing this in your View
#model IEnumerable<eManager.Web2.Models.CompDetailPlus>
to this:
#model eManager.Web2.Models.CompDetailPlus
And change the bottom part of your view so that it's iterating through Enumerable compaosite items inside your model.

Categories