Viewmodel nested checkbox not binding on post - asp.net

I have a subscription form that contains a matrix of options. The form can be seen in screenshot Subscription table
I am having trouble with ASP.NET MVC generating appropriate ID's and then on postback having the binder populate the model with the form selections.
The add on name is down the left side and when posted back the collection of SubscriptionInputModel.Addons get populated ok. But SubscriptionInputModel.Addons[i].SubscriptionLevelCombos is null as seen in debug screenshot
The current code is using CheckBoxFor but I've also tried manually generating ID's in format:
#Html.CheckBox("addon[" + a + "].SubscriptionLevelCombo[" + i + "].AddonSelected", addon.SubscriptionLevelCombos[i].AddonSelected)
Neither format has worked and also experimented while debugging but no luck. I would appreciate any ideas. Worst case I assume I would need to read the raw form collection?
I assume the level of nested object shouldn't matter as it is all object path notation and array indexes in html tag names?
Here are snippets of current code to help illustrate what exists.
View Models
public class SubscriptionInputModel
{
//other stuff to come
//....
//add on's, listed down left of table
public List<SubscriptionInputAddonModel> Addons;
}
public class SubscriptionInputAddonModel
{
public int Id { get; set; }
public string Name { get; set; }
public string Note { get; set; }
public List<SubscriptionInputAddonComboModel> SubscriptionLevelCombos { get; set; }
}
public class SubscriptionInputAddonComboModel
{
public int? Id { get; set; }
public decimal? AddonCost { get; set; }
public CostTimeUnitOption? CostTimeUnit { get; set; }
public bool? IsComplimentaryBySubscriptionLevel { get; set; }
public string ComboText { get; set; }
public bool AddonSelected { get; set; }
public int? AddonId { get; set; }
}
SubscriptionController
[Route("identity/subscription")]
// GET: Subscription
public ActionResult Index()
{
SubscriptionInputModel model = new SubscriptionInputModel();
ArrbOneDbContext db = new ArrbOneDbContext();
List<SubscriptionInputAddonModel> addons = Mapper.Map<Addon[], List<SubscriptionInputAddonModel>>(db.Addons.OrderBy(a => a.OrderPosition).ToArray());
model.Addons = addons;
foreach(var addon in model.Addons)
{
var addonCombos = db.Database.SqlQuery<SubscriptionInputAddonComboModel>(#"SELECT SLA.Id, AddonCost, CostTimeUnit, IsComplimentaryBySubscriptionLevel, ComboText, AddonId
FROM SubscriptionLevel L
LEFT OUTER JOIN SubscriptionLevelAddon SLA ON L.Id = SLA.SubscriptionLevelId AND SLA.AddonId = #p0
ORDER BY L.OrderPosition", addon.Id);
addon.SubscriptionLevelCombos = addonCombos.ToList();
}
return View(model);
}
[Route("identity/subscription")]
[ValidateAntiForgeryToken]
[HttpPost]
// POST: Subscription
public ActionResult Index(SubscriptionInputModel model)
{
ArrbOneDbContext db = new ArrbOneDbContext();
List<SubscriptionInputAddonModel> addons = Mapper.Map<Addon[], List<SubscriptionInputAddonModel>>(db.Addons.OrderBy(a => a.OrderPosition).ToArray());
model.Addons = addons;
//debug breakpoint to inspect returned model values
return View();
}
Index.cshtml
#model Identity_Server._Code.ViewModel.Subscription.SubscriptionInputModel
#{
ViewBag.Title = "Subscription";
}
#using (Html.BeginForm("Index", "Subscription", new { signin = Request.QueryString["signin"] }, FormMethod.Post))
{
#Html.ValidationSummary("Please correct the following errors")
#Html.AntiForgeryToken()
...
// ADD ONs ----------------------------------------------------------------------------------
#for (int a = 0; a < Model.Addons.Count; a++)
{
var addon = Model.Addons[a];
<tr>
<td class="text-left">#addon.Name
<div class="SubscriptionItemNote">#addon.Note
#Html.HiddenFor(m => m.Addons[a].Id)
</div>
</td>
#for (int i = 0; i < addon.SubscriptionLevelCombos.Count; i++)
{
<td>
#if (addon.SubscriptionLevelCombos[i].Id.HasValue)
{
if (addon.SubscriptionLevelCombos[i].AddonCost.HasValue && addon.SubscriptionLevelCombos[i].AddonCost.Value > 0)
{
#Html.Raw("<div>+ " + #addon.SubscriptionLevelCombos[i].AddonCost.Value.ToString("0.##") + " / " + #addon.SubscriptionLevelCombos[i].CostTimeUnit.Value.ToString() + "</div>")
}
else if (addon.SubscriptionLevelCombos[i].IsComplimentaryBySubscriptionLevel.HasValue && #addon.SubscriptionLevelCombos[i].IsComplimentaryBySubscriptionLevel.Value)
{
<span class="glyphicon glyphicon-ok"></span>
}
if (!string.IsNullOrEmpty(addon.SubscriptionLevelCombos[i].ComboText))
{
<div>#addon.SubscriptionLevelCombos[i].ComboText</div>
}
if (addon.SubscriptionLevelCombos[i].AddonCost.HasValue && addon.SubscriptionLevelCombos[i].AddonCost.Value > 0)
{
#Html.HiddenFor(m => m.Addons[a].SubscriptionLevelCombos[i].Id)
#Html.CheckBoxFor(m => m.Addons[a].SubscriptionLevelCombos[i].AddonSelected)
}
}
</td>
}
</tr>
}

Related

Razor post collection

I am having an issue for posting collection. I just start learing MVC and razor.
I am writing a small questionnaire application all what I am doing populate a list of question and let the user answer the question and post the answer.
I have two class in the model Questionaire and QuestionRepository the controller have two method one for fetch the question other one to get the post data, the problem i am struggling when I populate the form with textbox I am struggling to post back the collection.
Basically what i want to achive pull the list of question, population on the UI with a text box, send the the list of question with the answer to the controller for processing some logic.
I Would really appreciate if someone can help me.
public class Questionnaire
{
public string Title {
get;
set;
}
public IList<string> Questions {
get;
set;
}
public string Answer {
get;
set;
}
}
public Questionnaire GetQuestionnaire() {
return new Questionnaire {
Title = "Geography Questions",
Questions = new List<string>
{
"What is the capital of Cuba?",
"What is the capital of France?",
"What is the capital of Poland?",
"What is the capital of Germany?"
}
};
}
public int GetCustomerAnswer() {
return 0;
}
}
Controller
public ActionResult Index()
{
var q = new QuestionRepository().GetQuestionnaire();
return View(q);
}
[HttpPost]
public ActionResult GetAnswer(QuestionRepository q) {
return View();
}
View
#model Question.Models.Questionnaire
<h2>Question List</h2>
#using(Html.BeginForm("GetAnswer","Home"))
{
for(int i=0;i< Model.Questions.Count;i++)
{
#Html.Label("Question")
<text>#Model.Questions[i]</text>
#Html.TextBox(Q => Model.Questions[i])
<br />
}
<input type="submit" name="submit" />
}
Firstly you model is wrong and does not define any relationship between questions and associated answers (you only have the ability to add one answer to all the questions). Secondly your view binds a text box to each question, allowing the user to only edit the question and not add a answer. A simple example might look like
View models
public class QuestionVM
{
public int ID { get; set; }
public string Text { get; set; }
}
public class AnswerVM
{
public QuestionVM Question { get; set; }
public int ID { get; set; }
public string Text { get; set; }
}
public class QuestionnaireVM
{
public int ID { get; set; }
public string Title { get; set; }
public List<AnswerVM> Answers { get; set; }
}
Controller
public ActionResult Create(int ID)
{
QuestionnaireVM model = new QuestionnaireVM();
// populate the model from the database, but a hard coded example might be
model.ID = 1;
model.Title = "Questionaire 1";
model.Answers = new List<AnswerVM>()
{
new AnswerVM()
{
Question = new QuestionVM() { ID = 1, Text = "Question 1" }
},
new AnswerVM()
{
Question = new QuestionVM() { ID = 2, Text = "Question 2" }
}
};
return View(model);
}
[HttpPost]
public ActionResult Create(QuestionnaireVM model)
{
// save you data and redirect
}
View
#model QuestionnaireVM
#using(Html.BeginForm())
{
#Html.DisplayFor(m => m.Title)
for(int i = 0; i < Model.Answers.Count; i++)
{
#Html.HiddenFor(m => m.Answers[i].Question.ID)
#Html.DisplayFor(m => m.Answers[i].Question.Text)
#Html.TextBoxFor(m => m.Answers[i].Text)
}
<input type="submit" />
}
This will generate a view display each question with and associated textbox to add the answer.When you post back the model will contain the ID of the questionnaire, and a collection containing the ID of each question and its associated answer.
A couple of other examples of similar issues here and here
If you want to post back something. In the [HttpPost] action, you need to return a model. At the moment, you just return a View() with no model data to display. You should do something like this:
[HttpPost]
public ActionResult GetAnswer(QuestionRepository q) {
var model = new Answer();
model.answer = q.answer;
return View(model);
}
You also need to create the model Answer.
public class Answer
{
public string answer { get; set; }
}

asp.net MVC Checkboxes DaysOfWeek enum

I am working on a form that can be used to add new records and update existing records.
One form element is to capture multiple choices of week days. I therefore implemented an DayOfWeek Enum.
This is how my model looks like
public class EventFormModel
{
public EventFormModel()
{
AvailableDays = (from DayOfWeek d in Enum.GetValues(typeof(DayOfWeek))
select new MyDay
{
Id = (int)d,
Name = d.ToString()
}
public List<MyDay> AvailableDays { get; set; }
public int[] SelectedDays { get; set; }
}
}
public class MyDay
{
public string Name { get; set; }
public int Id { get; set; }
}
My View looks like this
#foreach (var day in Model.AvailableDays)
{
<input type="checkbox" name="SelectedDays" value="#day.Id"
#if (Model.SelectedDays != null && Model.SelectedDays.Contains(day.Id))
{
<text>checked="checked"</text>
}
/>#day.Name.Substring(0,3)
}
Now I am facing 2 problems, For 1 I can't figure out how to retrieve the values from my checkboxes ones they are posted to the controller and 2 how would I populate the checkboxlist with values I stored in my database ie SelectedDays = 1,4,6.
I hope I am approaching this the correct way in the first place. any help would be appreciated.
It never ceases to amaze me how people insist on writing their own html, and then are confused why things don't work.
Stick with the html helpers whenever possible. Change your model a bit... Make SelectedDays an array of bool[7]
#for(int i = 0; i < Model.SelectedDays.Count; i++)
{
#Html.CheckBoxFor(x => x.SelectedDays[i])
}
Now the checkboxes automatically populate, and default to unchecked.
Alternatively, you could add a "selected" bool field to your MyDay class, then change it to:
#for(int i = 0; i < Model.AvailableDays.Count; i++)
{
#Html.CheckBoxFor(x => x.AvailableDays[i].Selected)
}
Note: Do not change this to use a foreach, if you do, it won't create the proper indexing for the name fields to post the collection values.
After tinkering with this for a while I came up with this working solution.
Here is the model
public class EventFormModel
{
public EventFormModel()
{
AvailableDays = (from DayOfWeek d in Enum.GetValues(typeof(DayOfWeek))
select new MyDay
{
Id = (int)d,
Name = d.ToString()
}
public List<MyDay> AvailableDays { get; set; }
}
}
public class MyDay
{
public string Name { get; set; }
public int Id { get; set; }
public bool Selected { get; set; }
}
Here is the View
#for(int i = 0; i < Model.AvailableDays.Count; i++)
{
<div>
#Html.CheckBoxFor(x => x.AvailableDays[i].Selected)
#Html.HiddenFor(x => x.AvailableDays[i].Name)
#Model.AvailableDays[i].Name
</div>
}
Here is the Controller
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult AddEvent(EventFormModel model)
{
string days = "";
for(int i = 0; i < model.AvailableDays.Count; i++)
{
if (model.AvailableDays[i].Selected)
{
days = days + model.AvailableDays[i].Name + ",";
}
}
//Do something with your selected days
return View(model);
}
To populate the checkboxes with select items
string[] stringArray = "Sun,Mon,Tue,Wed".Split(',').ToArray<string>();
model.AvailableDays = (from DayOfWeek d in Enum.GetValues(typeof(DayOfWeek))
select new TestDay
{
Id = (int)d,
Name = d.ToString().Substring(0, 3),
Selected = stringArray.Contains(d.ToString().Substring(0, 3))
}).ToList();

How to show sum using LINQ statement in Grid view of the MVC app

I developing the MVC application.
I am stuck in LINQ Syntax.
I wan to show the sum of List Items in index view of parent.
Please check code below.
In Model I have two classes.
public class StockAdjustment
{
public int Id { get; set; }
public List<StockAdjustmentItem> StockAdjustmentItems { get; set; }
public int SumOfStockAdjustmentItemQuantity
{
get
{
if (this.StockAdjustmentItems != null)
{
return this.StockAdjustmentItems.Sum(s=>s.OriginalQuantity);
}
return 0;
}
}
}
public class StockAdjustmentItem
{
public int Id { get; set; }
public int OriginalQuantity { get; set; }
public StockAdjustment StockAdjustment { get; set; }
}
public StockAdjustment GetAll(int Id)
{
oStockAdjustment = GetStockAdjustmentById(Id);
var prepo = new ProductRepo();
oStockAdjustment.StockAdjustmentItems = new List<StockAdjustmentItem>();
StockAdjustmentItem ai1 = new StockAdjustmentItem();
ai1.Id = 1 ;
ai1.OriginalQuantity = 250;
oStockAdjustment.StockAdjustmentItems.Add(ai1);
StockAdjustmentItem ai2 = new StockAdjustmentItem();
ai2.Id = 1;
ai2.OriginalQuantity = 375;
oStockAdjustment.StockAdjustmentItems.Add(ai2);
return oStockAdjustment;
}
Now I have controller Code
public ActionResult Index(string searchContent = "")
{
AdjustmentRepo oAdjustmentRepo = new AdjustmentRepo();
var adjustments = from adjustment in oAdjustmentRepo.GetAll() select adjustment;
ViewBag.StockAdjustmentList = adjustments;
return View(adjustments);
}
This Working perfectly fine...
Now, the problem comes when, I am trying to show StockAdjustment in List.
I have to show the sum of the OriginalQuantites of StockAdjustmentItems in the Front of StockAdjustment item in grid.
in above Exmaple I want to show 650(250 + 375) in the row of a gird.
#model IEnumerable<StockWatchServices.DomainClass.StockAdjustment>
#Html.Grid(Model).Columns(columns =>
{
columns.Add(c=>c.StockAdjustmentItems.Sum( OriginalQuantity ???? Im stuck here... )
}
What should I write here ?
I can see like this...
Create a getter only property on the StockAdjustment class
public class StockAdjustment
{
public int Id { get; set; }
public List<StockAdjustmentItem> StockAdjustmentItems { get; set; }
public int SumOfStockAdjustmentItemQuantity
{
get
{
if (this.StockAdjustmentItems != null)
{
return this.StockAdjustmentItems.Sum(s=>s.OriginalQuantity);
}
return 0;
}
}
}
And then in your Razor view:
#Html.Grid(Model).Columns(columns =>
{
columns.Add(c => c.SumOfStockAdjustmentItemQuantity)
}
Can you try with below code :
#Html.Grid(Model).Columns(columns =>
{
columns.Add(c => c.StockAdjustmentItems.Where(quantity => quantity.OriginalQuantity != null).Sum(sum => sum.OriginalQuantity).ToString());
})

Model Item type ambiguity when using PagedList

I am developing a web survey application in ASP.Net Mvc3. I use PagedList in my application to paginate the questions page alone.
I get the following error:
The model item passed into the dictionary is of type
'PagedList.PagedList`1[SWSSMVC.Models.ViewModels.QuestionViewModel]',
but this dictionary requires a model item of type
'PagedList.IPagedList`1[SWSSMVC.Models.ViewModels.QuestionListViewModel]'.
There is a question which is of similar nature. The solution says not to specify anonymous type, as far as I understood. Can someone point out where in my code I have anonymous type? I believe I have typed all my variables with appropriate models.
This is the question Controller:
public class QuestionController : SessionController
{
DBManager dbmgr = new DBManager();
//
// GET: /Question/
public ActionResult Index(string currentSection, string currentPage, int? page)
{
int j;
SectionSession = currentSection;
PageSession = currentPage;
var questionList = new QuestionListViewModel();
int questionCount = dbmgr.getQuestionCount(currentPage);
var question = new QuestionViewModel();
for(int i=1 ; i<=questionCount; i++)
{
int questionid = dbmgr.getQuestionid(currentPage, i);
string questiontext = dbmgr.getQuestion(questionid);
List<string> oldchoices = dbmgr.getChoicesAns(questionid);
ChoiceViewModel choice = new ChoiceViewModel();
question = new QuestionViewModel { QuestionId = questionid, QuestionText = questiontext, Answer = oldchoices.Last()};
for (j = 0; j < oldchoices.Count() - 1; j++)
{
if (oldchoices[j] != null)
{
question.Choices.Add(new ChoiceViewModel { ChoiceId = j, ChoiceText = oldchoices[j] });
}
}
questionList.Questions.Add(question);
}
int pageSize = 3;
int pageNumber = (page ?? 1);
return View(questionList.Questions.ToPagedList(pageNumber, pageSize));
}
There are two models:
public class QuestionViewModel
{
public int QuestionId { get; set; }
public string QuestionText { get; set; }
public List<ChoiceViewModel> Choices { get; set; }
public string Answer { get; set; }
[Required]
public string SelectedAnswer { get; set; }
public QuestionViewModel()
{
Choices = new List<ChoiceViewModel>();
}
}
public class QuestionListViewModel
{
public List<QuestionViewModel> Questions { set; get; }
public QuestionListViewModel()
{
Questions = new List<QuestionViewModel>();
}
}
I am entering my part- Index View code for the above Question Controller
#model PagedList.IPagedList<SWSSMVC.Models.ViewModels.QuestionListViewModel>
#{
ViewBag.Title = "Index";
}
<h2>Questions</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
#foreach (var item in Model)
{
#Html.EditorFor(x => item.Questions)
}
I also have a Editor Template like this
#model SWSSMVC.Models.ViewModels.QuestionViewModel
<div>
#Html.HiddenFor(x => x.QuestionId)
<h3> #Model.QuestionText </h3>
#foreach (var a in Model.Choices)
{
<p>
#Html.RadioButtonFor(b => b.SelectedAnswer, a.ChoiceText) #a.ChoiceText
</p>
}
</div>
I tried to run through the code a couple of times and having hard time figuring it out. I also do not know how I could have made the questionList as a LINQ variable, given that, my questionList is inturn constructed with questions and choices from a separate model.
Creator of PagedList here. The problem is that this line:
return View(questionList.Questions.ToPagedList(pageNumber, pageSize));
Is sending a model of type IPagedList down to the page (because the extension method is being applied to a type of List), but your page says it is expecting:
#model PagedList.IPagedList<SWSSMVC.Models.ViewModels.QuestionListViewModel>
Changing your view code to say this instead should fix it:
#model PagedList.IPagedList<SWSSMVC.Models.ViewModels.QuestionViewModel>

Dynamic link issue in an MVC 3 website

I'm creating my first ASP.NET MVC 3 website for my company's intranet. It's a pretty cool, I play audio recorded by our phone system and saved in our db. That's working good, but I'm having a hard time figuring out how to do something that should be simple. Please forgive any syntax errors I most likely have, this is a rough draft.
I have a table in the Index View /Apps that list all the AppName's, and next to each AppName I want to display a link to another view, with the text of the link being a Count() of all CallDetails associated with that App.
I have two classes:
public class Apps
{
public int AppId { get; set; }
public string AppName { get; set; }
}
public class CallDetail
{
public int Id { get; set; }
public int AppID { get; set; }
public byte[] FirstName { get; set; }
public byte[] LastName { get; set; }
....etc
}
a context for each:
public class AppsContext : DbContext
{
public DbSet<Apps> Apps { get; set; }
}
public class CallContext : DbContext
{
public DbSet<CallDetail> CallDetails { get; set; }
}
a controller method for each:
// AppsController
private AppsContext db = new AppsContext();
public ViewResult Index()
{
return View(db.Apps.ToList());
}
// CallController method (from my current attempt)
public ActionResult CallCheck(int id)
{
bool? enabled = null;
var appcalls = from s in db.CallDetails
where s.AppID == id
&& s.Enabled.Equals(enabled)
select s;
string callnum = appcalls.Count().ToString();
return View(callnum);
}
It displays the AppName just fine in this portion of the View below, and I can create a link to a View for each associated CallDetail just fine. But I don't know how to display info I'd get from the CallDetail Controller since the View's Model is Apps and its Controller, AppsController.
#model IEnumerable<myMessagePlayer.Models.Apps>
...
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.AppName)
</td>
<td class="appLink">
...
</td>
</tr>
}
I've tried many different methods, some that I might have gotten to work, but they seemed semantically un-MVC. So I figured I'd just ask a general "whats the standard practice?" type of question.
The path you are currently going down would end up hitting the database for each app you have in your database. There is a way to display all the information with only one hit to the database.
Your context needs to change to this:
public class ApplicationContext : DbContext
{
public DbSet<Apps> Apps { get; set; }
public DbSet<CallDetail> CallDetails { get; set; }
}
You could create a view model object called AppCallInfo that has three properties:
public class AppCallInfo
{
public int AppID { get; set; }
public string AppName { get; set; }
public int CallCount { get; set; }
}
In your Index action you need to do something like this:
public ViewResult Index()
{
var model = from a in db.Apps
join c in db.CallDetails on a.AppID equals c.AppID
where c.Enabled == enabled
group a by new { AppName = a.AppName, AppID = a.AppID } into g
select new AppCallInfo {
AppName = g.Key.AppName,
AppID = g.Key.AppID,
CallCount = g.Count()
};
return View(model.ToList());
}
Now you have everything you need for each row in your table in one object.
#model List<myMessagePlayer.ViewModels.AppCallInfo>
...
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.AppName)
</td>
<td class="appLink">
#Html.ActionLink(item.CallCount, "ViewCalls", "Call", new { Id = item.AppID }, null)
</td>
</tr>
}
Using this method avoids hitting the database for each app you have in your table.
Is the view CallCheck a partial view?
In your index view you could use
#Html.RenderAction("CallCheck", "AppsController", new { Id = #Model.AppId } )
The syntax may not be 100% correct, but it should get you going in the right direction.

Resources