MVC Pair Checkbox with Text Input Field - c#

I'm looking to have something like the following:
#foreach(var option in ViewBag.OptionsAvailable)
{
//the 'option' variable will be used in these fields, of course
<input type='checkbox' name='thingToUse' />
<input type='text' name='textToSaveForThing' />
}
ViewBag.OptionsAvailable can be of variable length. I would like to take one of these options and add it to Model.Options with the saved value IF its checkbox is selected. The idea here is that, on the HttpPost controller method, I want to only acknowledge/save the value in the textbox if its corresponding checkbox is selected. What's the best way to approach this?
I've come across this explanation of binding to a list, but I'm not sure how to evolve upon that to create what I want.

Start by creating a view model to represent what you want to display/edit
public class OptionVM
{
public string Text { get; set; }
public bool IsSelected { get; set; }
public string Answer { get; set; }
.... // other properties
}
Then in the controller, initialize a collection of your view model and pass it to the view
public ActionResult Edit()
{
List<OptionVM> model = new List<OptionVM>();
.... // populate it
return View(model);
}
and the view
#model List<yourAssembly.OptionVM>
#using (Html.BeginForm())
{
for (int i = 0; i < Model.Count; i++)
{
<label>
#Html.CheckBoxFor(m => m[i].IsSelected)
<span>#Model[i].Text</span>
</label>
#Html.TextBoxFor(m => m[i].Answer)
#Html.ValidationMessageFor(m => m[i].Answer)
}
<input type="submit" ... />
}
and submit to
public ActionResult Edit(List<OptionVM> model)
{
// for example, get the answers where the checkbox has been selected
var selectedAnswers = model.Where(m => m.IsSelected).Select(m => m.Answer);
}
and you could enhance this by using a foolproof [RequiredIfTrue] or similar validation attribute applied to the Answer property to ensure the textbox has a value if the corresponding checkbox is checked

There is no need to have two inputs. You can use the "value" attribute of the checkboxes, and store that information. Also, in your controller, you can simply add a parameter with the same name as your input.
View
#using (Html.BeginForm())
{
foreach (string option in ViewBag.OptionsAvailable)
{
<input type='checkbox' name='checkboxes' value="#option" />
#option <!-- text to display -->
}
<button type="submit">Submit</button>
}
Controller
[HttpGet]
[ActionName("Index")]
public ActionResult GetCar()
{
ViewBag.OptionsAvailable = new List<string>
{
"Red",
"Yellow",
"Blue"
};
return View(new Car());
}
[HttpPost]
[ActionName("Index")]
public ActionResult PostCar(string[] checkboxes)
{
Car car = new Car
{
ColorsSelected = new List<string>()
};
foreach (string value in checkboxes)
{
car.ColorsSelected.Add(value);
}
return RedirectToAction("Index");
}

Related

Razor code producing unexpected result when looping through a list of checkboxes

What possible reason could there be for this razor code:
// ShowSelectedMembers.cshtml
#model MyApp.Models.MembersViewModel
#for (int m = 0; m < Model.Members.Count; m++)
{
<input type="hidden" asp-for="#Model.Members[m].Id" />
<input type="hidden" asp-for="#Model.Members[m].FirstName" />
<input type="hidden" asp-for="#Model.Members[m].LastName" />
<span>[debug-info: m = #m, id = #Model.Members[m].Id]</span>
<span>#Model.Members[m].LastName</span>,
<span>#Model.Members[m].FirstName</span>
}
to produce this HTML, when Model.Members.Count = 1, just containing the member with id 6653:
<input type="hidden" data-val="true" data-val-required="The Id field is required." id="Members_0__Id" name="Members[0].Id" value="6652" />
<input type="hidden" id="Members_0__FirstName" name="Members[0].FirstName" value="Peter" />
<input type="hidden" id="Members_0__LastName" name="Members[0].LastName" value="Hanson" />
<span>[debug-info: m = 0, id = 6653]</span>
<span>Swanson</span>,
<span>Lisa</span>
How can Members[0].Id have the value of 6652 in the hidden field, and 6653 inside the <span>?
This is the controller method for the view:
public IActionResult ShowSelectedMembers(MembersViewModel vm)
{
vm.Members = vm.Members.Where(s => s.Selected).OrderBy(o => o.LastName).ThenBy(o => o.FirstName).ToList();
return View(vm);
}
This is the form which sends the whole member list to the controller method:
// Index.cshtml
#model MyApp.Models.MembersViewModel
<form asp-action="ShowSelectedMembers">
<button type="submit">View selection</button>
#if (Model.Members.Any())
{
for (int i = 0; i < Model.Members.Count; i++)
{
Model.Members[i].Id
Model.Members[i].FirstName
Model.Members[i].LastName
<input type="checkbox" asp-for="#Model.Members[i].Selected" />
<input type="hidden" asp-for="#Model.Members[i].Id" />
<input type="hidden" asp-for="#Model.Members[i].FirstName" />
<input type="hidden" asp-for="#Model.Members[i].LastName" />
}
}
</form>
When the form data is sent to the controller method, it contains all the members in that view. The method then filters out all members with Selected set to false.
These are the ViewModels:
public class MembersViewModel
{
// ... some more properties
public List<MemberViewModel> Members { get; set; }
}
public class MemberViewModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool Selected { get; set; }
// ... some more properties
}
UPDATE
To clarify the flow of this operation:
The Index-view contains a form with Selected-checkboxes for each member in the list.
This form passes the whole list of members to the controller method ShowSelectedMembers.
The controller method filters out members with Selected set to false, and passes the selected members to the view ShowSelectedMembers.
The view displays the filtered list, which now has corrupted data.
UPDATE 2
I changed the controller method to this, creating a new instance instead of reusing the viewmodel:
public async Task<IActionResult> ShowSelectedMembers(MembersViewModel vmIn)
{
List<int> memberIds = new List<int>();
foreach (MemberViewModel member in vmIn.Members.Where(s => s.Selected).ToList())
{
memberIds.Add(member.Id);
}
List<Member> selectedMembers = await db.Members
.Where(s => memberIds.Contains(s.Id))
.ToListAsync();
MembersViewModel vmOut = new MembersViewModel {
Members = auto.Map<List<MemberViewModel>>(selectedMembers)
};
return View(vmOut);
}
... but the result is exactly the same.
UPDATE 3
This is so weird!
It appears to be working correctly when I change the razor code to this in ShowSelectedMembers.cshtml:
#*<input type="text" asp-for="#Model.Members[i].Id" />*#
#{
string _id = $"Members_{i}__Id";
string _name = $"Members[{i}].Id";
string _value = Model.Members[i].Id.ToString();
}
<input type="hidden"
data-val="true"
data-val-required="The Id field is required."
id=#_id
name=#_name
value=#_value />
I.e. manually generating the input field rather than using asp-for.
In your first example, you can add ModelState.Clear() and it should give you the result you want.
public IActionResult ShowSelectedMembers(MembersViewModel vm)
{
ModelState.Clear();
vm.Members = vm.Members.Where(s => s.Selected).OrderBy(o => o.LastName).ThenBy(o => o.FirstName).ToList();
return View(vm);
}
However, I'd follow the advice here https://stackoverflow.com/a/1775185/2030565 and adopt the PRG pattern -- POST the form then redirect to a GET action. You will need to store the model in the TempData store if you can't rehydrate the model from a database.

Calling httppost actionresult from inside a view using a button

I have a project to make an online shop between users (post a product, buy, etc.) using a database. In this project I have a view called "ShoppingCart":
#model IEnumerable<MyFirstProject.Models.Product>
#{
ViewBag.Title = "ShoppingCart";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Your Shopping Cart</h2>
#if (Model == null)
{
<div style="float:left">Your cart is empty.</div>
<div>
Total payment: 0
</div>
}
else
{
decimal tPrice = 0;
<div>
<table style="float:left">
#foreach (var product in Model)
{
tPrice = tPrice + product.Price;
{ Html.RenderPartial("ProductLine", product);}
}
</table>
</div>
<div>
Total payment: #tPrice
</div>
}
It receives a list of products which the user decided to buy and displays them (not the important part). I need to add a button which will send the list to an action result in the "ShoppingController":
[HttpPost]
public ActionResult ShoppingCart(List<Product> bought)
{
if (ModelState.IsValid)
{
foreach (var listP in bought.ToList())
{
foreach (var databaseP in db.Products.ToList())
{
if (listP.ProductID == databaseP.ProductID)
{
databaseP.State = 1;
db.SaveChanges();
break;
}
}
}
return RedirectToAction("Index");
}
else
{
return View(bought);
}
}
"State" indicates if the product was bought or not (0=not bought, 1=bought), db is the database
If you wan't to post any data from a view to an action method, you should keep that data in form elements and keep that in a form. Since you want to post a collection of items, You may use Editor Templates.
Let's start by creating a view model.
public class ShoppingCartViewModel
{
public decimal TotalPrice { set; get; }
public List<Product> CartItems { set; get; }
}
public class Product
{
public int Id { set; get; }
public string Name { set; get; }
}
Now in your GET action, you will create an object of the ShoppingCartViewModel, load the CartItems property and send to the view.
public ActionResult Index()
{
var cart = new ShoppingCartViewModel
{
CartItems = new List<Product>
{
new Product { Id = 1, Name = "Iphone" },
new Product { Id = 3, Name = "MacBookPro" }
},
TotalPrice = 3234.95
};
return View(cart);
}
Now i will create an EditorTemplate. To do that, Go to your ~/Views/YourControllerName folder, and Create a directory called EditorTemplates and add a view with name Product.cshtml
The name of the file should match with the name of the type.
Open this new view and add the below code.
#model YourNamespace.Product
<div>
<h4>#Model.Name</h4>
#Html.HiddenFor(s=>s.Id)
</div>
You can keep the display however you want. But the important thing is, We need to keep a form field for the productId. We are keeping that in a hidden field here.
Now let's go back to our main view. We need to make this view strongly typed to our ShoppingCartViewModel. We will use the EditorFor html helper method in this view to call our editor template
#model ReplaceYourNamespaceHere.ShoppingCartViewModel
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.CartItems)
<p>Total : #Model.TotalPrice</p>
<input type="submit" />
}
And in your HttpPost action method, We will have a paramer of type ShoppingCartViewModel. When the form is submitted, MVC Model binder will map the posted form values to an object of ShoppingCartViewModel.
[HttpPost]
public ActionResult Index(ShoppingCartViewModel model)
{
foreach (var item in model.CartItems)
{
var productId = item.Id;
// to do : Use productId and do something
}
return RedirectToAction("OrderSucessful");
}
You can iterate through the CartItems collection and get the Id of the Products and do whatever you want.
If you wan't to allow the user to edit the items (using a check box) in this page, Take a look at this answer. It is basically same, but you add a boolean property to Product class and use that for rendering a checkbox.

Use simple MVC Html.DropDownList to control visibility of a div, onLoad and on selectChange

I have the following code:
<div class="form-group">
<label class="col-xs-3 control-label">Intermediary Bank Required?:</label>
<div class="col-xs-9">
<p class="form-control-static">#Html.DropDownList("IntermediaryRequired",(SelectList)ViewBag.IntermediaryRequired,"NO", new { #class = "form-control" })</p>
</div>
</div>
IntermediaryRequired is a bool field on my model
I also have this Extension Helper:
public static class BooleanExtensions
{
public static string ToYesNoString(this bool value)
{
return value ? "YES" : "NO";
}
public static string ToDislay(this bool value)
{
return value ? "normal" : "none";
}
public static string ToChar(this bool value)
{
return value ? "1" : "0";
}
}
My aim is to hide/display a <div> in response to the selected value in the DropDownList for two cases:
when the user Manually changes the DropDownList selection
when the form loads with an existing value for the model field IntermediaryRequired
Please how can we achieve this.
You should be able to do this with a little bit of javascript. Listen to the change event of the dropdown, check the value and hide/show the div. do the samething on document.ready (page loaded) as well to work with existing value of the model.
<script type="text/javascript">
$(function(){
//On page load, update the visiblity
var v=$("#IntermediaryRequired").val();
UpdateDivVisibility(v);
//When user changes the dropdown, update visibility
$("#IntermediaryRequired").change(function(e){
var v=$("#IntermediaryRequired").val();
UpdateDivVisibility(v);
});
});
function UpdateDivVisibility(isVisible)
{
if(v=="YES")
{
$("#DivIdtoHide").show();
}
else
{
$("#DivIdtoHide").hide();
}
}
</script>
EDIT : As per the question in the comment
Usually I create a viewmodel like this
public class CreateCustomerVM
{
public string Name { set;get;}
public List<SelectListItem> IntermediaryOptions {set;get;}
public string IntermediaryRequired {set;get;}
public CreateCustomerVM()
{
this.IntermediaryOptions =new List<SelectListItem>()
}
}
and in your GET actions for create
public ActionResult create()
{
var vm = new CreateCustomerVM();
vm.IntermediaryOptions = GetOptions();
return View(vm);
}
private List<SelectListItem> GetOptions()
{
return new List<SelectListItem>
{
new SelectListItem {Value = "0", Text = "No"},
new SelectListItem {Value = "1", Text = "Yes"}
};
}
And your view will be bounded to the viewmodel
#model CreateCustomerVM
#using(Html.Beginform())
{
<div>
<p>Required?</p>
<p>#Html.DropdowListFor(s=>s.IntermediaryRequired,Model.IntermediaryOptions)
<div id="ExtraOptions">
<!-- Your additional UI elements here -->
</div>
<input type="submit" />
</div>
}
In your Form post, you can read the IntermediaryRequired value and convert that to boolean value
[HttpPost]
public ActionResult Create(CreateCustomerVM model)
{
//check model.IntermediaryRequired
// to do : Save and Redirect(PRG pattern)
}
You can do something like this to show/hide the div when the user Manually changes the Drop Down
var yourDiv = $('#yourDiv');
$('#IntermediaryRequired').on('change', function(){
if ($(this).val() == 'YES') {
yourDiv.show();
}
else {
yourDiv.hide();
}
});
And to get the same result on page load you can try
#if (Model.value)
{
<div id="yourDiv">
....
</div>
}
As a side note use p only when you want to add a paragraph, if you just want to add -meaningless- block element, you can use a div. You can read more about semantics here.

Html.EnumDropdownListFor: Showing a default text

In my view I have a enumdropdownlist (a new feature in Asp.Net MVC 5.1).
#Html.EnumDropDownListFor(m => m.SelectedLicense,new { #class="form-control"})
If I execute the above code I get dropdownlist for my following enum.
public enum LicenseTypes
{
Trial = 0,
Paid = 1
}
but by default I want my dropdownlist to have a value(custom text)
and this is what I tried
#Html.EnumDropDownListFor(m => m.SelectedLicense,"Select a license" ,new { #class="form-control"})
but now the problem is when i run it, my dropdownlist looks like this
So, the default text I want to show doesn't appear by default.
If a user selects "select a license" and tries to submit the form, it does show an error saying "select a license" but it doesn't show as default text.
Something i need to change?
Ps: The image is the screenshot of the page when it loads. By default it'll show Trial as selected option.
Try to change the Index of LicenseTypes start from 1 not 0 like below:
public enum LicenseTypes
{
Trial = 1,
Paid = 2
}
Then you can use Range attribute to validate the selected license type like below:
public class YourViewModel
{
//Other properties
[Range(1,int.MaxValue,ErrorMessage = "Select a correct license")]
public LicenseTypes LicenseTypes { get; set; }
}
Finally, in your view:
#Html.EnumDropDownListFor(m => m.LicenseTypes,"Select a license",new { #class = "form-control"})
#Html.ValidationMessageFor(m => m.LicenseTypes)
By the time your EnumDropDownListFor is rendered SelectedLicense already has the default value for the type, which is 0.
Just change the type of your SelectedLicense property to a nullable enum, like so:
public LicenseTypes? SelectedLicense { get; set; }
This also allows you to continue using the Required attribute, which I think is significantly cleaner. The Required attribute will not allow a null response, so even though your model allows nulls, the form will not.
I have an enum:
public enum Sex
{
Male,
Female
}
In my model I have:
[DisplayName("Sex")]
[Required]
public Sex? Sex { get; set; }
An in the view:
#Html.EnumDropDownListFor(model => model.Sex, "Select sex", new { #class = "form-control", type = "text"})
By this I have a dropdown with default option "Select sex", but validation accepts only options provided by enum ("Male" and "Female").
In MVC3 (without EnumDropDownListFor) I used in model:
[DisplayName("Sex")]
[Required(AllowEmptyStrings=false)]
public Sex? Sex { get; set; }
Sex = null;
Sexes = Repository.GetAutoSelectList<Sex>("");
In view:
#Html.DropDownListFor(model => model.Sex, Model.Sexes, new { #class = "form-control", type = "text" })
The ViewModel class needs to have the default value set on the enum property for it to be the default selected
public
public class Test
{
public Cars MyCars { get; set; }
public enum Cars
{
[Display(Name = #"Car #1")]
Car1 = 1,
[Display(Name = #"Car #2")]
Car2 = 2,
[Display(Name = #"Car #3")]
Car3 = 3
}
}
Controller:
public class EnumController : Controller
{
// GET: Enum
public ActionResult Index()
{
var model = new Test {MyCars = Test.Cars.Car3}; // set default value
return View(model);
}
[HttpPost]
public ActionResult Index(Test model)
{
.....
}
}
View:
#Html.BeginForm()
{
<div class="panel bg-white">
<div class="panel-header fg-white">
Enums
</div>
<div class="panel-content">
<div class="input-control select size3">
#Html.EnumDropDownListFor(model => model.MyCars)
</div>
</div>
<input type="submit" class="button success large" />
</div>
}
Am I a bit late ?
Changing the values of the enum type is not very satisfying.
Neither is changing the model property to render it nullable and then add a [Required] attribute to prevent it to be nullable.
I propose to use the ViewBag to set the default selected value of the dropdown.
The line 4 of the controller just bellow is the only important one.
EDIT : Ah... newbies... My first idea was to use ModelState.SetModelValue because my newbie instinct prevented me to simply try to set the desired value in the ViewBag since the dropdown was binded to the model. I was sure to have a problem: it would bind to the model's property, not to the ViewBag's property. I was all wrong: ViewBag is OK. I corrected the code.
Here is an example.
Model:
namespace WebApplication1.Models {
public enum GoodMusic {
Metal,
HeavyMetal,
PowerMetal,
BlackMetal,
ThashMetal,
DeathMetal // . . .
}
public class Fan {
[Required(ErrorMessage = "Don't be shy!")]
public String Name { get; set; }
[Required(ErrorMessage = "There's enough good music here for you to chose!")]
public GoodMusic FavouriteMusic { get; set; }
}
}
Controller:
namespace WebApplication1.Controllers {
public class FanController : Controller {
public ActionResult Index() {
ViewBag.FavouriteMusic = string.Empty;
//ModelState.SetModelValue( "FavouriteMusic", new ValueProviderResult( string.Empty, string.Empty, System.Globalization.CultureInfo.InvariantCulture ) );
return View( "Index" );
}
[HttpPost, ActionName( "Index" )]
public ActionResult Register( Models.Fan newFan ) {
if( !ModelState.IsValid )
return View( "Index" );
ModelState.Clear();
ViewBag.Message = "OK - You may register another fan";
return Index();
}
}
}
View:
#model WebApplication1.Models.Fan
<h2>Hello, fan</h2>
#using( Html.BeginForm() ) {
<p>#Html.LabelFor( m => m.Name )</p>
<p>#Html.EditorFor( m => m.Name ) #Html.ValidationMessageFor( m => m.Name )</p>
<p>#Html.LabelFor( m => m.FavouriteMusic )</p>
<p>#Html.EnumDropDownListFor( m => m.FavouriteMusic, "Chose your favorite music from here..." ) #Html.ValidationMessageFor( m => m.FavouriteMusic )</p>
<input type="submit" value="Register" />
#ViewBag.Message
}
Without the "ModelState.SetModelValue or ViewBag.FavouriteMusic = string.Empty" line in the model Index action the default selected value would be "Metal" and not "Select your music..."

All Dropdown Items have ZERO index MVC3

I have populated a dropdown list with values from Database Table. The list gets populated with correct table data but all values have ZERO index in the list. Here is the code to fill dropdown list:
//Get
public ActionResult NewBooking()
{
var db = new VirtualTicketsDBEntities2();
IEnumerable<SelectListItem> items = db.Attractions
.ToList()
.Select(c => new SelectListItem
{
Value = c.A_ID.ToString(),
Text = c.Name
});
ViewBag.Attractions = items;
return View();
}
And on Dropdown View Page:
<div class="editor-label">
#Html.LabelFor(model => model.Attraction)
</div>
<div class="editor-field">
#Html.DropDownList("Attractions")
</div>
For example if table have 3 values A,B, and C. These values are appearing in dropdown list but when I get its selected index in POST request function, it always returns ZERO. Here is the POST submit function:
//Post
[HttpPost]
public ActionResult NewBooking(BookingView booking)
{
try
{
BookingManager bookingManagerObj = new BookingManager();
bookingManagerObj.Add(booking);
ViewBag.BookingSavedSucess = "Booking saved!";
return View("WelcomeConsumer","Home");
}
catch
{
return View(booking);
}
}
booking.Attraction is always ZERO even user selected greater than ZERO index item.
Any suggestions?
I would guess that it is because you are getting a collection of SelectListItems back and not an actual SelectList. Try something like:
<div class="editor-field">
#Html.DropDownListFor(model => model.Attraction, new SelectList(ViewBag.Attractions, "Value", "Text");
It's best not to use ViewBag, you should always use a ViewModel.
Say you have a ViewModel like this:
public class AttractionViewModel
{
public int AttractionId { get; set; }
public SelectList Attractions { get; set; }
}
and modify your view like this - I presume you already have a form in there, the relevant bit is the #Html.DropDownListFor(...) and making sure you have the full namespace to the ViewModel if you haven't already included it in the Views web.config file:
#model AttractionViewModel
#using(Html.BeginForm("NewBooking", "ControllerName"))
{
<div class="editor-label">
#Html.LabelFor(model => model.AttractionId)
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.AttractionId, Model.Attractions)
</div>
<input type="submit" value="Submit">
}
and modify your HttpGet like this:
//Get
public ActionResult NewBooking()
{
var db = new VirtualTicketsDBEntities2();
var items = db.Attractions.ToList();
var attractionIdDefault = 0;// default value if you have one
var vm = new AttractionViewModel {
AttractionId = attractionIdDefault,// set this if you have a default value
Attractions = new SelectList(items, "A_ID", "Name", attractionIdDefault)
}
return View(vm);
}
and create an HttpPost ActionResult like this:
// Post
public ActionResult NewBooking(AttractionViewModel vm)
{
var attractionId = vm.AttractionId; // You have passed back your selected attraction Id.
return View();
}
Then it should work.
I know that you have already selected your answer but here is an alternative way of doing what you did. When I started off with ASP.NET MVC I struggled with SelectListItem and found another way of populating my drop down list. I have stuck to this way ever since.
I always have a view model that I bind to my view. I never send through a domain model, always a view model. A view model is just a scaled down version of your domain model and can contain data from multiple domain models.
I have made some modifications to your code and tips, but like I mentioned, it's just an alternative to what you already have.
Your domain model could look like this. Try and give your property names some meaningful descriptions:
public class Attraction
{
public int Id { get; set; }
public string Name { get; set; }
}
You view model could look something like this:
public class BookingViewModel
{
public int AttractionId { get; set; }
public IEnumerable<Attraction> Attractions { get; set; }
// Add your other properties here
}
Do not have your data access methods in your controllers, rather have a service layer or repository expose this functionality:
public class BookingController : Controller
{
private readonly IAttractionRepository attractionRepository;
public BookingController(IAttractionRepository attractionRepository)
{
this.attractionRepository = attractionRepository;
}
public ActionResult NewBooking()
{
BookingViewModel viewModel = new BookingViewModel
{
Attractions = attractionRepository.GetAll()
};
return View(viewModel);
}
[HttpPost]
public ActionResult NewBooking(BookingViewModel viewModel)
{
// Check for null viewModel
if (!ModelState.IsValid)
{
viewModel.Attractions = attractionRepository.GetAll();
return View(viewModel);
}
// Do whatever else you need to do here
}
}
And then your view will populate your drop down like this:
#model YourProject.ViewModels.Attractionss.BookingViewModel
#Html.DropDownListFor(
x => x.AttractionId,
new SelectList(Model.Attractions, "Id", "Name", Model.AttractionId),
"-- Select --"
)
#Html.ValidationMessageFor(x => x.AttractionId)
I hope this helps.

Categories

Resources