ValueChanged and ValueEpression in blazor? - c#

I have seen the use of ValueChanged and ValueExpression in blazor component but does not have any idea wy they are used and what does ValueChanged and ValueEpression do in blazor?
If any body can explain them in simple language, it would be really a great help/

Please check the InputBase Class Properties:
ValueChanged: Gets or sets a callback that updates the bound value.
ValueExpression: Gets or sets an expression that identifies the bound value.
Generally, Razor components provide data binding features via an HTML element attribute named #bind with a field, property, or Razor expression value, we could use it to bind values to the html elements. If we are not using the bind attribute, we could use the ValueExpression and ValueChanged to update the bound value. Please refer to the following sample:
<EditForm Model="form">
<p>Current value: #form.MyProperty</p>
<InputSelect ValueChanged="#((string s) => DoThing(s))"
ValueExpression="#(()=> form.MyProperty)">
<option value="0">0</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="300">300</option>
</InputSelect>
</EditForm>
#code {
MyForm form = new MyForm(); //create an model instance.
void DoThing(string s) //value changed event.
{
form.MyProperty = s; //get the current selected value and assign to the Model.
}
//define a class to get/set the dropdownlist selected value.
public class MyForm
{
public string MyProperty { get; set; }
}
}
If using the #bind attribute, the sample code as below:
<p>Current Season: #model.Season</p>
<InputSelect #bind-Value="model.Season">
#foreach (var value in Enum.GetValues(typeof(Season)))
{
<option>#value</option>
}
</InputSelect>
#code {
Model model = new Model();
class Model
{
public Season Season { get; set; }
}
enum Season
{
Spring,
Summer,
Autumn,
Winter
}
}
The screenshot as below:
Reference:
ASP.NET Core Blazor data binding

Related

(Blazor) Select value not updating

I have a <select> element in my page, of which I want to get the value. To do that, I've added the #bind= property. Here's the code:
<form target="#">
<label for="subject">Vak</label>
<select #bind="#selectedSubject" id="subject">
#foreach (Subject subject in Auth.User.Subjects)
{
<option value="#subject.Uuid">#subject.Name</option>
}
</select><br />
<!-- (more input fields) -->
<button class="button btn-primary mt-3" #onclick="addNote">Toevoegen</button>
</form>
#code {
private String selectedSubject { get; set; }
private String inputTitle { get; set; }
private String inputContent { get; set; }
private void addNote()
{
Console.WriteLine(selectedSubject);
}
}
This doesn't output anything, and trying to use selectedSubject results in a NullReferenceError.
The other values (inputTitle, inputContent) work as expected.
Yes, the <select> is properly being filled with <option>s.
I've tried switching to whatever <EditForm> is, but it kept giving me warnings and failed to build.
First of all, you don't need a form at all. The main purpose of EditForm is to validate inputs-- like, making sure a password is the correct format.
Also, unless you want to programmatically SET the value of the dropdown, you don't need to bind to it-- instead, use #onchange:
<select #onchange=ProcessSelection>
. . .
#code {
Subject SelectedSubject {get; set;}
async Task ProcessSelection (ChangeEventArgs args){
SelectedSubject = Auth.User.Subjects.Single(s=>s.uuid = args.Value.ToString();
// Process the selection.
}
}
This will (1) give you a place to breakpoint for debugging. (2) Let you do useful things with the SelectedSubject object more easily (like add / remove it from a list, pass it as a parameter to a display component, etc.).
My approach to the problem is different from the other answer.
Here's everything in one Razor component.
Separate your data out into a class. Note that the fields can be null (you may not want them to be).
Use EditForm with normal binding. You can then do validation.
Logic in select list to determine if no value is selected and display a select message.
Separate out the Select code into a separate RenderFragment block to keep the code cleaner. Not necessary if you like all your markup in one place.
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
<EditForm EditContext="editContext">
<div class="col-12 mb-3">
<label class="form-label small muted">Subject</label>
<select class="form-select" #bind="model.Subject">
#this.SubjectList
</select>
</div>
<div class="col-12 mb-3">
<label class="form-label small muted">Title</label>
<input class="form-control" #bind="model.Title">
</div>
</EditForm>
<div class="alert alert-info m-3">
<div class="m-2">
Subject : #model.Subject
</div>
<div class="m-2">
Title : #model.Title
</div>
<div class="m-2">
Content : #model.Content
</div>
</div>
#code {
//private Note model = new() { Subject="Portugal"};
private Note model = new();
private EditContext? editContext;
protected override void OnInitialized()
=> this.editContext = new EditContext(model);
private IEnumerable<string> Subjects = new List<string> { "France", "Portugal", "Spain" };
private bool noSubjectSelected => !Subjects.Any(item => item == model.Subject);
// Separates out the code into a separate render block
private RenderFragment SubjectList => (__builder) =>
{
if (noSubjectSelected)
{
<option disabled selected value="">-- Select a Subject --</option>
}
foreach (var subject in Subjects)
{
<option value="#subject">#subject</option>
}
};
// Normally separate out into own class file
public class Note
{
public string? Subject { get; set; }
public string? Title { get; set; }
public string? Content { get; set; }
}
}
I've left the subject as a string as that's what you have it as. If it's a guid you will need to adjust the Select Code to handle a Guid/String pair.
Bear in mind that source code is an instruction of rendering intent so it isn't always clear how your code will react in all cases of operation.
You have to decide whether there will be a selection at all times, or whether no material selection is also valid. For example, if you want an option of "Select a Subject" at the top of the list which is the default, so set am option with that text an a value of Guid.Empty (all zeros). When processing values - just check that the selected Guid is not equal to Guid.Empty if you want to know if a "real" subject option is selected
I use the following two approaches with InputSelect. Note that the Microsoft renderer may set the text of the select to your bound option but may not set that option to "selected"
<InputSelect #bind-Value=MyData>
<option selected value="#Guid.Empty">Select a Subject</option>
#foreach (var i in Subjects)
{
<option value=#i.Id>#i.SubjectName</option>
}
</InputSelect>
If you want finer grained control over the select, there is this binding option. As well as a value to which you bind, you also need a function which is called on change - SelectChanged - where you can run other code. you should set the bound value within this method. If the InputSelect is inside a component, the bound value should also have an EventHandler of the same name with the "Changed" text appended - you should Invoke this in your SelectChanged function. Any parent control binding to the child will have its data automatically updated and the SetStateChanged is called for you
<InputSelect Value="#MyValue" ValueChanged="#((Guid value) => SelectChanged(value))" ValueExpression="#(() => MyValue)" >
#foreach (var i in MyValues)
{
if (TheSelectedValue == i.TheValue)
{
<option selected value=#i.TheValue>#i.ValueDisplay</option>
}
else
{
<option value=#i.TheValue>#i.ValueDisplay</option>
}
}
</InputSelect>
To solve your issue with things not changing, there are two ways you can do it:
Start with a null object for your SelectedSubject, do a null check in your click, and have a dummy select option that forces you to select an item:
.
<select #onchange=ProcessChange>
<option selected disabled>Pick a subject. . . </option>
#foreach (var subject in Auth.User.Subjects) {
<option>. . . </option>
}
</select>
<button #onclick=AddSubject />
#code {
List<Subject> SelectedSubjects = new();
Subject SelectedSubject {get; set; } = null;
async Task ProcessChange (ChangeEventArgs args){. . . }
async Task AddSubject{
if (Subject is not null) SelectedSubjects.Add (SelectedSubject);
}
}
Preselect your first option in your data initialization, and then you don't NEED the dummy <option> or the null check.
.
#code{
async Task OnInitializedAsync (bool IsFirstRender){
Auth.User.Subjects = await LoadSubjectsForUser (Auth.User);
SelectedSubject = Auth.User.Subjects.First();
}
}
In your use case, I think #1 is probably better.

Can I specify the default for a SelectListItem in the .cshtml?

I have a set of dropdown values defined in a global .cs file:
mydropdowns.cs
public class Foo_DropdownValues
{
public static List<SelectListItem> NoYes { get; } = new List<SelectListItem>
{
new SelectListItem { Value = "false", Text = "No"},
new SelectListItem { Value = "true", Text = "Yes" }
};
...
I'm using these SelectLists in different places in my .cshtml web form:
mypage.cshtml
<div class="form-group" id="PC_QU10_div">
<label>#Foo_Questions.QU10</label>
<select asp-for="Foo_Answers.QU10" asp-items="Foo_DropdownValues.NoYes"></select>
</div>
<div class="form-group" id="PC_QU20_div">
<label>#Foo_Questions.QU20</label>
<select asp-for="Foo_Answers.QU20" asp-items="Foo_DropdownValues.NoYes"></select>
...
I'd like to specify a default of "Yes" for the first item, and a default of "No" for the second?
Q: Is there any way for my to specify an explicit default in the .cshtml markup? On a per-item basis?
My app is written in C# in .Net Core 3.1, if it matters.
When a view has a model type specified, input controls for each of the properties of model object can be created using either the HTML Helper methods (#Html.TextBoxFor, #Html.CheckBoxFor, #Html.RadioButtonFor, #Html.DropdownListFor) etc. or using asp-for attributes in the input controls.
Both of these above mentioned ways, makes sure that the value entered/selected in the input control will be assigned to the model object properties when form is submitted and also the input controls are populated with the respective property values from the model object if the model object is being passed to the view from the server.
That's why, in this particular case, if the dropdown needs to have a specific value pre-selected (or by default selected), a value needs to be assigned to the dropdownlist. And it can be easily done by populating value in the property of the model object.
Consider example below:
Model class:
public class PersonData
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Gender { get; set; }
public bool IsMarried { get; set; }
public bool IsLeftHanded { get; set; }
}
Static list of items for dropdownlist.
public static class StaticValues
{
public static SelectListItem[] Genders
{
get
{
return new SelectListItem[]
{
new SelectListItem("Male", "Male"),
new SelectListItem("Female", "Female")
};
}
}
public static SelectListItem[] YesNoItems
{
get
{
return new SelectListItem[]
{
new SelectListItem("Yes", "True"),
new SelectListItem("No", "False")
};
}
}
}
View Code:
#model PersonData;
#{
ViewData["Title"] = "Person data";
}
<div>
<label asp-for="FirstName">Firstname:</label>
<input asp-for="FirstName"/>
</div>
<div>
<label asp-for="LastName">Lastname:</label>
<input asp-for="LastName" />
</div>
<div>
<label asp-for="Gender">Firstname:</label>
<select asp-for="Gender" asp-items="#StaticValues.Genders"></select>
</div>
<div>
<label asp-for="IsMarried">Married:</label>
<select asp-for="IsMarried" asp-items="#StaticValues.YesNoItems"></select>
</div>
<div>
<label asp-for="IsLeftHanded">Left handed:</label>
<select asp-for="IsLeftHanded" asp-items="#StaticValues.YesNoItems"></select>
</div>
In the following code from Controller Action method, model object is populated with a few properties with values assigned to them. And the model object is passed to the View.
public async Task<IActionResult> Index()
{
var personData = new PersonData();
// This assignment should populate the text box with John value
personData.FirstName = "John";
// Gender dropdown should have Male pre-selected
personData.Gender = "Male";
// IsMarried dropwodn should have "Yes" pre-selected.
personData.IsMarried = true;
return View(personData);
}
Following is the view rendered when application is run.
In a different use case, there can be a requirement where a specific value needs to be pre-selected by default when model property does not have value assigned to the property.
In such situations, specific SelectListItem should have Selected property set to true.
For example, in below list of SelectListItem New York has true passed as third argument in constructor. That will mark that item as selected be-default in the dropdownlist.
public static SelectListItem[] OfficeLocations
{
get
{
return new SelectListItem[]
{
new SelectListItem("London", "LON"),
new SelectListItem("New York", "NY", true),
new SelectListItem("Singapore", "SG")
};
}
}
Now I will add new property OfficeLocation to PersonData class as following.
public string OfficeLocation { get; set; }
And add following code to view.
<div>
<label asp-for="OfficeLocation">Office location:</label>
<select asp-for="OfficeLocation" asp-items="#StaticValues.OfficeLocations"></select>
</div>
Now if the model object does not have any value assigned to OfficeLocation property, the OfficeLocation dropdown will have New York selected by default.
The view will look as following.
I hope this will help you resolve your issue.
There are 2 options that I know of that can work depending on requirements:
1.) Add a disabled default option directly in the mark-up
<select ...>
<option disabled value="">Choose an option</option>
</select>
2.) Set the default selected value in your PageModel / Code behind before returning the view. If you want to continue to use a static reference for the common list, you can create a static method that takes a bool selected parameter instead of a readonly property:
var items = new List<SelectListItem>();
...
items.Insert(0, new SelectListItem("No", "No", selected: true));
//..insert other items

onChange event not firing Blazor InputSelect

I have the following Code for an InputSelect
<InputSelect class="form-control form-control form-control-sm"
placeholder="Role"
disabled="#IsReadOnly"
#bind-Value="Model.Role"
#onchange="#RoleChanged">
<option value="Member">Member</option>
<option value="Admin">Admin</option>
<option value="Pioner">Pioneer</option>
<option value="Retailer">Retailer</option>
</InputSelect>
And for the Code:
bool ShowCreated;
bool ShowUpdated;
bool IsReadOnly;
string SelectedRole;
public EditForm AccountForm;
public Multi.Dtos.RegistrationDto Model = new Dtos.RegistrationDto() { Role = "Member" };
public void RoleChanged(ChangeEventArgs e)
{
SelectedRole = e.Value.ToString();
Console.WriteLine(e.Value);
}
The RoleChange function is not invoked when i try to select a new item. Can someone point out what wrong with this. The value of the proper has changed but the Event was not called.
Note:
InputSelect is a component element, not HTML element, which is why you cannot
apply to it the #onchange compiler directive. This directive is applied to
elements, usually but not necessarily with the value attribute to form the so-
called two way data-binding.
#bind-Value is a compiler directive directive instructing the compiler to
produce code that enable two-way data binding between components. Applying
#bind-Value to the InputSelect component requires you (already done in this
case by the Blazor team) to define a parameter property named Value and an
EventCallback 'delegate', conventionally named ValueChanged. Value and
ValueChanged are properties of the InputSelect component.
There are a couple of ways to do this:
<InputSelect ValueExpression="#(()=>comment.Country)"
Value="#comment.Country"
ValueChanged="#((string value) => OnValueChanged(value ))">
<option value="">Select country...</option>
<option value="USA">USA</option>
<option value="Britain">Britain</option>
<option value="Germany">Germany</option>
<option value="Israel">Israel</option>
</InputSelect>
And
private Task OnValueChanged(string value)
{
// Assign the selected value to the Model
comment.Country = value;
return Task.CompletedTask;
}
You can also implement INotifyPropertyChanged.PropertyChanged Event in your Model, as dani herrera has suggested. You do that if you want to do away with the Forms components, including the EditForm, and use the normal HTML tags instead, in which case you'll be able to do something like:
<input type="text" value="#MyValue" #onchange=OnChange"/>
Of course your model class is going to be thick, and it should communicate with the EditContext....
Hope this helps...
The request is not that old, so you may consider the following which works with EditForm
(basically hook up onto another event) => at oninput eventhandler you pass The ChangeEventArgs var, and so you can get the value to process
<InputSelect #bind-Value="#labBook.StockID" #oninput="OnLabbookStockChange" class="form-control">
#if (lstStocks != null)
{
<option value="">select...</option>
#foreach (var stock in lstStocks)
{
<option value="#stock.StockID">#stock.TestArticle.TAName</option>
}
}
</InputSelect>
The problem is the #bind attribute makes use of the #onchange event and Blazor will not allow multiple #onchange event handlers. Workarounds in the code below:
Method 1: This is the vanilla example. It shows how to wire up a dropdown using an HTML select tag when you do not require an onchange event handler.
Method 2: This is, I think, the simplest if you need 2-way data binding AND an event handler. It uses the HTML select tag (not a Blazor component) with 1-way data binding using the "value" attribute. Since the #bind attribute is not used, we are free to attach a handler to the #onchange event. This handler, as well as handling the event, also needs to populate the property in order to achieve 2-way data binding.
Method 3: If you are using the whole Blazor EditForm and InputText/InputSelect/etc components infrastructure, this method may be best for you. Without the EditContext, the example shows 2-way binding using #bind-Value. In order to handle the onchange event for any component, we add an event handler (EditContext.OnFieldChanged) for the entire form. The handler checks to see which property was changed (based on the FieldName) and fires the required event handler accordingly.
I created this page to remind me of the options for this. Hope it helps anyone who finds their way here.
#page "/dropdown"
<h2 class='#(hightlight ? "bg-info" : "")'>
User Id: #UserId
</h2>
<hr />
<h3>Using Select Tag</h3>
<p>
<label>
Method 1 (2-way binding but do not need to handle an onchange event):<br />
<select #bind="UserId">
<option value=""></option>
#foreach (var u in users)
{
<option value="#u.Key">#u.Value</option>
}
</select>
</label>
</p>
<p>
<label>
Method 2 (need to handle the onchange event; 2-way binding via the control onchange event handler):<br />
<select value="#UserId" #onchange="OnChangeUserId">
<option value=""></option>
#foreach (var u in users)
{
<option value="#u.Key">#u.Value</option>
}
</select>
</label>
</p>
<h3>Using InputSelect Tag</h3>
<EditForm EditContext="EditContext">
<p>
<label>
Method 3 (2-way binding; event handling via the EditForm EditContext.OnFieldChanged)<br />
<InputSelect #bind-Value="#UserId">
<option value=""></option>
#foreach (var u in users)
{
<option value="#u.Key">#u.Value</option>
}
</InputSelect>
</label>
</p>
</EditForm>
#code {
private EditContext EditContext;
private Dictionary<int, string> users = new Dictionary<int, string>();
private int? UserId { get; set; }
private bool hightlight { get; set; }
protected override void OnInitialized()
{
EditContext = new EditContext(users);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
}
protected override void OnParametersSet()
{
// simulate getting current data from the db (which would use async version of this event handler)
users.Clear();
users.Add(1, "Bob");
users.Add(2, "Sue");
users.Add(3, "R2D2");
UserId = 2;
base.OnParametersSet();
}
private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e)
{
if (e.FieldIdentifier.FieldName == "UserId")
{
DoStuff();
}
}
private void OnChangeUserId(ChangeEventArgs args)
{
// select tag's "value" attribute provide one-way data binding; to get 2-way,
// we need to manually set the value of the UserId based on the ChangeEventArgs
if (args.Value == null)
UserId = null;
else
UserId = int.Parse(args.Value.ToString());
DoStuff();
}
private void DoStuff()
{
hightlight = (UserId == 3);
}
}
Instead of using the onchange event. You may want to attempt using oninput event instead.
May be late to the party, but I found the easiest thing to do is use the #bind-Value="MyProperty" and then for the property call the function in the setter. Therefore, no need to use #onchanged.
so in the c# model:
private T _myProperty;
public T MyProperty
{
get => _myProperty;
set =>
{
_myProperty = value;
MyFunction();
}
}
I solved this simply using the #onclick event. It fires multiple times, but the last time it fires, its the selected item.
<div>
<EditForm Model="model">
<InputSelect #bind-Value="model.SimulatorType" #onclick="SelectionChanged">
#foreach (var value in Enum.GetValues(typeof(SimulatorType)))
{
<option>#value</option>
}
</InputSelect>
</EditForm>
</div>
#code {
class Model
{
public SimulatorType SimulatorType
{
get;set;
}
}
Model model = new Model();
private async void SelectionChanged()
{
await OnSelectionChanged.InvokeAsync(model.SimulatorType.ToString());
}
[Parameter]
public EventCallback<string> OnSelectionChanged { get; set; }
}

MVC dropdownlist with dynamic custom parameter

I have a list of objects say Employee, and I am trying to get the dropdown list of all employee names with a custom parameter with their employee id. The Model is as follows
public class Emp
{
public string EmployeeName {get;set;}
public int employeeId {get;set;}
}
I am looking to create a Dropdownlist with HTML.DropdownlistFor as below
<select>
<option employeeid="101"> Emp1 <option>
<option employeeid="102"> Emp2 <option>
<option employeeid="103"> Emp3 <option>
</select>
Your HTML is invalid, a <select> tag does not have an attribute called employeeid. Since you're clearly trying to bind the employeeId to a the value of an option, you should use the classic name and value attributes which can be used as follows:
<select name="employeeid">
<option value="101">Gary</option>
<option value="102">John</option>
<!-- etc -->
</select>
Now in your controller action in your MVC project, you recieve a parameter called employeeid which is of the type int. The model binder (look it up) will automatically bind the value you selected in the 'employeeid' select input element when you submit the form.
The answer #daniell89 provided shows you a nice way of populating your select tag using the Razor template language pretty much everyone uses for c# MVC projects. Note how a ViewModel you use in your view does not have to be the same as the ViewModel you submit to the action you post to, so the public int SelectedEmployee { get; set; } is actually not necessary in the ViewModel, as long as you have the 'employeeid' as a parameter for the action you submit to.
EDIT
In order to use custom attributes you will kind of have to move away from classic MVC, because the post you will do will be done by an AJAX Post. You will need to import the jQuery library (actually you can use vanilla javascript or any library of your choosing, I prefer jQuery for this kind of stuff), look up how to import this if you need to.
Use a ViewModel in your code alike this:
public class Employee {
public string Name { get; set; }
public string EmployeeId { get; set; }
}
Then use this model in your view as such (first line of code of your view, unless you have using statements):
#model IEnumerable<Employee>
Then right at the place you want to have your select button create this code:
<select id="my-select-element">
#foreach(var employee in Model) {
<text>
<option employeeid="#employee.EmployeeId">#employee.Name</option>
</text>
}
</select>
<button id="submit-employee">Send!</button>
This will create your select tag with all the employees you've put in your viewmodel, together with a button you will use to send your data to your server.
Then include the following jQuery script on your page, that will collect your data and send it to the server.
<script>
$("#submit-employee").on("click", function(event) {
event.preventDefault(); //will prevent default behaviour done by clicking the button
var selecttag = $("#my-select-element");
var id = $(selecttag).find("option").prop("selected").attr("employeeid");
$.post("<your URL>", { employeeid: id }, function(result) {
console.log(result);
}
}
</script>
Then in your controller on your server code, have an action corresponding with and have it parameterised with an int emplyeeid as such:
[HttpPost]
public ActionResult SubmitEmployee (int employeeid) {
return new Json().Body(new { message = "I saw " + employeeid })
}
If all goes right, your console window in your browser will be showing you a returned object that has a 'message' key with a value 'I saw 101' if you selected 101 as the employee before sending it.
I typed all this code right in the stackoverflow editor, so I may have made some syntax errors or mistakes in the way types are called (Im not sure if you can just do new Json().Body() for instance, though I know you can do something similar for sure.
I hope this answers your question.
I think you need another class- EmpViewModel:
public class EmpViewModel
{
public int SelectedEmployee { get; set; }
public IEnumerable<Employee> EmployeeList { get; set; }
}
And then you can pass instance of this class to the view and create dropdown list like this:
#Html.DropDownListFor(m => m.SelectedEmployee, new SelectList(Model.EmployeeList, "employeeId", "EmployeeName"));

How to set selected value in Html.DropDownList? [duplicate]

I have tried this is RC1 and then upgraded to RC2 which did not resolve the issue.
// in my controller
ViewData["UserId"] = new SelectList(
users,
"UserId",
"DisplayName",
selectedUserId.Value); // this has a value
result: the SelectedValue property is set on the object
// in my view
<%=Html.DropDownList("UserId", (SelectList)ViewData["UserId"])%>
result: all expected options are rendered to the client, but the selected attribute is not set. The item in SelectedValue exists within the list, but the first item in the list is always defaulted to selected.
How should I be doing this?
Update
Thanks to John Feminella's reply I found out what the issue is. "UserId" is a property in the Model my View is strongly typed to. When Html.DropDownList("UserId" is changed to any other name but "UserId", the selected value is rendered correctly.
This results in the value not being bound to the model though.
This is how I fixed this problem:
I had the following:
Controller:
ViewData["DealerTypes"] = Helper.SetSelectedValue(listOfValues, selectedValue) ;
View
<%=Html.DropDownList("DealerTypes", ViewData["DealerTypes"] as SelectList)%>
Changed by the following:
View
<%=Html.DropDownList("DealerTypesDD", ViewData["DealerTypes"] as SelectList)%>
It appears that the DropDown must not have the same name has the ViewData name :S weird but it worked.
Try this:
public class Person {
public int Id { get; set; }
public string Name { get; set; }
}
And then:
var list = new[] {
new Person { Id = 1, Name = "Name1" },
new Person { Id = 2, Name = "Name2" },
new Person { Id = 3, Name = "Name3" }
};
var selectList = new SelectList(list, "Id", "Name", 2);
ViewData["People"] = selectList;
Html.DropDownList("PeopleClass", (SelectList)ViewData["People"])
With MVC RC2, I get:
<select id="PeopleClass" name="PeopleClass">
<option value="1">Name1</option>
<option selected="selected" value="2">Name2</option>
<option value="3">Name3</option>
</select>
You can still name the DropDown as "UserId" and still have model binding working correctly for you.
The only requirement for this to work is that the ViewData key that contains the SelectList does not have the same name as the Model property that you want to bind. In your specific case this would be:
// in my controller
ViewData["Users"] = new SelectList(
users,
"UserId",
"DisplayName",
selectedUserId.Value); // this has a value
// in my view
<%=Html.DropDownList("UserId", (SelectList)ViewData["Users"])%>
This will produce a select element that is named UserId, which has the same name as the UserId property in your model and therefore the model binder will set it with the value selected in the html's select element generated by the Html.DropDownList helper.
I'm not sure why that particular Html.DropDownList constructor won't select the value specified in the SelectList when you put the select list in the ViewData with a key equal to the property name. I suspect it has something to do with how the DropDownList helper is used in other scenarios, where the convention is that you do have a SelectList in the ViewData with the same name as the property in your model. This will work correctly:
// in my controller
ViewData["UserId"] = new SelectList(
users,
"UserId",
"DisplayName",
selectedUserId.Value); // this has a value
// in my view
<%=Html.DropDownList("UserId")%>
The code in the previous MVC 3 post does not work but it is a good start. I will fix it. I have tested this code and it works in MVC 3 Razor C# This code uses the ViewModel pattern to populate a property that returns a List<SelectListItem>.
The Model class
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
The ViewModel class
using System.Web.Mvc;
public class ProductListviewModel
{
public List<SelectListItem> Products { get; set; }
}
The Controller Method
public ViewResult List()
{
var productList = new List<SelectListItem>();
foreach (Product p in Products)
{
productList.Add(new SelectListItem
{
Value = p.ProductId.ToString(),
Text = "Product: " + p.Name + " " + p.Price.ToString(),
// To set the selected item use the following code
// Note: you should not set every item to selected
Selected = true
});
}
ProductListViewModel productListVM = new ProductListViewModeld();
productListVM.Products = productList;
return View(productListVM);
}
The view
#model MvcApp.ViewModels.ProductListViewModel
#using (Html.BeginForm())
{
#Html.DropDownList("Products", Model.Products)
}
The HTML output will be something like
<select id="Products" name="Products">
<option value="3">Product: Widget 10.00</option>
<option value="4">Product: Gadget 5.95</option>
</select>
depending on how you format the output. I hope this helps. The code does work.
If we don't think this is a bug the team should fix, at lease MSDN should improve the document. The confusing really comes from the poor document of this. In MSDN, it explains the parameters name as,
Type: System.String
The name of the form field to return.
This just means the final html it generates will use that parameter as the name of the select input. But, it actually means more than that.
I guess the designer assumes that user will use a view model to display the dropdownlist, also will use post back to the same view model. But in a lot cases, we don't really follow that assumption.
Use the example above,
public class Person {
public int Id { get; set; }
public string Name { get; set; }
}
If we follow the assumption,we should define a view model for this dropdownlist related view
public class PersonsSelectViewModel{
public string SelectedPersonId,
public List<SelectListItem> Persons;
}
Because when post back, only the selected value will post back, so it assume it should post back to the model's property SelectedPersonId, which means Html.DropDownList's first parameter name should be 'SelectedPersonId'. So, the designer thinks that when display the model view in the view, the model's property SelectedPersonId should hold the default value of that dropdown list. Even thought your List<SelectListItem> Persons already set the Selected flag to indicate which one is selected/default, the tml.DropDownList will actually ignore that and rebuild it's own IEnumerable<SelectListItem> and set the default/selected item based on the name.
Here is the code from asp.net mvc
private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata,
string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple,
IDictionary<string, object> htmlAttributes)
{
...
bool usedViewData = false;
// If we got a null selectList, try to use ViewData to get the list of items.
if (selectList == null)
{
selectList = htmlHelper.GetSelectData(name);
usedViewData = true;
}
object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));
// If we haven't already used ViewData to get the entire list of items then we need to
// use the ViewData-supplied value before using the parameter-supplied value.
if (defaultValue == null && !String.IsNullOrEmpty(name))
{
if (!usedViewData)
{
defaultValue = htmlHelper.ViewData.Eval(name);
}
else if (metadata != null)
{
defaultValue = metadata.Model;
}
}
if (defaultValue != null)
{
selectList = GetSelectListWithDefaultValue(selectList, defaultValue, allowMultiple);
}
...
return tagBuilder.ToMvcHtmlString(TagRenderMode.Normal);
}
So, the code actually went further, it not only try to look up the name in the model, but also in the viewdata, as soon as it finds one, it will rebuild the selectList and ignore your original Selected.
The problem is, in a lot of cases, we don't really use it that way. we just want to throw in a selectList with one/multiple item(s) Selected set true.
Of course the solution is simple, use a name that not in the model nor in the viewdata. When it can not find a match, it will use the original selectList and the original Selected will take affect.
But i still think mvc should improve it by add one more condition
if ((defaultValue != null) && (!selectList.Any(i=>i.Selected)))
{
selectList = GetSelectListWithDefaultValue(selectList, defaultValue, allowMultiple);
}
Because, if the original selectList has already had one Selected, why would you ignore that?
Just my thoughts.
This appears to be a bug in the SelectExtensions class as it will only check the ViewData rather than the model for the selected item. So the trick is to copy the selected item from the model into the ViewData collection under the name of the property.
This is taken from the answer I gave on the MVC forums, I also have a more complete answer in a blog post that uses Kazi's DropDownList attribute...
Given a model
public class ArticleType
{
public Guid Id { get; set; }
public string Description { get; set; }
}
public class Article
{
public Guid Id { get; set; }
public string Name { get; set; }
public ArticleType { get; set; }
}
and a basic view model of
public class ArticleModel
{
public Guid Id { get; set; }
public string Name { get; set; }
[UIHint("DropDownList")]
public Guid ArticleType { get; set; }
}
Then we write a DropDownList editor template as follows..
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<script runat="server">
IEnumerable<SelectListItem> GetSelectList()
{
var metaData = ViewData.ModelMetadata;
if (metaData == null)
{
return null;
}
var selected = Model is SelectListItem ? ((SelectListItem) Model).Value : Model.ToString();
ViewData[metaData.PropertyName] = selected;
var key = metaData.PropertyName + "List";
return (IEnumerable<SelectListItem>)ViewData[key];
}
</script>
<%= Html.DropDownList(null, GetSelectList()) %>
This will also work if you change ArticleType in the view model to a SelectListItem, though you do have to implement a type converter as per Kazi's blog and register it to force the binder to treat this as a simple type.
In your controller we then have...
public ArticleController
{
...
public ActionResult Edit(int id)
{
var entity = repository.FindOne<Article>(id);
var model = builder.Convert<ArticleModel>(entity);
var types = repository.FindAll<ArticleTypes>();
ViewData["ArticleTypeList"] = builder.Convert<SelectListItem>(types);
return VIew(model);
}
...
}
The problems is that dropboxes don't work the same as listboxes, at least the way ASP.NET MVC2 design expects: A dropbox allows only zero or one values, as listboxes can have a multiple value selection. So, being strict with HTML, that value shouldn't be in the option list as "selected" flag, but in the input itself.
See the following example:
<select id="combo" name="combo" value="id2">
<option value="id1">This is option 1</option>
<option value="id2" selected="selected">This is option 2</option>
<option value="id3">This is option 3</option>
</select>
<select id="listbox" name="listbox" multiple>
<option value="id1">This is option 1</option>
<option value="id2" selected="selected">This is option 2</option>
<option value="id3">This is option 3</option>
</select>
The combo has the option selected, but also has its value attribute set. So, if you want ASP.NET MVC2 to render a dropbox and also have a specific value selected (i.e., default values, etc.), you should give it a value in the rendering, like this:
// in my view
<%=Html.DropDownList("UserId", selectListItems /* (SelectList)ViewData["UserId"]*/, new { #Value = selectedUser.Id } /* Your selected value as an additional HTML attribute */)%>
In ASP.NET MVC 3 you can simply add your list to ViewData...
var options = new List<SelectListItem>();
options.Add(new SelectListItem { Value = "1", Text = "1" });
options.Add(new SelectListItem { Value = "2", Text = "2" });
options.Add(new SelectListItem { Value = "3", Text = "3", Selected = true });
ViewData["options"] = options;
...and then reference it by name in your razor view...
#Html.DropDownList("options")
You don't have to manually "use" the list in the DropDownList call. Doing it this way correctly set the selected value for me too.
Disclaimer:
Haven't tried this with the web forms view engine, but it should work too.
I haven't tested this in the v1 and v2, but it might work.
I managed to get the desired result, but with a slightly different approach. In the Dropdownlist i used the Model and then referenced it. Not sure if this was what you were looking for.
#Html.DropDownList("Example", new SelectList(Model.FeeStructures, "Id", "NameOfFeeStructure", Model.Matters.FeeStructures))
Model.Matters.FeeStructures in above is my id, which could be your value of the item that should be selected.

Categories

Resources