SelectList from Enum .NET Core - asp.net

I am using the following syntax in my View to create a dropdown list from a Enum in my class. The error is
One or more compilation references are missing. Possible causes include a missing preserveCompilationContext property under buildOptions in the application's project.json.
<div class="form-group">
<select asp-for="Subscription" class="form-control" asp-items="#new SelectList(Enum.GetNames(typeof(SubscriptionTypes)))">
<option disabled selected value="">Select a Subscription</option>
</select>
</div>
The Enum in the class is:
public enum SubscriptionTypes
{
Type1, Type2, Type3
}

You should use the build in generator, to generate userfriendly Values. See [Display(Name="Some Cool Name with whitespaces"] Annotation on enum values.
Html.GetEnumSelectList<CountryEnum>()
So your razor page would look like this:
#model CountryEnumViewModel
<form asp-controller="Home" asp-action="IndexEnum" method="post">
<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<Some.NameSpace.CountryEnum>()">
</select>
<br /><button type="submit">Register</button>
</form>
More info here: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-2.2#the-select-tag-helper

Common root cause for this problem is using of non-fully qualified type names (like SubscriptionTypes) when no using directive is specified.
To fix it either add using directive at the source top:
#using Your.Namespace.Here
or use fully qualified type names:
<select asp-for="Subscription" class="form-control" asp-items="#new SelectList(Enum.GetNames(typeof(Your.Namespace.Here.SubscriptionTypes)))">

I think this was a pretty elegant solution. I will use one of my own examples to demonstrate. I have a Model where one of the fields needs to be selected from an enumeration. The Model is called DonationMaterials and the property is Availability
public string? Availability { get; set; } = String.Empty;
This property can have 3 values; Available, Not Available, or Unknown. I created a public static method under the model called AvailabilityOptions
public static IEnumerable<SelectListItem>? AvailabilityOptions()
{
return new[]
{
new SelectListItem { Text = "Available", Value = "Available"},
new SelectListItem { Text = "Not Available", Value = "Not Available"},
new SelectListItem { Text = "Unknown", Value = "Unknown"}
};
}
Once I created the static method, anytime I need to fill the SelectList I simply use the code in the cshtml for the page. You will need to include the Namespace of the model to have accessibility to the static method. This is clean. You could also answer the enumeration from the static method and use code to convert the enumeration into a collection of SelectListItem's
<select asp-for="DonationMaterials.Availability" asp-items="DonationMaterials.AvailabilityOptions()" class="form-control"></select>
The beauty of this approach is that the enumeration is really a Domain (mode) specific data and it keeps it where it belongs (in the model). The use of the model in the interface is quite simple.

Related

Is using Viewbag for remembering past form inputs a bad idea?

I have a small asp.net core mvc application that basically consists of a form that a user can input some constraints into, and then get a filtered list of data depending on those constraints.
The controller action for filtering data basically looks like this:
[HttpPost]
public async Task<IActionResult> Query(QueryModel query)
{
var customers = await _context.Customers.AsQueryable().FilterCustomerList(query);
return View("Index", customers);
}
Now, my issue is that I would like the inputs in the fields to persist after entering them and being redirected to the view again. Right now they are currently just reset.
One way of doing this that I found was using viewBag. An example for a single query attribute is this:
public async Task<IActionResult> Query(QueryModel query)
{
var customers = await _context.Customers.AsQueryable().FilterCustomerList(query);
ViewBag.Name = query.Name;
return View("Index", customers);
}
and then the inpuit html elelment would look like:
<div class="col-md-4">
<input name="Name" type="text" placeholder="First name" value="#ViewBag.Name"class="form-control">
</div>
And this makes sure that if something has been entered into a field, it will now be entered into the field when after the query has been submitted.
But when I read up on ViewBag, I understand that a lot of .net developers have an aversion to it. It's not safe, the compiler can't catch errors in it easily etc.
Also, If I were to add all the input fields in my form to the viewbag, I would need a lot of lines of ViewBag.Attribute = query.SomeAttribute (20-30). Which seems like a code-smell too.
Is there any nicer way to do what I am trying to here?
You haven't included your QueryModel class and that class could be a key point to a cleaner approach.
You see, usually the user data, POSTed to your action is bound to the model, from there it's rendered on the form and is POSTed again. The model binding is where an input of a specific name is bound to a model member of the same name.
Thus, there's no need for viewbags.
More formally:
The Model
public class QueryModel
{
[your-validators-in-attributes, e.g. Required or MaxLength
there can be multiple validators]
public string Name { get; set; }
}
The controller:
[HttpPost]
async Task<IActionResult> Query(QueryModel query)
{
// query.Name is there, bound from the view
}
The View:
#model .....QueryModel
<div>
#Html.TextBoxFor( m => m.Name, new { placeholder = "a placeholder" } )
</div>
The html helper does two things
renders an input of the given name (Name in this case)
sets its value depending on the actual value from the model
In newer ASP.NETs you can achieve similar result by using tag helpers, where instead of Html.TextBoxFor(...) you write
<input asp-for="Name" />
These two approaches, using html helpers or using tag helpers are equivalent. In both cases there's no need for view bags.

Blazor: binding to a MultiSelectList (ideally with a checkbox)

Experimenting with Blazor (Server, if that makes any difference), and I'm having difficulty getting binding to a MultiSelectList to work....
Bit of background: I'm dealing with EF Core and have a Many-to-Many relationship, let's say between people and cars. I'm currently loading a page that shows the existing details, and allowing the user to update this page.
So in my Service, I load my Person entity from the DB, and this includes the details of all the cars they currently own. I also load the list of all the available cars. My Service method then creates a MultiSelectList and adds it to my ViewModel (to be returned to the Razor Page):
Service method
vm.CarSelector = new MultiSelectList(
allCars,
nameof(Car.CarId),
nameof(Car.Name),
person.OwnedCars.Select(oc => oc.CarId));
This is fictitious code, but I hope you get the picture. When debugging this (in the Service method) I can see that this MultiSelectList has an entry for every car, and the ones that are already selected are showing as Selected. Great!
Blazor Razor Page
So, this is where I come unstuck.... I can't work out how to do the two-way data-binding of a Razor control to this object.
I'm trying to use an <InputSelect />, but that might not be the best control to use.
ideally (actually, that's more of a "must have"), each option should have CheckBox.
I'm wondering whether the use of a MultiSelectList really buys me anything
Checkboxes are a bit different in blazor. Normally you would use the bind-value attribute on an input element as shown below, however, this is not recommended as you will only be able to read the value and NOT update the UI by changing the boolean value via code:
<input type="checkbox" #bind-value="#item.Selected"/>
Instead, use the #bind syntax for checkboxes, which is much more robust and will work both ways (changing the bound boolean value from code & interacting with the checkbox on the UI). See the syntax below:
<input type="checkbox" #bind="#item.Selected"/>
The bind attribute will automatically bind your boolean value to the "checked" property of the html element.
Also make sure you are binding to the "Selected" property rather than the "Value" property.
Using the built in bind will prevent the need to manually setup events as you did in your answer. You can also get rid of the if/else block and merge your code into a single code flow since you are now binding to the boolean rather than setting the checked property manually. If you still need to tap into an event to fire off some process(maybe hiding parts of UI on checking a box), I'd suggest using the onclick event and manually passing in the multiselect Item for each line. Here is the final code:
#foreach(var item in list)
{
<input type="checkbox" #bind="item.Selected" #onclick="(()=>handleClick(item))" />
}
#foreach(var item in list.Where(x=>x.Selected))
{
<p> Item #item.Text is Selected</p>
}
#code {
MultiSelectList list = new MultiSelectList(new List<Car> { new Car { Year = 2019, Make = "Honda", Model = "Accord" }, new Car { Make = "Honda", Model = "Civic", Year = 2019 } });
private void handleClick(SelectListItem item)
{
//Do something crazy
}
}
I got this to work with a component that takes the MultiSelectList as a parameter. There may be more elegant ways to achieve this (please do update if you know of a better way).
#using Microsoft.AspNetCore.Components
#using Microsoft.AspNetCore.Mvc.Rendering
<div class="multiselect">
<div id="checkboxes">
#foreach (var item in this.Items)
{
<div>
<label for="#item.Value">
#if (item.Selected)
{
<input type="checkbox" id="#item.Value" checked="checked" #onchange="#((e) => CheckboxChanged(e, item.Value))" />
}
else
{
<input type="checkbox" id="#item.Value" #onchange="#((e) => CheckboxChanged(e, item.Value))" />
}
#item.Text
</label>
</div>
}
</div>
</div>
#code
{
[Parameter]
public MultiSelectList Items { get; set; } = null!;
private void CheckboxChanged(ChangeEventArgs e, string key)
{
var i = this.Items.FirstOrDefault(i => i.Value == key);
if (i != null)
{
i.Selected = (bool)e.Value;
}
}
}

ASP CORE: #Html.ValidationMessage migration to asp-validation-for or similar tag helper

How to setup the asp-validation-for with string value, not with expression?
I want to migrate the multiselect list:
#Html.ListBox("Privileges", ViewBag.PrivilegesMultiSelectList as MultiSelectList)
#Html.ValidationMessage("Privileges", "")
to
<select multiple="multiple" name="Privileges" asp-items="#ViewBag.PrivilegesMultiSelectList"></select>
<span asp-validation-for="Privileges" class="text-danger"></span>
But the last line is invalid:
Error CS1061 '...Model' does not contain a definition for 'Privileges'
and no accessible extension method 'Privileges' accepting a first
argument of type '..Model' could be found (are you missing a using
directive or an assembly reference?)
I want to stay using tag-helper because of consistency.
This asp-validation-for="Privileges" is trying to look for Privileges property in your model (not ViewBag). If it doesn't exist, it will give you that error. That line is the equivalent of ValidationMessageFor(), and afaik there's no equivalent in .net core for ValidationMessage().
Have a look at the asp-validation-for tag helper, which as stated should align with the name of another taghelper.
How to setup the asp-validation-for with string value, not with expression?
Again, there's no equivalent for ValidationMessage() it in TagHelpers. So you can just use #Html.ValidationMessage().
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-2.2
it's important to recognize that Tag Helpers don't replace HTML Helpers and there's not a Tag Helper for each HTML Helper.
You could also just write your own tag helper with the ValidationMessage HtmlHelper
Some advice from the docs regarding ViewBags:
We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view model is more robust at providing MVC metadata and generally less problematic.
A better approach:
You will need to add the selected Privilege(s) to your model that you wish to return.
public class CustomViewModel {
[Required]
public string Privilege { get; set; } // update if you want to return multiple privileges
public List<SelectListItem> PrivilegesMultiSelectList { get; set; }
}
and then use that in your View #model CustomViewModel.
Using asp-for="Privilege" on your select, it becomes m => m.Privilege.
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-2.2
The asp-for attribute value is a special case and doesn't require a
Model prefix, the other Tag Helper attributes do (such as asp-items)
You can then just write it as such:
<select asp-for="Privilege" asp-items="#Model.PrivilegesMultiSelectList"></select>
<span asp-validation-for="Privilege" class="text-danger"></span>
I hope this helps.

Getting values of check-box from formcollection in asp.net mvc

I viewed some topics here but I still have a problem with getting values from checkboxes.
Part of Model :
public Dictionary<Language, bool> TargetLanguages { get; set; }
Part of View :
<div class="editor-label">
<label for="TargetLanguages">select target languages</label>
</div>
<div class="editor-field">
<form>
#foreach (var item in Model.TargetLanguages)
{
#Html.CheckBox("TargetLanguages["+item.Key.Name+"]", item.Value)
#item.Key.Name
}
</form>
</div>
Part of Controller :
[HttpPost, ActionName("AddDictionary")]
public ActionResult AddDictionary(FormCollection collection)
{
...
}
And the problem is I don't get any trace of TargetLanguages in my FormCollection. I tried CheckBoxFor but it wasn't help. I tried write check-box manually also.
EDITED : Okay, I just noticed where the problem was. I've got messed up markers and that was the reason why I can't get data from FormCollection.
Create all the checkboxes with the same name. In this sample I'm using 'SelectedTargetLanguages'.
#using (Html.BeginForm())
{
foreach (var item in Model.TargetLanguages)
{
<label>
#Html.CheckBoxFor(m => m.SelectedTargetLanguages, item.value)
#item.KeyName
</label>
}
<br/>
#Html.SubmitButton("Actualizar listado")
}
Then, in your action the parameter must be an array of strings like this:
public ActionResult AddDictionary(string[] selectedTargetLanguages)
Note that the name of the argument is the same name of the checkboxes. (It works even with the different casing).
You should use explicit arguments like this, rather than the generic FormCollection. Anyway, if you use FormCollection, you shpuld also receive the array.
I have asked same type of question previously. Please check the following links
MVC3 #Html.RadioButtonfor Pass data from view to controller
MVC3 #html.radiobuttonfor
I think this might helps you.

How to use multiple select in mvc 4?

I want to use multiple select in
Chosen.
I have Skill model like,
public class Skill
{
public int Id { get; set; }
public string Name { get; set; }
}
This works in my application:
<select data-placeholder="Choose a Country..." class="chzn-select" multiple >
<option value=""></option>
<option value="United States">United States</option>
<option value="Albania">Albania</option>
<option value="Algeria">Algeria</option>
</select>
I want to replace Countries data with my data. In controller i write:
var list = MyService.LoadAllSkills();
ViewBag.Skills = new MultiSelectList(list, "Id", "Name");
In view:
#Html.ListBox("Name", ViewBag.Skills as MultiSelectList,
new { #class = "chzn-select" } )
View result of #Html.ListBox() and #Html.DropDownList() is not like <select>
I get so result:
But, I want to get result as
How can I change Chosen sample?
The only difference I can see between the hardcoded example (which you stated that is working) and the one you generate with the ListBox helper is the absence of the data-placeholder attribute. So:
#Html.ListBox(
"Countries",
ViewBag.Skills as MultiSelectList,
new { #class = "chzn-select", data_placeholder = "Choose a Country..." }
)
This should at least generate the same markup as what you said is working. If it doesn't work then probably you haven't setup the plugin correctly or you have some other javascript errors. Read the documentation of the plugin about how it should be setup.
as other dudes mentioned, it seems your problem cause is not server-side (razor), it's actually client-side (most probably your Jquery plugin initialization).
probably when the plugin initialization called the html DOM is not generated yet, put your plugin initialization script at the end of the body or inside $(document).ready() and don't forget to take a look at the console to see if there is any errors
happy coding

Resources