Bind multiple controls to collections in MVC - c#

I have 10 text type input controls, 5 on first tab & 5 on second tab on same page.
Now I have a class as
public partial class TravelerMaster
{
public virtual ICollection<TravelerDetail> TravelerDetails { get; set; }
}
which has a collection as TravelerDetail.
public partial class TravelerDetail
{
public byte PackageTypeId { get; set; }
public int? NoOfPackage { get; set; }
public decimal? Weight { get; set; }
public decimal? PricePerKg { get; set; }
public decimal? PricePerPackage { get; set; }
}
How do i bind the controls such that when I post the data, the TravelerMaster object has two objects in its collection.
Please help me. Thanks in advance. Feel free to ask any query.

You can submit a collection of a particular model like this. By applying an index (this can be anything unique but I've used an integer value for ease) you can submit a collection of the same model to the controller:
Form
<div id="tab1">
#Html.Hidden("data.Index","1")
#Html.TextBox("data[1].weight", "", new { #class = "your-class-names", #id = "data[1].weight" })
...other fields...
</div>
<div id="tab2">
#Html.Hidden("data.Index","2")
#Html.TextBox("data[2].weight", "", new { #class = "your-class-names", #id = "data[2].weight" })
...other fields...
</div>
Controller
[HttpPost]
public ActionResult YourControllerMethod(IEnumerable<TravelerDetail> data)
{
if (data==null || data.Count() == 0) throw new Exception("No Data Added");
....other validation...
foreach(var item in data) {
...database work...
}
}

Related

.NET5 - #HiddenFor(model => model.RowVersion) caches value? [duplicate]

I'm working on a MVC/EF Web Application. In one of the forms I edit a model. The model has an image field (public byte[] BioPhoto)
When I upload an image to that field and save data, ModelState.IsValid is false, because BioPhoto property is null in ModelState. Note that the BioPhoto in model is loaded with image data.
I tried to inject the picture to ModelState using below code but still ModelState.IsValid is false
public ActionResult Edit([Bind(Include = "BusinessId,Name,About,Phone,TollFree,FAX,Email,Bio,BioPhoto")] Business business)
{
if (System.IO.File.Exists("image.jpg"))
{
business.BioPhoto = System.IO.File.ReadAllBytes("image.jpg");
ModelState.SetModelValue("BioPhoto",
new ValueProviderResult(business.BioPhoto, "", System.Globalization.CultureInfo.InvariantCulture));
}
if (ModelState.IsValid)
{
db.Entry(business).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(business);
}
What am I doing wrong. Is there any better solution for this issue?
I read couple of answer in SO but could not figure out how to solve my case.
This is my model
public class Business
{
public int BusinessId { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
[Required]
public Address address { get; set; }
[Required]
[StringLength(20)]
public string Phone { get; set; }
[StringLength(20)]
public string TollFree { get; set; }
[StringLength(20)]
public string FAX { get; set; }
[Required]
[StringLength(50)]
public string Email { get; set; }
[Required]
[StringLength(100)]
public string WebSite { get; set; }
[Required]
public string About { get; set; }
[Required]
public string Bio { get; set; }
[Required]
public byte[] BioPhoto { get; set; }
}
My View
<div class="form-group">
#Html.LabelFor(model => model.BioPhoto, "BIO Photo (Best Size: 350 x 450)", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<form enctype="multipart/form-data">
<div class="form-group" style="width:400px">
<input id="BioPhoto" type="file" multiple class="file" data-overwrite-initial="false" />
</div>
</form>
#Html.ValidationMessageFor(model => model.BioPhoto, "", new { #class = "text-danger" })
</div>
</div>
Like I said the issue is the form is not posting the BioPhoto to the controller-most likely. You can use Chrome to see what is in the body of the http request.
If you want to add the image even if the controller is not receiving it, then try this.
Try removing only the error related to the image property:
ModelState["BioPhoto"].Errors.Clear();
Then add the image:
business.BioPhoto = System.IO.File.ReadAllBytes("image.jpg");
Then update the model:
UpdateModel(business);
Now if you call ModelState.IsValid, it will take the setting of BioPhoto into consideration and re-evaluate the IsValid.
EDIT:
I suggest to call
ModelState.SetModelValue("BioPhoto", new ValueProviderResult(business.BioPhoto, "", System.Globalization.CultureInfo.InvariantCulture));
to actually push the value to ModelState.Values. this is not neccessary though.
Since you are not using a view model, you should use a parameter of type HttpPostedFileBase for the image. Then in your action method, you can convert it's InputStream property value to byte array and save it.
public ActionResult Edit([Bind(Include = "BusinessId,Name,About,Phone,TollFree,
FAX,Email,Bio")] Business business,HttpPostedFileBase BioPhoto)
{
// to do read BioPhoto.InputStream and convert to your byteArray
// to do :Return something
}
You have to remove the [Required] attribute from your byte array property of your model class.
The best solution is to use a view model which has only properties you needed from your view. You do not need to repost the image from the edit screen everytime. Update that property only if user selects a new image in the edit form. If not do not update the existing value of that column when you save.
public class EditBusinessVm
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
[Required]
public Address address { get; set; }
[Required]
[StringLength(20)]
public string Phone { get; set; }
public HttpPostedFileBase BioPhoto { set;get;}
// Add other properties as needed.
}
Now your view must be strongly typed to this class
#model EditBusinessVm
#using(Html.BeginForm())
{
#Html.TextBoxFor(s=>s.Name)
<input type="file" name="BioPhoto" />
#Html.HiddenFor(f=>f.Id)
<!-- add other fields needed -->
<input type="submit" />
}
and your httppost action method will use this type as parameter
public ACtionResult Edit(EditBusinessVm model)
{
// purposefully omitting null checks/model validations code here
//get existing entity
var b=db.Businesses.FirstOrDefault(f=>f.BusinessId==model.Id);
//update the properties from the posted view model
b.Name = model.Name;
if(model.BioPhoto!=null) // user selected a new photo.
b.BioPhoto = GetByteArray(model.BioPhoto);
// to do : Set other properties as well.
db.Entry(business).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
private byte[] GetByteArray(HttpPostedFileBase file)
{
MemoryStream target = new MemoryStream();
file.InputStream.CopyTo(target);
return target.ToArray();
}

MVC Html.DropDownListFor returns complex type with null values

I have the following controller create action in my controller. It takes care that the displayname in the select list will be 'StoreName - StoreAddress'. The Store complexType is stored in Store.
// GET: Purchases/Create
public ActionResult Create()
{
ViewBag.Stores = db.Stores.Select(s => new { DisplayName = s.StoreName.ToString() + " - " + s.Address.ToString(), Store = s});
return View();
}
In the Create View the following code takes care that it will be displayed correctly.
<div class="form-group">
#Html.LabelFor(model => model.Store.StoreName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.Store.StoreName, new SelectList(ViewBag.Stores, "Store", "DisplayName"), new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Store.StoreName, "", new { #class = "text-danger" })
</div>
</div>
It will go to the post method of the controller (if I am correct).
// POST: Purchases/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,Store,Price,Date")] Purchase purchase)
{
if (ModelState.IsValid)
{
Store store = purchase.Store;
db.Purchases.Add(purchase);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(purchase);
}
However the Store store = purchase.Store will now give a complex Store type with the values for anything other than StoreName set to null. StoreName will be a string.
How can I get a complex type returned that is equal to the selected Store object?
Edit 1:
public class Purchase
{
public int Id { get; set; }
public Store Store { get; set; }
public string Type { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Price { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}", ApplyFormatInEditMode = true)]
public DateTime Date { get; set; }
}
public class PurchaseDBContext : DbContext
{
public DbSet<Purchase> Purchases { get; set; }
public DbSet<Store> Stores { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
}
}
public class Store
{
public int StoreId { get; set; }
public string StoreName { get; set; }
public string Address { get; set; }
public string City { get; set; }
[DataType(DataType.PhoneNumber)]
[RegularExpression(#"^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9])((\s|\s?-\s?)?[0-9])((\s|\s?-\s?)?[0-9])\s?[0-9]\s?[0-9]\s?[0-9]\s?[0-9]\s?[0-9]$", ErrorMessage = "This is not a valid phonenumber")]
public string PhoneNumber { get; set; }
}
Do I need to use additional annotations to set the navigation properties?
There's alot going on in your example, but there isn't a need to pass a complex object if you're using Navigation Properties and your model is setup in a certain way. If you setup a navigation property for Store/StoreId, Entity Framework should infer things for you, so it will simplify your view.
public class Purchase
{
public int StoreId { get; set; }
[ForeignKey("StoreId")]
public Store Store { get; set;}
}
I'd pass a model to the view similar to below. IMO, it's cleaner than using ViewBag.
public class CreatePurchaseModel
{
//To populate a list
public List<Store> AvailableStores { get; set; }
//The actual object to be created
public Purchase Purchase { get; set; }
}
Controller Method:
// GET: Purchases/Create
public ActionResult Create()
{
var vm = new CreatePurchaseModel()
{
AvailableStores = db.Stores.ToList(),
Purchase = new Purchase()
};
return View(vm);
}
Populate a dropdownlist with the AvailableStores property to set the Purchase.StoreId
#Html.DropDownListFor(m => m.Purchase.StoreId, Model.AvailableStores.Select(x => new SelectListItem { Text = x.StoreName.ToString(), Value = x.StoreId.ToString() }))
If it's setup properly, all you need in the post method parameters is the purchase
[ValidateAntiForgeryToken, HttpPost]
public ActionResult Create(Purchase purchase)
{
//purchase.StoreId should have a non zero value
db.Purchases.Add(purchase);
db.SaveChanges();
}

How to render a model with list of objects in view as a form? [duplicate]

This question already has answers here:
Asp.Net MVC: Why is my view passing NULL models back to my controller?
(2 answers)
Closed 5 years ago.
I need to display a specific form in a view with a model. The model is like that:
This is the final object filled what I need :
public class ObjetTotal
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Numero { get; set; }
public string Value { get; set; }
}
I choose to cut the form into two differents parts :
First a "static" part where the user can put common values for the differents ObjetsTotal.
Second a "variable" part where the user put differents values for the differents ObjetsTotal.
The final aim is that the user doest'n have to type, the same thing for all the objects ObjetTotal.
So, I create other objects (I don't know if it's a good practice) which represents the differents part of the form.
The static part with MainObjet and the variable part with Numbers. I put these two object into an other object "Mix" which contains one "MainObjet" and a list of "Numbers".
public class MainObjet
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class Numbers
{
public string Numero { get; set; }
public string Value { get; set; }
}
public class Mix
{
public MainObjet obj { get; set; }
public IEnumerable<Numbers> num { get; set; }
public Mix()
{
obj = new MainObjet();
num = new List<Numbers>();
}
}
Then I want to render the model Mix in a view to have the two parts of the form.
I've try this :
#model App.Models.Mix
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Mix</legend>
<h3>First Properties</h3>
<div>
#Html.TextBoxFor(model => model.obj.Id);
#Html.TextBoxFor(model => model.obj.Name);
#Html.TextBoxFor(model => model.obj.Description);
</div>
<div>
<table>
#for (int i = 0; i < 5; i++)
{
<tr>
<td>
#Html.TextBoxFor(model => model.num[i].Numero)
</td>
<td>
#Html.TextBoxFor(model => model.num[i].Value)
</td>
</tr>
}
</table>
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
But after the submit I get an object Mix null in this ActionResult :
[HttpPost]
public ActionResult Test(Mix obj)
{
return View();
}
Can you explain me how to do that ? May I'm on a wrong way.
Don't consider the design of the form, and I don't know the right type to put to Numbers, Maybe a simple list be enough for that.
The thing I can see is that you are missing the initialization of your model properties in the parameter-less constructor. You should try to update your model code to be:
public class Mix
{
public MainObjet obj { get; set; }
public IEnumerable<Numbers> num { get; set; }
public Mix()
{
obj = new MainObjet();
num = new List<Numbers>();
}
}
As the model binder will instantiate your model, and it will find obj and num to null and will not be able to post the values back.
Hope this helps you.

Multiple answer group & answers asp.net MVC

I want to do a single OR multiple answer quiz using radiobuttonfor and checkboxFor, but I cannot make it work. The problem with all example I see is that the Question model also register the SelectedAnswer, but in my case I want each possible answer to be selectable since some questions will have multiples answers, and thus the isSelected property is directly inside the Answer model.
Therefore, for questions with single answers, when I try to create my model using RadioButtonFor(m => m[question].Answers[answerToDisplayId].IsSelected), every answer is in its own group and is not unchecked when I check another answer from that question (basically it behave like a checkBoxFor)
What I currently have: The question model
public enum questionfield
{
Chaser, Beater, Seeker, Contact, Process, Other
};
public enum QuestionDifficulty
{
Basic, Advanced
};
public enum AnswerType
{
SingleAnswer, MultipleAnswer
}
public class Question
{
public int Id { get; set; }
[Required(ErrorMessage = "Question name not valid")]
public string Name { get; set; }
[Required]
public QuestionField Field { get; set; }
[Required]
public QuestionDifficulty Difficulty { get; set; }
[Required]
public bool IsVideo { get; set; }
public string VideoURL { get; set; }
[Required]
public string QuestionText { get; set; }
[Required]
public AnswerType AnswerType { get; set; }
[Required]
public List<Answer> Answers { get; set; }
[Required]
public String AnswerExplanation { get; set; }
Answer model :
public class Answer
{
public int Id { get; set; }
public String Answertext { get; set; }
public Boolean IsTrue { get; set; }
public Boolean IsSelected { get; set; }
}
The view :
<div>
<!-- For each Question, display a new div with the Title, the question code, the question text, the video if there is one, then the possible answers depending on the type of answers-->
#using(Html.BeginForm("QuizzResult", "Home"))
{
for(int i = 0; i < Model.Count; i++)
{
<div class="QuizzQuestion">
<div class="QuizzQuestionTitle">#Model[i].Id : #Model[i].Name</div> #Html.HiddenFor(m => Model[i].Id)
<div class="QuizzQuestiontext">#Model[i].QuestionText</div>
#if(#Model[i].IsVideo)
{
<div class="QuizzQuestionVideoContainer">
<iframe class="QuizzQuestionVideo" id="ytplayer" type="text/html"
src="#Model[i].VideoURL"
frameborder="0"></iframe>
</div>
}
<div class="RadioButtonAnswers">
#if (#Model[i].AnswerType == QRefTrain3.Models.AnswerType.SingleAnswer)
{
for (int j = 0; j < Model[i].Answers.Count; j++)
{
#Model[i].Answers[j].Answertext #Html.RadioButtonFor(m => m[i].Answers[j].IsSelected, true)
#Html.HiddenFor(m => Model[i].Answers[j].IsTrue)
}
}
</div>
</div>
}
<input type="submit" value="Validate Answers"/>
}
</div>
The controller :
[HttpPost]
public ActionResult QuizzResult(List<Question> answers)
{
foreach(Question a in answers)
{
var b = Request.Form[a.Id.ToString()];
}
Result result = new Result();
foreach (Question q in answers)
{
result.QuestionsAskedIds.Add(q.Id);
if (Question.IsGoodAnswer(q))
{
result.GoodAnswersIds.Add(q.Id);
}
}
if (User.Identity.IsAuthenticated)
{
result.User = Dal.Instance.GetUserByName(HttpContext.User.Identity.Name);
Dal.Instance.CreateResult(result);
}
return View("QuizResult", result);
}
What would be the good way to do this? Thank you!
In case someone will see this :
The solution I found was to change the model : Instead of having one IsSelected parameter per answer, add a List selectedAnswers to your Question model. Then, in the view, add your radiobutton like this :
#Html.RadioButtonFor(m => m[i].SelectedAnswers, Model[i].Answers[j].Id)
You will store the id of each selected answer for this question ine the SelectedAnswers list. You can then create your results using this data.

MVC Add and remove items from subcollection

I'm having trouble binding data to a collection item's collection (I'm also having trouble wording my problem correctly). Let's just make thing easier on everyone by using an example with psudo models.
Lets say I have the following example models:
public class Month()
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Week> Weeks { get; set; }
}
public class Week()
{
public int ID { get; set; }
public int MonthID { get; set; }
public String Name { get; set; }
public virtual ICollection<Day> Days { get; set; }
}
public class Day()
{
public int ID { get; set; }
public String Name { get; set; }
}
...and an example viewmodel:
public class EditMonthViewModel()
{
public Month Month { get; set; }
public List<Week> Weeks { get; set; }
public List<Day> AllDays { get; set; }
}
The purpose of the Edit Action/View is to enable users to edit a month, the weeks assigned to the month, and add and remove days from weeks of a certain month. A view might help.
#model myProject.ViewModels.EditMonthViewModel
//...
#using (Html.BeginForm())
{
//Edit Month Stuff...
#for(int i = 0; i < Model.Weeks.Count(); i++)
{
<h2>#Model.Weeks[i].Name</h2>
#Html.EditorFor(model => Model.Weeks[i].Name)
//loop through all possible days
//Select only days that are assigned to Week[i]
#for(int d = 0; d < Model.AllDays.Count(); d ++)
{
//This is the focus of this question.
//How do you bind the data here?
<input type="checkbox"
name="I have no idea"
#Html.Raw(Model.Weeks[i].Days.Contains(Model.AllDays[d]) ? "checked" : "") />
}
}
}
Controller Action methods
public ActionResult Edit(int id)
{
var viewModel = new EditMonthViewModel();
viewModel.Month = db.Months.Find(id);
viewModel.Weeks = db.Weeks.Where(w => w.MonthID == id).ToList();
viewModel.AllDays = db.Days.ToList();
}
[HttpPost]
public ActionResult Edit(EditMonthViewModel viewModel)
{
var monthToUpdate = db.Months.Find(viewModel.Month.ID);
//...
if(viewModel.Weeks != null)
{
foreach (var week in viewModel.Weeks)
{
var weekToUpdate = monthToUpdate.Weeks.Single(w => w.ID == week.ID);
//...
/*So, I have a collection of weeks that I can grab,
but how do I know what was selected? My viewModel only has a
list of AllDays, not the days selected for Week[i]
*/
}
}
How can I ensure that when I submit the form the selected days will bind to the week?
It looks like the easiest thing to do is to make it a goal for your form to populate a data structure of the type IEnumerable<DayModel>, where DayModel is defined as:
public class DayModel
{
public int WeekId { get; set; }
public int DayId { get; set; }
public bool IsIncluded { get; set; }
}
You could keep your Razor code as is for the most part, but then when it comes to rendering the checkboxes, you can do something like this:
#{
var modelIdx = 0;
}
// ...
<input type="hidden" name="days[#modelIdx].WeekId" value="#Model.Weeks[i].Id" />
<input type="hidden" name="days[#modelIdx].DayId" value="#Model.AllDays[d].Id" />
<input type="checkbox" name="days[#modelIdx].IsIncluded" value="#(Model.Weeks[i].Days.Contains(Model.AllDays[d]) ? "checked" : "")" />
#{ modelIdx++; }
Then, your controller action you post to could have this signature:
[HttpPost]
public ActionResult Edit(IEnumerable<DayModel> days)
{
//...
}
Something that helps me is to never confuse view models, which should only be used for the model for views (GET actions generally) and non-view models (what we call plain models). Avoid having your POST actions try to bind to view models, and it will simplify your life greatly.

Categories

Resources