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

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;
}
}
}

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.

Insert id into list if checkbox is checked

Net MVC and I have a table with products I want to buy.
I want to use a Checkbox to confirm the purchase.
But how can I insert the ID in a List<int>?
I have a class Approved with an attribute List<int> approve
I tried:
#Html.CheckBoxFor(m.approve.Add(id))
#Html.CheckBoxFor(m=>m.approve.Add(id))
#Html.CheckBoxFor(ViewBag.l.approve.Add(id)
How can that task be done ?
You can easyly bind a list to a model:
Model Binding To A List
So, you simply need to make a list of a class created for this, which includes the integer id, and a boolean to hold the checkbox state, somethign like this:
public class IdState
{
public int Id {get;set;}
public bool Checked {get;set;}
}
The list must be of this class, i.e. List<IdState>
The key is using a syntax like this:
Html.CheckBoxFor(m => m[i].Product)
Which will produce and input whose name is something like this:
name="[0].Product"
Then, the model binder will bind your list automatically. Of course, your post action must recevied a List<IdState> to function properly.
u can use html to insert id to your checkbox
#model yourModel
//for generate checkbox
<input type="checkbox" #(yourmodel ? " checked=checked " : null) name="YourCheckBoxName" value="#model.ID" >
use formCollection for get value in your controller from your view
I solved this problem that way:
This Javascript function walks through all checkboxes and saves the Id of the checked once to an hidden input.
<script>
function submit() {
var x = new Array();
for (i = 0; i < ids.length; i++){
if ($("#" + ids[i].toString() + ".cb").is(':checked')) {
x.push(ids[i]);
}
}
console.log(x.toString());
$("#str").val(x.toString());
}
</script>
After toggling a checkbox the value of the hidden input is refreshed
<script>
$(document).ready(function () {
$("input.cb").change(function () {
submit();
});
});
</script>
Here I create an array with all possible ids to improve simplicity and performance.
Otherwise I had to walk through all possible ids and check them.
<script>ids.push(#id);</script>
This is my checkbox:
<input type="checkbox" value="#id" id="#id" class="cb" />

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.

Vary type of input control based on drop down selection?

I'm building an edit screen where a use can edit rows of data. One of the fields is represented by a drop down, and another is an input field named 'value'. Now, depending on the value in the dropdown, I need to have different kinds of input controls for the value input control. Sometimes it should be a text box, others a datetime control (html5 and / or jqUI date picker), and finally a dropdown list containing a fixed set of values ('Yes' / 'No').
So basically sometimes I need to accept any string data, sometimes a date, and sometimes a boolean (but with a select box, not a check box). What's my best option for implementing this? Ideally the value entered would not be lost moving from one kind of input to another, while the user is on this edit page. On post back, I have a single database value to store (its a sql_variant).
Also, I'm using asp.net mvc3 so an ideal solution will work with the normal Html.ValidateFor and Html.ValidationMessageFor methods.
After lot's of time in JSFiddle, I made this solution. And I think it's pretty cool. It wasn't really that hard. and you can adapt it to whatever you need. just click here.
basically I make variables to represent the possible values. then I make a variable to hold the active element.
Whenever the type selector changes, it calls the change() function which uses if() statements to check what was selected, and then it sets the active element accordingly.
And finally, it calls the hide() function which hides the inactive elements.
here is the updated version
RED ALERT: I realized this didn't work in FF (maybe it was just my browser but whatever).
so I fixed it here
The typical way I accomplish something like this is to actually store 3 different fields in the db for each of the different types of values. Then I create something like the following html:
<!-- Input type selector -->
<div class="cell variable-selector">
<select><option ...</select>
</div>
<!-- varied input -->
<div class="cell variable show-text">
<div class="text"><input type="textbox"></div>
<div class="date-picker"><input type="textbox" class="datepicker"></div>
<div class="drop-down-bool"><select><option ...</select>
</div>
Then I have css that hides or shows the correct input element based on which class the cell has:
div.variable div { display:none }
div.show-text div.text { display: inline }
div.show-date-picker div.date-picker {display: inline }
div.show-drop-down-bool div.drop-down-bool {display: inline}
lastly you can setup some javascript so that when you change your variable-selector you change the class of your variable cell. Which jquery one might do this as so:
$(document).ready(function() {
var variableSelector = $("div.variable-selector > select");
variableSelector.change(function() {
var type = $(this).text();
var class = "cell variable show-" + type;
var variableCell = $(this).parent().parent().find("variable");
variableCell.attr("class", class);
})
});
As a quick warning I wrote the above code on the fly in the stack overflow editor window so there might be a couple of syntax errors or a minor bug somewhere but the basic idea should work. Hope it helps.
--Adam
In case you want to make full use of mvc3 validations, consider this approach.
Model
public class MultiValueViewModel
{
[Required]
public string TextValue { get; set; }
[Required]
public bool? BooleanValue { get; set; }
public MultiValueType ValueType { get; set; }
}
public enum MultiValueType
{
Text,
Boolean
}
View
#model MultiValueViewModel
#Html.DropDownListFor(m => m.ValueType, new SelectList(new[]
{
MultiValueType.Text,
MultiValueType.Boolean
}), new { #id = "multi_value_dropdown" })
<p>
<div data-type="#MultiValueType.Text" class="multi-value-pane">
#Html.EditorFor(m => m.TextValue)
</div>
<div style="display: none" data-type="#MultiValueType.Boolean" class="multi-value-pane">
#Html.DropDownListFor(m => m.BooleanValue, new SelectList
(new [] {
new SelectListItem { Text = "Yes", Value = "true"},
new SelectListItem { Text = "No", Value = "false"}
}, "Value", "Text"), optionLabel: "[Not Set]")
</div>
</p>
Javascript:
<script type="text/javascript">
$(function () {
$("#multi_value_dropdown").change(function () {
var value = $(this).val();
$(".multi-value-pane").each(function () {
$(this).css("display", value == $(this).attr("data-type") ? "block" : "none");
});
});
})
Inside your controller, receive MultiValueViewModel value (alone or inside parent model), and based on selected ValueType save to database. Please note that you will need jquery.validate version 1.9 if you need to skip validation on hidden fields (e.g. :hidden).

Accessing models from view in MVC 2 Timesheet application?

I am trying to create a timesheet application in MVC 2, but I feel like I am still struggling to grasp the model/view relationships and all that.
The problem I have is, I want to let the user report a new time segment in a create view. But I want to have dropdownlists populated with Projects, Tasks, and Consultants from the model.
Basically the database structure looks like this:
(table) TimeSegments
TimeSegmentID
Hours
Date
ConsultantID (FK)
TaskID (FK)
ProjectID (FK)
(table) Projects
ProjectID
ProjectName
(table) Tasks
TaskID
TaskName
(table) Consultants
ConsultantID
ConsultantName
This design may be extended in future, right now I want to get basic functionality working before I complicate it further.
Now, I am passing the entire model to the create view (actually a viewmodel based on it, just to simplify some coding, but it might as well have been the entire model).
The problem is, normally when I have done similar things with a create view, I would have created a new object in the controller and passed that to the view. In this case it would have been the TimeSegment object, since it is a new time segment that should be created in the database. Then I could just submit it and update the database in the controller. However, if I only pass a new TimeSegment object to the view, I can't populate the dropdownlists with Projects, Tasks and Consultants.
And oppositely, if I only pass the entire model, how would I bind a textbox to a new TimeSegment to be updated in the database?
I feel like I need to send both a new TimeSegment object and the entire model to do this, but then I have no idea how I would access it (there's only that one "Model" to access from the view). Also, back in the controller after a submit, how would the controller know what to update?
I'm sure I'm just thoroughly confused still by the MVC way of thinking, but I would really appreciate it if someone could clarify this for me and tell me (as pedagogically as possible) what to do to solve this.
Okay, I will give it a shot.
MVC is not hard, but you do have to alter your way of thinking a bit. In MVC you have the Models (your data layer[s]), the Views and the Controllers.
Before we continue, I make the assumptions with my examples below that you are using LINQ to SQL for you data access layer (Model), and I have labeled it as dc.
The Controllers fetch and format the data out of the Models and hand it off to the Views to display. So lets start with your first view which would be the view to create a TimeSegment.
[HttpGet]
public ActionResult CreateTimeSegment() {
return View(new TimeSegmentView {
Consultants = dc.Consultants.ToList(),
Projects = dc.Projects.ToList(),
Tasks = dc.Tasks.ToList()
});
}
This action will create a TimeSegmentView object and pass that to the View as its Model. Keep in mind that this action is decorated with [HttpGet]. TimeSegmentView` is a container class for the objects you need to pass to the view to create your UI and it looks like this:
public class TimeSegmentView {
public IList<Consultant> Consultants { get; set; }
public IList<Project> Projects { get; set; }
public IList<Task> Tasks { get; set; }
public TimeSegment TimeSegment { get; set; }
}
NOTE: I'm not using the TimeSegment property yet, it's further down...
In the view make sure you have it inherit from TimeSegmentView. Assuming that you're following the default MVC project structure and with me taking the liberty to add a Views folder into the Models folder your full reference would look like this:
<%# Page Inherits="System.Web.Mvc.ViewPage<PROJECTNAME.Models.Views.TimeSegmentView>" %>
Now you've typed the view to that object and you can now interact with its properties. So, you can build a form such as:
<form action="/" method="post">
<p>
<label>Hours</label>
<input name="TimeSegment.Hours" />
</p>
<p>
<label>Date</label>
<input name="TimeSegment.Date" />
</p>
<p>
<label>Consultant</label>
<select name="TimeSegment.ConsultantID">
<% foreach (Consultant C in Model.Consultants) { %>
<option value="<%=C.ConsultantID%>"><%=C.ConsultantName%></option>
<% }; %>
</select>
</p>
<p>
<label>Project</label>
<select name="TimeSegment.ProjectID">
<% foreach (Project P in Model.Projects) { %>
<option value="<%=P.ProjectID%>"><%=P.ProjectName%></option>
<% }; %>
</select>
</p>
<p>
<label>Task</label>
<select name="TimeSegment.TaskID">
<% foreach (Task T in Model.Tasks) { %>
<option value="<%=T.TaskID%>"><%=T.TaskName%></option>
<% }; %>
</select>
</p>
</form>
As you can see it created 3 select fields and just performed loops in each of them to build up their values based off of the model.
Now, taking a submission of this form, we'll need to get the data and add it to our database with:
[HttpPost]
public RedirectToRouteResult CreateTimeSegment(
[Bind(Prefix = "TimeSegment", Include = "Hours,Date,ConsultantID,ProjectID,TaskID")] TimeSegment TimeSegment) {
dc.TimeSegments.InsertOnSubmit(TimeSegment);
dc.SubmitChanges();
return RedirectToAction("EditTimeSegment", new {
TimeSegmentID = TimeSegment.TimeSegmentID
});
}
Okay, first notice that I've named the action the same, but this one has an [HttpPost] decoration. I'm telling the action that I'm sending it a TimeSegment object and that I want it to bind the properties in the Include clause (this is mostly for security and validation). I then take the TimeSegment object I've passed in, add it to the data context, submit the changes and redirect. In this case I'm redirecting to another action to edit the object I just created passing in the new TimeSegmentID. You can redirect to what ever, this just felt appropriate to me...
[HttpGet]
public ActionResult EditTimeSegment(
int TimeSegmentID) {
return View(new TimeSegmentView {
Consultants = dc.Consultants.ToList(),
Projects = dc.Projects.ToList(),
Tasks = dc.Tasks.ToList(),
TimeSegment = dc.TimeSegments.Single(t => t.TimeSegmentID == TimeSegmentID)
});
}
In the edit action your doing the same thing as in the create action by building a new TimeSegmentView object and passing it to the view. The key difference here is that you're now populating the TimeSegment property. Your form would look something like this (shortened from above):
<form action="/<%=Model.TimeSegment.TimeSegmentID%>" method="post">
<p>
<label>Hours</label>
<input name="TimeSegment.Hours" value="<%=Model.TimeSegment.Hours%>" />
</p>
</form>
And your receiving action on the controller would look like this:
[HttpPost]
public RedirectToRouteResult EditTimeSegment(
int TimeSegmentID) {
TimeSegment TS = dc.TimeSegments.Single(t => t.TimeSegmentID == TimeSegmentID);
TryUpdateModel<TimeSegment>(TS, "TimeSegment", new string[5] {
"Hours", "Date", "ConsultantID", "ProjectID", "TaskID"
});
dc.SubmitChanges();
return RedirectToAction("EditTimeSegment", new {
TimeSegmentID = TimeSegment.TimeSegmentID
});
}
Lastly, if you want to display a list of TimeSegments you can do something like this:
[HttpGet]
public ActionResult ListTimeSegments() {
return View(new TimeSegmentsView {
TimeSegments = dc.TimeSegments.ToList()
});
}
And TimeSegmentsView looks like this:
public class TimeSegmentsView {
public IList<TimeSegment> TimeSegments { get; set; }
}
And in the View you'd want to do this:
<table>
<% foreach (TimeSegment TS in Model.TimeSegments) { %>
<tr>
<td><%=TS.Hours%></td>
<td><%=TS.Date%></td>
<td><%=TS.Project.ProjectName%></td>
<td><%=TS.Consultant.ConsultantName%></td>
<td><%=TS.Task.TaskName%></td>
</tr>
<% }; %>
</table>
I hope this is enough to give you a start. It's by no means complete, but its 5 AM and I haven't slept yet, so this will have to do for now (from me). Feel free to name your actions what you want, you don't have to stick to my naming conventions.
I would suggest however that you change the naming of the properties of your tables. For example when your writing the expressions like in the table above you'll have to do TS.Project.ProjectName and that's redundant. You're already accessing the Project property of TS through their relationship so you know you're only going to work with a Project. This then makes ProjectName a pointless blob of text re-describing the object your working with. Instead just use Name, and turn your expression to TS.Project.Name.
Anyway, just a suggestion, do what you like better. I'm passing out, so good night and happy Thanksgiving!
UPDATE
The process with collections is essentially the same as far as the controller side is conserned. It's the client side and the JavaScript that's more difficult to get going, so I'll assume that you have something established on that end.
So, here's how the controller would work. You pass in an array of TimeSegment and the model binder is smart enough to figure it out through the Prefix of your form elements.
<form action="/<%=Model.TimeSegment.TimeSegmentID%>" method="post">
<p>
<label>Hours</label>
<input name="TimeSegment[0].Hours" />
<!-- Notice the array in the prefix -->
</p>
<p>
<label>Hours</label>
<input name="TimeSegment[1].Hours" />
<!-- Notice the array in the prefix -->
</p>
</form>
And the controller:
[HttpPost]
public RedirectToRouteResult CreateTimeSegments(
[Bind(Prefix = "TimeSegment", Include = "Hours,Date,ConsultantID,ProjectID,TaskID")] TimeSegment[] TimeSegments) {
dc.TimeSegments.InsertAllOnSubmit(TimeSegments);
dc.SubmitChanges();
return RedirectToAction("ListTimeSegments");
}
And that's it. Of course you'll want to validate or do other stuff before sending to the database, but that's roughly all there is to it.
UPDATE 2
I believe you can do an IList<TimeSegment> instead of TimeSegment[] without issues, but as far as if it's better, that's up for debate. The way I look at it the browser still sends a virtual array to the server so having the action receive an array feels natural, but its up to you what you want to use.
So, a generic list action would look like this:
[HttpPost]
public RedirectToRouteResult CreateTimeSegments(
[Bind(Prefix = "TimeSegment", Include = "Hours,Date,ConsultantID,ProjectID,TaskID")] IList<TimeSegment> TimeSegments) {
dc.TimeSegments.InsertAllOnSubmit(TimeSegments);
dc.SubmitChanges();
return RedirectToAction("ListTimeSegments");
}
Keep in mind that I haven't used this (meaning the IList) before so I can't guarantee it will work, just speculating...
UPDATE 3
About what you want to do with the Consultant, it sound a lot like what I do with Cookies. I have a BaseView class which is the type used by the Site.Master and then all other views extend from it. In the BaseView I have a Cookie property which is always populated by each controller action. I then use that property to get the id of the currently authorized user.
So, in code it looks like this (using examples from one of my apps):
public class BaseView {
// Don't confuse with an HttpCookie, this is an object in my database...
public Cookie Cookie { get; set;}
}
public class EmployeeView : BaseView {
public Employee Employee { get; set; }
}
And with this, say I want to add a note to an employee, my form would look like this where I pass in a hidden field which is where your ConsultantID comes into play.
<form>
<input type="hidden" name="Note.AuthorId" value="<%=Model.Cookie.EmployeeId%>" />
<!-- other fields -->
</form>
Hope this helps.

Resources