1

NOTE: I added working examples at the end of this post. In my FireFox, they demonstrate the problem. CAN ANYONE TAKE MY EXAMPLES AND REPRO IN YOUR FIREFOX. I have no way of knowing if this is a work problem or not.

All I want is for my Blazor solution to work in Edge and FireFox, but the latter is not working as expected with a select list.

The issue I have has been talked about again and again 4, 10 and 15 years ago yet the solutions discussed are not solving the problem nor are some other Blazor ideas. You can see some of the remnants of the attempted fixes are still in my html, such as selectedIndex = "-1" and everything has a unique name.

The below picture is the initial state of the select in the rendered display and the browser's dev tools.

Edge initial state

The issue happens when I pick the third or fourth option - FireFox flips it back to the second option. In Edge it sticks to the option I picked.

Blazor generates the select using a basic loop technique. (I've also tried setting the selectedIndex using JSRuntime).

<select name="@QuestionItem.Id" style="width: 218px; height:34px;" TValue="Answer" @onchange="SelectChanged">
    <option disabled="" selected="">Select Answer</option>
    @foreach (var a in QuestionItem.Answers)
    {
        // maybe https://stackoverflow.com/questions/60599131/blazor-select-dropdown-set-active-value-by-code
        @if (a.IsDisqualified)
        {
            <option disabled="disabled" value="@a.Id">@a.Name</option>
        }
        else
        {
            @if (a.IsSelected)
            {
                <option name="@a.Id" value="@a.Id" selected="selected">@a.Name</option>
            }
            else
            {
                <option name="@a.Id" value="@a.Id">@a.Name</option>
            }
        }
    }
</select>

The objects that drive the loop have an IsSelected property that has Blazor write out an <option selected="selected"> element. The @onchange event commits the newly picked option to the database and runs a bunch of biz rules.

Now, I'll demonstrate the problem.

Using Edge, I pick the "Full Installation" option and it behaves fine - the select renders "Full Installation" and the html properly has selected="selected" on the same option.

Edge consistent state

Now I repeat those steps with FireFox. Here again is the default state.

FireFox initial state

Once again I choose "Full Installation" but now we get an inconsistent state - the select flips to "No" but the html shows "Full Installation" is selected.

FireFox inconsistent state

Regardless, the correct value is written to the database so upon refresh FireFox then shows the correct selected option. It seems inconceivable that web developers have put up with this or that Mozilla has left the problem languishing all this time. So what am I doing wrong?

The version of FireFox I have is 78.11.0esr (64-bit).

Here is a simple example that reproduces the problem.

@page "/Select"
<h3>Select</h3>

@if (SelectData != null)
{
<div class="form-group col-md-6">
    <label for="dur">Duration</label>
    <select class="custom-select" @onchange="this.SelectChanged">
        <option disabled value="-1" selected>Select an Option</option>
        @foreach (var duration in SelectData)
        {
            @if (duration.IsSelected)
            {
            <option value="@duration.Name" selected>@duration.Name</option>
            }
            else
            {
            <option value="@duration.Name">@duration.Name</option>
            }
        }
    </select>
</div>
}
else
{
    <label>Loading</label>
}

<div class="p-2 m-2">
    Current Value : @_Duration.Name
</div>
<div class="p-2 m-2">
    IsSelected : @_Duration.IsSelected
</div>


@code {
    private class Duration
    {
        public string Name { get; set; }
        public bool IsSelected { get; set; }
    }

    private Duration _Duration = new Duration();


    private void SelectChanged(ChangeEventArgs e)
    {
        var selected = SelectData.SingleOrDefault(t => t.Name == (string)e.Value);

        if (selected != null)
        {
            _Duration = selected;
            _Duration.IsSelected = true;
        }
    }

    private List<Duration> SelectData = new List<Duration>
{
        new Duration{ Name = "Hello1", IsSelected = false},
        new Duration{ Name = "Hello2", IsSelected = false},
        new Duration{ Name = "Hello3", IsSelected = false},
    };
}

Here is a slightly different example with the same FF problem

@page "/Select"
<h3>Select</h3>

@if (SelectData != null)
{
<div class="form-group col-md-6">
    <label for="dur">Duration</label>
    <select class="custom-select" @onchange="this.SelectChanged">
        <option disabled selected>Select an Option</option>
        @foreach (var duration in SelectData)
        {
            @if (_Duration == duration)
            {
            <option value="@duration.Name" selected>@duration.Name</option>
            }
            else
            {
            <option value="@duration.Name">@duration.Name</option>
            }
        }
    </select>
</div>
}
else
{
    <label>Loading</label>
}

<div class="p-2 m-2">
    Current Value : @_Duration.Name
</div>
<div class="p-2 m-2">
    IsSelected : @_Duration.IsSelected
</div>


@code {
    private class Duration
    {
        public string Name { get; set; }
        public bool IsSelected { get; set; }
    }

    private Duration _Duration = new Duration();


    private void SelectChanged(ChangeEventArgs e)
    {
        var selected = SelectData.SingleOrDefault(t => t.Name == (string)e.Value);

        if (selected != null)
        {
            _Duration = selected;
            _Duration.IsSelected = true;
        }
    }

    private List<Duration> SelectData = new List<Duration>
    {
        new Duration{ Name = "Hello1", IsSelected = false},
        new Duration{ Name = "Hello2", IsSelected = false},
        new Duration{ Name = "Hello3", IsSelected = false},
    };

    protected override async Task OnInitializedAsync()
    {
        var a = SelectData.FirstOrDefault(t => t.IsSelected);
        if (a != null)
            _Duration = a;
    }
}
John Mc
  • 212
  • 2
  • 16

4 Answers4

1

As you haven't provided code, but screen shots here's a standalone Blazor page that I believe encapsulates your issue. I've missed a few of your code bits out. I haven't tested it will all your extra bits on you screen captures, for a reason, as far as I can see it works (to quote "as it says on the tin").

@page "/Select"
<h3>Select</h3>
<div class="form-group col-md-6">
    <label for="dur">Duration</label>
    <select @bind="Duration" class="custom-select">
        <option disabled value="-1" selected>Select an Option</option>
        @foreach (var kvp in SelectData)
        {
            <option value="@kvp.Key">@kvp.Value</option>
        }
    </select>
</div>
<div class="p-2 m-2">
    Current Value : @_Duration
</div>


@code {
    private string _Duration;

    private string Duration
    {
        get => _Duration;
        set
        {
            if (value != _Duration)
            {
                _Duration = value;
            }
        }
    }

    private Dictionary<int, string> SelectData = new Dictionary<int, string>()
    {
        { 11746, "No"},
        { 11747, "Provision Only"},
        { 11748, "Full Installation"}
    };
}

My Firefox is "88.0.1". 64 Bit. Windows 10.

Show me where your problem is in this demo?

Here's the onchanged version:

@page "/Select"
<h3>Select</h3>

<div class="form-group col-md-6">
    <label for="dur">Duration</label>
    <select class="custom-select" @onchange="this.SelectChanged">
        <option disabled value="-1" selected>Select an Option</option>
        @foreach (var kvp in SelectData)
        {
            <option value="@kvp.Key">@kvp.Value</option>
        }
    </select>
</div>
<div class="p-2 m-2">
    Current Key : @_Duration
</div>
<div class="p-2 m-2">
    Current Value : @_DurationValue
</div>


@code {
    private string _Duration;

    private string _DurationValue;

    private string Duration
    {
        get => _Duration;
        set
        {
            if (value != _Duration)
            {
                _Duration = value;
            }
        }
    }

    private void SelectChanged(ChangeEventArgs e)
    {
        _Duration = e.Value.ToString();
        if (int.TryParse(e.Value.ToString(), out int value))
            _DurationValue = SelectData[value];

    }

    private Dictionary<int, string> SelectData = new Dictionary<int, string>()
{
        { 11746, "No"},
        { 11747, "Provision Only"},
        { 11748, "Full Installation"}
    };
}
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • I cannot use "@bind" since "@onchange" is needed – John Mc Jun 25 '21 at 19:32
  • Why? You don't show that. You can't expect an answer without providing some detail, we're not mind readers! – MrC aka Shaun Curtis Jun 25 '21 at 20:22
  • I did edit my post to include this "The @onchange event commits the newly picked option to the database and runs a bunch of biz rules." – John Mc Jun 25 '21 at 20:32
  • Hi, I've added an OnChange to my answer and it still works as it should. you need to show us some real code. – MrC aka Shaun Curtis Jun 25 '21 at 20:41
  • I replaced the Blazor code picture with Blazor code. Now your code works like mine which submits the selected option to the onchange. However your code is missing the ability tell the select which option was already selected. Upon refresh is just displays "Select an Option". – John Mc Jun 25 '21 at 21:01
  • I'm not sure how else to take your example and provide on-loading behavior beside what my implementation already does. Then there is the problem with FireFox that it shows one thing and the HTML that says another thing. – John Mc Jun 25 '21 at 21:03
  • I added a small repro based on your code to the end of my post. – John Mc Jun 25 '21 at 21:35
1

Here's a complete solution...Copy and paste, run and test.

@page "/"


@if (SelectData != null)
{
    <div class="form-group col-md-6">
        <label for="dur">Duration</label>
        <select class="custom-select" @onchange="this.SelectChanged">
            <option disabled selected>Select an Option</option>
            @foreach (var duration in SelectData)
            {
                  <option value="@duration.Name" selected="@duration.IsSelected">@duration.Name</option>
               
            }
        </select>
    </div>

   
}
else
{
    <label>Loading</label>
}

<span class="p-2 m-2">
    Current Value : @_Duration.Name
</span>
<span class="p-2 m-2">
    IsSelected : @_Duration.IsSelected
</span>
<br />
<div>
@foreach (var duration in SelectData)
 {
     <div>@duration.Name: @duration.IsSelected</div>
 }
</div>


@code {
    private class Duration
    {
        public string Name { get; set; }
        public bool IsSelected { get; set; }
    }

    private Duration _Duration = new Duration();


    private void SelectChanged(ChangeEventArgs e)
    {
        var selected = SelectData.SingleOrDefault(t => t.Name == (string)e.Value);

           
        if (selected != null)
        {
            _Duration = selected;
            _Duration.IsSelected = true;
        }
    }

    private List<Duration> SelectData = new List<Duration>
{
        new Duration{ Name = "Hello1", IsSelected = false},
        new Duration{ Name = "Hello2", IsSelected = false},
        new Duration{ Name = "Hello3", IsSelected = false},
        new Duration{ Name = "Hello4", IsSelected = false}
    };
} 

Edit

John Mc, selected="@duration.IsSelected" is not Html. This is Razor markups that is rendered as the empty Html selected attribute or selected="", depending on the browser used. That is the way to do that in Blazor ( though in the current case you could apply only the selected attribute without a value. That is, you could have written:

@if (_Duration == duration)
{ 
  <option value="@duration.Name" selected>@duration.Name</option>
}

), meaning that the value of the selected attribute is processed by the Blazor team, and if it is evaluated to true, the selected Html attribute is added, if it is evaluated to 'false`, it is omitted altogether, before rendered as Html in the browser.

The answer you provided a link to deals with Html, and this is not the case here. The @if statement above is not Html. it is Razor markups.

The selected Html attribute is an empty attribute; that is, its presence instruct the browser to select a given option. If absent, no selection is made. If you use selected="false" the given option would be selected. If you use selected="Not false" the given option would be selected.

To sum up, depending on what you really want to do, there are much better ways to implement this. Most importantly, you should use two way data-binding, with the @bind attribute. Are you aware that you were using one-way data-binding ?

miken32
  • 42,008
  • 16
  • 111
  • 154
enet
  • 41,195
  • 5
  • 76
  • 113
  • I had no idea that selected can accept a bool, but it works! This other conversation says doing so is "non-standard" and "there aren't any other valid values other than 'selected'" and "anything inside the quotes is ignored" etc. https://stackoverflow.com/questions/1033944/what-values-can-appear-in-the-selected-attribute-of-the-option-tag – John Mc Jun 28 '21 at 15:13
  • Setting disabled to a bool also works. I want to Accept your answer but I don't understand why it works. – John Mc Jun 28 '21 at 15:47
  • Thank you. That Razor interpretation of – John Mc Jun 28 '21 at 19:54
  • "Where did you learn it?" From various sources, including the Blazor's source code. No, you won't find any documentations explaining this specifically, just as you won't find a single source explaining to Blazor fans that the HttpClient service used in WebAssembly Blazor Apps is based on the JavaScript Fetch API. How do I know that? I inspected the C# code relating the the HttpClient object; I inspected the JS code of Blazor, I know JS, and thus the obvious conclusion. Of course you won't find it documented in the Blazor documents. Perhaps in Github's issues, it may be referred to somehow... – enet Jun 28 '21 at 20:43
  • "With regard to @bind." I was expecting this question ;} This is easy peasy, but merits a new question. And I inclined to believe that it is documented no where, though I've noticed that Steve Sanderson code like that. – enet Jun 28 '21 at 20:43
  • John Mc, email me if you're interested in getting the code that uses the @bind directive... See email in my profile... – enet Jun 29 '21 at 10:22
0

IF you generated both your Edge and FF markup with the exact same code, then it looks like those browsers are interpreting selected and disabled flags differently when you try to provide a string value for them.

I remember once upon a time having to use things like disabled="disabled" but AFAIK the standard now is just to include the flag name with no value. My guess is that your code line <option disabled="" selected="">Select Answer</option> is handled better by Edge, and worse by FF, but the reason is that the markup you've provided is ambiguous.

Could you start with 3 changes and let me know if it works for you?

  • Remove the "SelectedIndex=-1" line, since it doesn't do anything. You will be sending your value out in @onchange, and your title option is not selectable because it's disabled.
  • Amend the title line to <option disabled selected>Select Answer</option>
  • Change all selected = "selected" items to just selected and do the same for disabled as well.
Bennyboy1973
  • 3,413
  • 2
  • 11
  • 16
  • I can do better. I put a working example at the end of my post that has the changes you requested. It demonstrates the problem in my FF. – John Mc Jun 25 '21 at 21:40
  • Your new sample is passing your selected item as a reference-- which means that when you set `_Duration.IsSelected = true;`, you are also setting the selected status of the corresponding item in your list. Firefox is confused because you have multiple options set as `selected`, but have not set your ` – Bennyboy1973 Jun 25 '21 at 21:58
  • Okay, I'm adding an additional answer, which works fine in FF. – Bennyboy1973 Jun 25 '21 at 22:08
0

The following works fine for me in FF. Note that:

-You don't have to set a -1 value for your title option

-Since you are initializing SelectData directly it won't be null.

@page "/Select"
<h3>Select</h3>

<div class="form-group col-md-6">
    <label for="dur">Duration</label>
    <select class="custom-select" @onchange="this.SelectChanged">
        <option disabled selected>Select an Option</option>
        @foreach (var duration in SelectData)
        {
            if (duration == _Duration)
            {
                <option value="@duration.Name" selected>@duration.Name</option>
            }
            else
            {
                <option value="@duration.Name">@duration.Name</option>
            }
        }
    </select>
</div>


<div class="p-2 m-2">
    Current Value : @_Duration.Name
</div>
<div class="p-2 m-2">
    Duration : @_Duration.Value.ToString()
</div>


@code {
    private class Duration
    {
        public string Name { get; set; }
        public TimeSpan Value { get; set; }
    }

    private Duration _Duration = new Duration();


    private void SelectChanged(ChangeEventArgs e)
    {
        _Duration = SelectData.SingleOrDefault(t => t.Name == (string)e.Value);
    }

    private List<Duration> SelectData = new List<Duration>
{
        new Duration{ Name = "Hello1", Value= new TimeSpan(1, 30, 0)},
        new Duration{ Name = "Hello2", Value= new TimeSpan(3, 0, 0)},
        new Duration{ Name = "Hello3", Value= new TimeSpan(10, 0, 0)},
    };

    protected override async Task OnInitializedAsync()
    {
        _Duration = SelectData[2]; // Do your awaitable DB stuff here.
    }
}
Bennyboy1973
  • 3,413
  • 2
  • 11
  • 16
  • Thank you Benny. Keep in mind I need to satisfy two states. First is existing datum may already be selected and the select must show it. Second, if the select is changed, then the IsSelected must be set in onchange which then is used by a bunch of business logic. As far as I know, (without using @bind) the only way to show the first case is to incorporate IsSelected as I've shown. Your example will not show a pre-existing selected answer. – John Mc Jun 25 '21 at 22:17
  • Unless you want to have multiple items selected, then you shouldn't do it that way. In your data loader, you should identify which item is selected, and set _Duration to that item, then use that to determine which item to select. I'll update this answer to show that. – Bennyboy1973 Jun 25 '21 at 22:22
  • It is interesting the Edge doesn't mind my current code. – John Mc Jun 25 '21 at 22:28
  • Okay, updated the answer to show pre-selected data. You're right, it is interesting. Each browser developer has to make choices about edge conditions like `selected=""` Should it just output `selected` since you don't need a string for that flag anyway? Should it decide you have a good reason to add an empty string, like maybe you'll do some JavaScript trickery with it later, and output it even though it's malformed? Should it evaluate as false, since it's not an allowable option? That's the kind of edge case where browsers are likely to act differently. – Bennyboy1973 Jun 25 '21 at 22:31
  • I adapted your example with a few tweaks to manage my IsSelected flag for the "backend" leaving your render logic as is. I'll paste it under my previous example. Alas, the FireFox problem remains. – John Mc Jun 25 '21 at 22:39
  • Can you try my second example in FF to see if when selecting Hello3, FF shows Hello1? – John Mc Jun 25 '21 at 22:57
  • This is a C# issue, not a Blazor one. A class is a reference type, not a value type. When you set `_Duration` to one of the objects in your list (`_Duration = selected;`), then changes made to _Duration will change that object in your list. So you'll end up with multiple objects with IsSelected being set to true. Then, the browser has a single-option ` – Bennyboy1973 Jun 25 '21 at 23:18