Blazor Dropdown does not keep bound value when options change - drop-down-menu

I have the below code in a new Blazor Server project, just a single page with this on.
#page "/"
#using BlazorApp1.Data;
<select #bind="selectedValue1">
#foreach (DropdownOption option in notValue(0))
{
<option value="#option.value" aria-label="#option"> #option.displayName </option>
}
</select>
<select #bind="selectedValue2">
#foreach (DropdownOption option in notValue(1))
{
<option value="#option.value" aria-label="#option"> #option.displayName </option>
}
</select>
<select #bind="selectedValue3">
#foreach (DropdownOption option in notValue(2))
{
<option value="#option.value" aria-label="#option"> #option.displayName </option>
}
</select>
#code {
List<DropdownOption> test = new List<DropdownOption>()
{
new("BOB", "BOB"),
new("GEOFF", "GEOFF"),
new("GREGROY", "GREGORY"),
new("GERTRUDE", "GERTRUDE"),
new("NameyMcNameFace", "NameyMcNameFace"),
new("GILES", "GILES")
};
List<DropdownOption> notValue(int index) => GetValues(index);
string selectedValue1;
string selectedValue2;
string selectedValue3;
private List<DropdownOption> GetValues(int index)
{
var selected = new List<string>() { selectedValue1, selectedValue2, selectedValue3 };
selected = selected.Select((x, i) => i == index ? "" : x).ToList();
return test.Where(x => !selected.Contains(x.value)).ToList();
}
//public readonly record struct DropdownOption(string value, string displayName, string? group = null)
//{
//}
}
The commented out bit of code is a helper to manage dropdowns and keep it type safe and is simply in another file.
Behaviour happens if you the following steps.
Select dropdowns to
Bob
Gertrude
Giles
Then change the third dropdown from Giles to Geoff and the second dropdown changes value even though it is bound and Gertrude is still an option in the second dropdown.
Is anyone able to explain this behaviour?
Scrrenshot showing behaviour (do not change second dropdown)
See how second dropdown value chnges without being changes manually and it still binds to correct string in backend

Your problem is in the way you are using indexes. Take the first select. You select a value say Gregory - that's index 2 in the list provided. You then select Geoff in the second select. The first select hasn't changed, so it doesn't update, yet you've removed index 1 from it's list. Gregory is now at index 1, but the select thinks the selected item is index 2. You'll find the underlying value is correct, but not the displayed value.
Your second problem is that by default Bob is selected, but the underlying value isn't set. You need to use something like the construct I show in the component to show an unselectable message.
Hope you don't mind, but I've taken the liberty to redesign what you have to iron out these issues.
First a component for your select to encapsulate the functionality. I've added a -- Select a Person -- option that only shows when nothing is selected.
<select class="form-select mb-3" value="#Value" #onchange="(e) => SetValue(e, this.Index)">
#if (this.Selected is null)
{
<option value="" selected disabled aria-label="not selected"> -- Select a Person -- </option>
}
#foreach (DropdownOption option in Items )
{
<option #key="option" value="#option.displayName" aria-label="#option.displayName"> #option.displayName </option>
}
</select>
#code {
private string? Value => Selected?.displayName ?? null;
[Parameter,EditorRequired] public DropdownOption? Selected { get; set; }
[Parameter, EditorRequired] public int Index { get; set; }
[Parameter, EditorRequired] public EventCallback<Tuple<string?, int>> ValueChanged { get; set; }
[Parameter, EditorRequired] public IEnumerable<DropdownOption> Items { get; set; } = Enumerable.Empty<DropdownOption>();
private Task SetValue(ChangeEventArgs e, int index)
{
var value = e.Value?.ToString() ?? null;
this.ValueChanged.InvokeAsync(new Tuple<string?, int>(value, index));
return Task.CompletedTask;
}
}
And then your page:
#page "/"
<MySelect Index="1" Selected="selectedValue1" Items="GetValues(1)" ValueChanged="this.OnValueChanged" />
<MySelect Index="2" Selected="selectedValue2" Items="GetValues(2)" ValueChanged="this.OnValueChanged" />
<MySelect Index="3" Selected="selectedValue3" Items="GetValues(3)" ValueChanged="this.OnValueChanged" />
#code {
private DropdownOption? selectedValue1;
private DropdownOption? selectedValue2;
private DropdownOption? selectedValue3;
private void OnValueChanged(Tuple<string?, int> tuple)
{
var value = tuple.Item1;
var index = tuple.Item2;
if (value is null)
return;
if (index == 1)
selectedValue1 = test.SingleOrDefault(item => item.displayName.Equals(value));
if (index == 2)
selectedValue2 = test.SingleOrDefault(item => item.displayName.Equals(value));
if (index == 3)
selectedValue3 = test.SingleOrDefault(item => item.displayName.Equals(value));
}
private IEnumerable<DropdownOption> GetValues(int index)
{
var selected = new List<DropdownOption>();
if (selectedValue1 is not null && index != 1)
selected.Add((DropdownOption)selectedValue1);
if (selectedValue2 is not null && index != 2)
selected.Add((DropdownOption)selectedValue2);
if (selectedValue3 is not null && index != 3)
selected.Add((DropdownOption)selectedValue3);
return test.Except(selected);
}
private List<DropdownOption> test = new List<DropdownOption>()
{
new("BOB", "BOB"),
new("GEOFF", "GEOFF"),
new("GREGROY", "GREGORY"),
new("GERTRUDE", "GERTRUDE"),
new("NameyMcNameFace", "NameyMcNameFace"),
new("GILES", "GILES")
};
}

Related

Populating Umbraco Contour forms from using cookie data

We're currently using Umbraco version 7.1.4 assembly: 1.0.5261.28127 with Contour version 3.0.26
I'm trying to populate a contour form with information pulled from a database, but dependent on a user cookie (the cookie hold the primary key for the record in the database).
To implement this I'm looking at writing a custom field type (well a bunch of them, one for each data field) which examines the cookie makes the db request and then populates the textbox with the value (users name/address/etc).
I've managed to add custom setting to a control and have it display the value that's populated at design time, but I can't seem to amend that value at run time.
I'm happy to post the code if relevant, but my question is. Am I barking up the wrong tree? is this the best way to handle this or would it even work?
Any pointers would be most welcome
Thanks
EDIT
Thanks Tim,
I've now managed to break it in such a way it's not even rendering the controls (the debug message is saying the SVT value doesn't exist).
This just (or should) just populate the form with the current date/time just to get something working.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Umbraco.Forms.Core;
using System.Web.UI.WebControls;
namespace Custom.FieldType
{
public class CustomTextfield : Umbraco.Forms.Core.FieldType
{
public CustomTextfield()
{
//Provider
this.Id = new Guid("b994bc8b-2c65-461d-bfba-43c4b3bd2915");
this.Name = "Custom Textfield";
this.Description = "Renders a html input fieldKey"; //FieldType
this.Icon = "textfield.png";
this.SVT = DateTime.Now.ToLongTimeString();
}
public System.Web.UI.WebControls.TextBox tb;
public List<Object> _value;
[Umbraco.Forms.Core.Attributes.Setting("SVT", description = "the SVT")]
public string SVT { get; set; }
public override WebControl Editor
{
get
{
tb.TextMode = System.Web.UI.WebControls.TextBoxMode.SingleLine;
tb.CssClass = "text gaudete";
if (_value.Count > 0)
tb.Text = _value[0].ToString();
SVT = DateTime.Now.ToLongTimeString();
tb.Text = tb.Text + SVT;
return tb;
}
set { base.Editor = value; }
}
public override List<Object> Values
{
get
{
if (tb.Text != "")
{
_value.Clear();
_value.Add(tb.Text);
}
return _value;
}
set { _value = value; }
}
public override string RenderPreview()
{
return
"<input type=\"text\" id=\"text-content\" class=\"text\" maxlength=\"500\" value=\"" + this.SVT + "\" />";
}
public override string RenderPreviewWithPrevalues(List<object> prevalues)
{
return RenderPreview();
}
public override bool SupportsRegex
{
get { return true; }
}
}
}
And the view is
#model Umbraco.Forms.Mvc.Models.FieldViewModel
#{
var widthSetting = Model.AdditionalSettings.FirstOrDefault(s => s.Key.Equals("Width"));
string width = (widthSetting == null) ? null : widthSetting.Value;
var textSetting = Model.AdditionalSettings.FirstOrDefault(s => s.Key.Equals("SVT"));
string widthTXT = (textSetting == null) ? null : textSetting.Value;
}
<input type="text" name="#Model.Name" id="#Model.Id" class="text" maxlength="500"
value="#{if(!string.IsNullOrEmpty(widthTXT)){<text>#(SVT)</text>}}"
#{if(Model.Mandatory || Model.Validate){<text>data-val="true"</text>}}
#{if (Model.Mandatory) {<text> data-val-required="#Model.RequiredErrorMessage"</text>}}
#{if (Model.Validate) {<text> data-val-regex="#Model.InvalidErrorMessage" data-regex="#Model.Regex"</text>}}
/>
The code is mostly cobbled together from online tutorials which is why the naming is abysmal but if I can get something to populate the text box on the clients side then I can start the process of refactoring (well scrapping this demo version and writing a real version)
Thanks.
EDIT2
I was able to fix the error stopping the view loading thanks to the pointer from Tim, the new view looks as follows
#model Umbraco.Forms.Mvc.Models.FieldViewModel
#{
var textSetting = Model.AdditionalSettings.FirstOrDefault(s => s.Key.Equals("SVT"));
string widthTXT = (textSetting == null) ? null : textSetting.Value;
}
<input type="text" name="#Model.Name" id="#Model.Id" class="text" maxlength="500"
value="#{if(!string.IsNullOrEmpty(widthTXT)){<text>#(widthTXT)</text>}else{<text>Unknown</text>}}"
#{if(Model.Mandatory || Model.Validate){<text>data-val="true"</text>}}
#{if (Model.Mandatory) {<text> data-val-required="#Model.RequiredErrorMessage"</text>}}
#{if (Model.Validate) {<text> data-val-regex="#Model.InvalidErrorMessage" data-regex="#Model.Regex"</text>}}
/>
And just displays "Unknown" in the text box
thanks again.

MVC 3 #Html.DropDownListFor model binding fails

I am using VS 2010 with MVC 3 and EF 5. I am using a common pattern for dropdown lists that works correctly in all but one instance and I cannot see why this one is failing to select the correct entry in the select list. The following are code snippets.
The select list is created as follows:
public static IEnumerable<SelectListItem> GetOutcomes()
{
CodesEntities dataContextCodes = new CodesEntities(ConnectionString);
return new SelectList(dataContextCodes.CodeOutcome.
Where(x => x.DisplayOrder > 0).OrderBy(x => x.DisplayOrder),
"OutcomeCodeID", "Outcome");
}
This returns the correct select list.
The view has the following code:
#Html.DropDownListFor(m => m.OutcomeCodeID,
PerintalFormViewModels.GetOutcomes(), "Please select an item")
The model value m.OutcomeCodeID has a valid value (1) but no item is being selected.
The generated HTML is:
<select id="CodeID" name="OutcomeCodeID" data-val-required="Outcome is required" data-val-number="The field outcome must be a number." data-val="true">
<option value="">Please select an item</option>
<option value="1">Termination</option>
<option value="2">Loss</option>
<option value="3">Still</option>
<option value="4">Live</option>
</select>
I am in the hair tearing out, being driven nuts stage. Does anyone have any thoughts?
Thanks
You can do it like this:
public static IEnumerable<SelectListItem> GetOutcomes(string selectedID)
{
CodesEntities dataContextCodes = new CodesEntities(ConnectionString);
return new SelectList(dataContextCodes.CodeOutcome.
Where(x => x.DisplayOrder > 0).OrderBy(x => x.DisplayOrder),
"OutcomeCodeID", "Outcome"
, selectedID); // add this parameter
}
Or this:
public static IEnumerable<SelectListItem> GetOutcomes(string selectedID)
{
CodesEntities dataContextCodes = new CodesEntities(ConnectionString);
return
dataContextCodes.CodeOutCome
.Where(x => x.DisplayOrder > 0)
.OrderBy(x => x.DisplayOrder)
.ToList()
.Select(x => new SelectListItem
{
Value = x.OutcomeCodeID.ToString(),
Text = x.Outcome.ToString(),
Selected = x.OutcomeCodeID == selectedID
});
}
Then call it like this:
#Html.DropDownListFor(m => m.OutcomeCodeID,
PerintalFormViewModels.GetOutcomes(Model.OutcomeCodeID),
"Please select an item")
You are not setting the default selected value anywhere.
The particular constructor you are using for the SelectList is this one:
public SelectList(
IEnumerable items,
string dataValueField,
string dataTextField
)
Which does not set the default value. Either you use this one:
public SelectList(
IEnumerable items,
string dataValueField,
string dataTextField,
Object selectedValue
)
And specify the selectedValue or you manually set the SelectedListItem's Selected property to true on the item you want selected (http://msdn.microsoft.com/en-us/library/system.web.mvc.selectlistitem(v=vs.108).aspx).
It's a shameless plug but still I think it's a good resource: http://blinkingcaret.wordpress.com/2012/08/11/using-html-dropdownlistfor/

MVC3 DropDownListFor not populating selectedValue

I feel like I'm taking crazy pills. I have a dropdownlist for a view that reads from our database all of the wine producers we have. I want to set the selectedValue to a particular ID driven by the referring page. I can see it picks up the selectedValue in debug, I see the selected value populated (906 for this example), but it doesn't set the dropdownlist to the correct value when the page is rendered, it always defaults to 1 for the default value. I've tried creating the selectList in razor as opposed to my controller, but nothing works. Any help on this would be appreciated, I'm guessing it is something small.
Controller:
if (User.IsInRole("admin"))
{
if (ID != 0)
{
ViewBag.ProducerSelect = new SelectList(db.Producers.OrderBy(p => p.Name), "ProducerID", "Name", ID);
}
else
{
ViewBag.ProducerSelect = new SelectList(db.Producers.OrderBy(p => p.Name), "ProducerID", "Name");
}
}
View:
if (User.IsInRole("producereditor"))
{
<h3>#ViewBag.ProducerName</h3>
}
else
{
<div class="editor-label">
#Html.LabelFor(m => m.Wine.ProducerID, "Producer")
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.Wine.ProducerID, ViewBag.ProducerSelect as SelectList)
</div>
}
Tried the below but no success:
ViewBag.ProducerSelect = new SelectList(from p in db.Producers
orderby p.Name
select new { p.ProducerID, p.Name }
, "ProducerID", "Name", ID);
If you want to preselect an item, You set that value to your ProducerId property.
var yourViewModelObj=new YourViewModel;
yourViewModelObj.Wine.ProducerId=906; //or whatever value you want
return View(yourViewModelObj);
Suggestion : For better code readablity/Maintenance, Try to avoid ViewBag / ViewData and use a ViewModel to pass the data.
I would add a Property to my ViewModel to hold the Collection of Producers
public class WineViewModel
{
//Other Existing properties also
public IEnumerable<SelectListItem> Producers{ get; set; }
public string SelectedProducer { get; set; }
}
Then in yout GetAction method, you can set the value like this, If you want to set one select option as the default selected one.
public ActionResult CreateWine()
{
var vm=new WineViewModel();
//The below code is hardcoded for demo. you mat replace with DB data.
vm.Producers= new[]
{
new SelectListItem { Value = "1", Text = "Prodcer A" },
new SelectListItem { Value = "2", Text = "Prodcer B" },
new SelectListItem { Value = "3", Text = "Prodcer C" }
};
//Now let's set the default one's value
vm.SelectedProducer = "2";
return View(vm);
}
And in your Strongly typed View,
#Html.DropDownListFor(x => x.SelectedProducer,
new SelectList(Model.Producers, "Value", "Text"), "Select Producer")
The HTML Markup generated by above code will have the HTML select with the option with value 2 as selected one.
I figured this out. I had ViewModel.wine.ProducerID elsewhere on the page in a hidden field, and that defaults to 1, so I just assigned that to passed in value, and it worked great. I knew it was something like that. Thanks!
User a ViewModel ex WineViewModel
public class WineViewModel
{
public Wine Wine { get; set; }
public SelectList PProducerList { get; set; }
public WineViewModel() { }
public WineViewModel(Wine wine)
{
this.Wine = wine;
}
}
Try the following in your controller
var model = new WineViewModel( selectwine);
model.ProjectTypeList = new SelectList( from p in db.Producers
orderby p.Name
select new { p.ID, p.Name }, "ID", "Name")
notice how I am exclusively declaring which is the ID and which is the Value in my SelectList
Then in your view do
#Html.DropDownListFor(model => model.Wine.ProducerID, Model.ProjectTypeList)

MVC-3 DropdownList or DropdownListFor - Can't save values in controller POST

I searches for hours (or days) and didn't find a solution yet. I want to edit a customer with a DropdownListFor for the salutation with the right preselected value.
I've got 3 entities (Database first concept, this is not my own design...): customer, address, salutation
A CUSTOMER has an address_id (f_key) and an ADDRESS has got a salutation_id (f_key). The ADDRESS holds the first and last name for example. Inside the SALUTATION entity there is a column sal1 which holds all possible salutations.
Now, I want to edit my customer via a ViewModel which looks like this:
public class CustomerViewModel
{
public CUSTOMER cust { get; set; }
public SelectList salutationList { get; set; }
CustomerRepository repository = new CustomerRepository();
public CustomerViewModel(int id)
{
cust = repository.GetCustomerByIdAsQueryable(id).Single();
salutationList = new SelectList(repository.GetSalutations(), cust.ADDRESS.SALUTATION.SAL1);
}
// Some more
}
The CutsomerRepository methods:
public class CustomerRepository
{
private MyEntities db = new MyEntities();
public IQueryable<CUSTOMER> GetCustomerByIdAsQueryable(int id) {...}
public IQueryable<CUSTOMER> GetCustomersByName(string firstName, string lastName, int maxCount) {...}
public List<string> GetSalutations()
{
var salutationList = new List<string>();
var salutationListQry = from s in db.SALUTATION
select s.SAL1;
salutationListTemp.AddRange(salutationListQry);
return salutationList;
}
// ...
}
This is my controller method:
[HttpPost]
public ActionResult CustomerData(int id, FormCollection fc)
{
var vm = new CustomerViewModel(id);
// Why do I need the following line?
vm.cust = repository.GetCustomerByIdAsQueryable(id).Single();
try
{
UpdateModel(vm, fc);
repository.Save();
return View("CustomerData", vm);
}
catch (Exception e)
{
return View();
}
}
And finally the part from my View:
#model WebCRM.ViewModels.CustomerViewModel
#using (Html.BeginForm())
{
// ...
<div class="editor-label">
#Html.Label("Salutation:")
#Html.DropDownListFor(model => model.cust.ADDRESS.SALUTATION.SAL1, Model.salutationList)
// #Html.DropDownList("Salutation", Model.salutationList)
</div>
<div class="editor-label">
</div>
<div class="editor-field">
#Html.Label("Last name:")
#Html.EditorFor(model => model.cust.ADDRESS.LASTNAME)
#Html.ValidationMessageFor(model => model.cust.ADDRESS.LASTNAME)
</div>
<p>
<input type="submit" value="Speichern" />
</p>
}
Changing and saving last names works fine. But when saving the salutation it changes the SAL1 value in the SALUTATION entity to the one I've chosen in the DropdownListFor. What I want is to change the salutation_id inside the ADDRESS entity for my customer. Why isn't that working?
Another strange behavoior: When removing the marked line in my CustomerController, I can't even change and save the last name. Normally the constructor of the CustimerViewModel sets the customer. So why do I have to have the line of code for setting the customer inside my ViewModel? It's duplicated, but has to be there...
Thanks in advance.
You need to have Selected property in your list.
I can show you my working example:
public static IEnumerable<SelectListItem> GetCountries(short? selectedValue)
{
List<SelectListItem> _countries = new List<SelectListItem>();
_countries.Add(new SelectListItem() { Text = "Select country...", Value = "0", Selected = selectedValue == 0 });
foreach (var country in ObjectFactory.GetInstance<DataRepository>().GetCountries())
{
_countries.Add(new SelectListItem()
{
Text = country.Name,
Value = country.ID.ToString(),
Selected = selectedValue > 0 && selectedValue.Equals(country.ID)
});
}
return _countries;
}
In controller i store this into viewbag:
ViewBag.Countries = CompanyModel.GetCountries(0);
In view:
#Html.DropDownListFor(m => m.CompanyModel.CountryId, (IEnumerable<SelectListItem>)ViewBag.Countries)

Spark-like syntax in Razor foreach

We've been using Spark view engine for a while now in our application. Since the release of Resharper 6 with its great Razor support, we've caved and started to swap over.
One of the best parts of Spark was its automatic generation of variables inside a foreach loop. For instance, inside a loop you automatically get index, last, and first variables, which let you know the index, whether the item is the first item, and whether the item is the last item respectively.
My question is as follows: is there a way to have a foreach automatically generate these variables for me, so I don't have to do it manually? Do I need to create my own helper that does this?
As BuildStarted suggested, I used Phil Haack's post to formulate a solution.
public static HelperResult Each<TItem>(this IEnumerable<TItem> items, Func<EnumeratedItem<TItem>, HelperResult> template)
{
return new HelperResult(writer =>
{
int index = 0;
ICollection<TItem> list = items.ToList();
foreach (var item in list)
{
var result = template(new EnumeratedItem<TItem>(index, index == 0, index == list.Count - 1, item));
index++;
result.WriteTo(writer);
}
});
}
And the EnumeratedItem class:
public class EnumeratedItem<TModel>
{
public EnumeratedItem(int index, bool isFirst, bool isLast, TModel item)
{
this.Index = index;
this.IsFirst = isFirst;
this.IsLast = isLast;
this.Item = item;
}
public int Index { get; private set; }
public bool IsFirst { get; private set; }
public bool IsLast { get; private set; }
public TModel Item { get; private set; }
}
I was forced to convert the IEnumerable to an ICollection in order to have the Count property.
Usage:
#Model.History.Each(
#<text>
<li class="#(#item.IsLast ? "last" : string.Empty)">
#{ var versionNo = Model.History.Count - #item.Index; }
<div class="clearfix version selected">
<div class="index">
#versionNo
</div>
<div class="modified">
#item.Item.DateModified<br/>
#Html.Raw(item.Item.ModifiedBy)
</div>
<div class="view">
View changes
</div>
</div>
</li>
</text>)
Phil Haack wrote a nice example that will give you the items you're looking for here http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx
It's not the same as foreach however you'll be required to do something similar if you want the features you're looking for.
HugoWare also had a great article for IEnumerable extensions that provides the entire functionality you're looking for http://hugoware.net/blog/build-a-smarter-loop-with-c
I would probably recommend using Hugoware's example as it's more than just razor.
in Visual Studio 2010 there are "code snippets" - code parts, which allow to generate code. with some utilityes you can edit default snippet for "foreach" and write own code, with variables you need.

Resources