Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Blazor error "The call is ambiguous between the following methods or properties" when working with InputSelect

I am working on a server side Blazor application. My goal is to dynamically populate a dropdown with a list of objects, then do something with that object when it is selected. This is the error I am getting:

The call is ambiguous between the following methods or properties: 'RenderTreeBuilder.AddAttribute(int, string, string?)' and 'RenderTreeBuilder.AddAttribute(int, string, MulticastDelegate?)'

The error is in the file "ListDropdownComponent.g.cs" I have not made any changes to this file, so I suspect Visual Studio is pointing me to the wrong source.

When I comment out this bit of code the error goes away, so I think this is where the error is:

<InputSelect @bind-Value="SelectedList" TValue="ListModel" @onchange="AddToList">
    <option value="@null">Add to list</option>
    @foreach (ListModel list in Lists)
    {
        <option value="@list">@list.ListName</option>
    }
    <option value="@(new ListModel{ ListID = -1, ListName="New List" }) ">New List</option>
</InputSelect>

SelectedList is using the ListModel class:

public ListModel SelectedList;

UPDATE: I switched to using the int ID property of my class and I am still getting the same error. So I have no idea what the cause is.

UPDATE 2: This is the problem: value="@null". I will find a way to work around this, but since int? is nullable I would still like to know what the actual issue is.

like image 380
Joe Avatar asked Oct 31 '25 12:10

Joe


1 Answers

This is the problem: value="@null". I will find a way to work around this, but since int? is nullable I would still like to know what the actual issue is

The code you wrote:

<option value="@null" selected>Add to list...</option>

Is transformed to more involved code that builds the page. Your @null there gets transformed in a *.g.cs file that will end up looking something like:

                __builder2.AddAttribute(13, "ValueChanged", global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<Microsoft.AspNetCore.Components.EventCallback<int?>>(Microsoft.AspNetCore.Components.EventCallback.Factory.Create<int?>(this, global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.CreateInferredEventCallback(this, __value => _selectedList = __value, _selectedList))));
                __builder2.AddAttribute(14, "ValueExpression", global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<System.Linq.Expressions.Expression<System.Func<int?>>>(() => _selectedList));
                __builder2.AddAttribute(15, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((__builder3) => {
                    __builder3.OpenElement(16, "option");
                    __builder3.AddAttribute(17, "value", 
#nullable restore
#line 15 "C:\repos\path\to\your\project\Pages\Whatever.razor"
                            null

#line default
#line hidden
#nullable disable
                    );

Your null from the page markup has ended up placed under the #line 15 in the generated code. You can see your page markup is transformed into other code, and that other code builds the page html. The problem has come because your use of null where you did has led to code being generated that calls:

__builder3.AddAttribute(17, "value", null);

But there are two overloads of AddAttribute that match this:

RenderTreeBuilder.AddAttribute(int, string, string?)
RenderTreeBuilder.AddAttribute(int, string, MulticastDelegate?)

The compiler can't know which one to call. It's exactly the same as writing this code:

static void Main(){
  Hello(null);        //error: the call is ambiguous between Hello(string) and Hello(int?)
}

static void Hello(string x) { }
static void Hello(int? x) { }

If you want to overcome this kind of thing, you can cast the null to a particular type so the compiler can know which overload you want to call:

Hello((string)null);

As such it would be possible to amend your code to be like:

<option value="@(int?)null" selected>Add to list...</option>

And that would make the error go away, because the compiler would know which overload to call..

..but it introduces another problem, and looking at the code as a whole we don't generally write Blazor like this anyway.

Blazor has a data binding focus; you bind page elements to variables and you then use those variables to make other decisions. You don't take the mindset of "have an event that is fired when the combo changes, and in that event handler you get the selected index of the combo and get the..." - that's quite a Windows Forms way of looking at the world. In Blazor it's more like "you bind the combo to the X variable, then when you come to make some decision about what to save in the DB (like when the user presses Save), you look at the value of X"

As such, you might code something more like:

...

        <InputSelect @bind-Value="_selectedListId" TValue="int" >
            <option value="-1">Choose a list...</option>
            @foreach (var list in _existingLists)
            {
                <option value="@list.ListID">@list.ListName</option>
            }
            <option value="-2">New List</option>
        </InputSelect>

        @if(_selectedList == -2){
            called: 
            <InputText @bind-Value="_newListName" ></InputText>
        }
...

@code{
    class ListModel{ public string ListName { get; set; } public int ListID { get; set; } }

    int _selectedListId = -1;
    string _newListName;
    List<ListModel> _existingLists = new(){ 
      new ListModel { ListID = 100, ListName = "Red Team" }, 
      new ListModel { ListID = 101, ListName = "Blue Team" } 
    };
}

You bind the Select to the _selectedListId private int field. You give everything in the list Options a number for its value=. Perhaps you're opening in "edit-an-existing-X" mode and your initialize routine looks in some DB and gets the chosen list number for whatever is being shown, and sets _selectedListId to 100 etc. Or maybe this is the "add-new-X" page and we default the selected list id to -1.

The combo is rendered with all its option items; in the code above the _selectedlistId is -1, so the combo is showing at "Choose a list..". The user opens the combo and chooses "Red Team". _selectedListId is set to 100 as a consequence of them doing this - you don't need to act on this right away - they chose Red Team, _selectedListId is now 100, and it will still be 100 when time comes to save it to DB or whatever..

In a similar vein, if they choose "Add to new list..", that sets _selectedListId to -2, and when a re-render occurs, this now means that the if(_selectedListId == -2) in the markup is true, so more things are rendered - a textbox appears for the user to type their new list name in. That textbox's value is @bind'd to _newListName, so when time comes to save, you can read _selectedListId of -2, you can invoke the int CreateNameListNamed(string name) method, which creates the new list and returns the id 102 etc, then you can save your data with 102 as the list ID..

What I'm trying to get to, is that generally we look for ways to leverage this "bind inputs so they change state, make rendering or logic decisions from state" process, rather than that WinFormsy "bind handlers to events on control X, query control X for selection, change properties of control Y as a result" approach

If this doesn't go far enough in terms of what you want to do when the user makes a selection you can, instead of using @bind-Value (which generates a two way binding by created pre-formed behaviors on the Value/ValueExpression/ValueChanged properties) manually specify behaviors for these.

I'm sure plenty of blogs will tell you about @bind-Value vs Value + ValueChanged + ValueExpression but in essence, mostly you can get away with @bind-Value, and you can even make the setting of the property to trigger your additional action if it makes sense:

<InputSelect @bind-Value="SelectedListId" TValue="int" >

int _selectedListId;
int SelectedListId {
  get => _selectedListId;
  set { _undoHistoryList.Add(_selectedListId); _selectedListId = value; }
}

See how the setting of the property also updated the undo history? This means we carry on using bind-Value, because it is clean and simple and trigger these extra bits from the code in the context of the set

You could also split out and provide your own:

<InputSelect Value="_selectedListId" ValueExpression="() => selectedListId" ValueChanged="x => { _undoHistory.Add(_selectedListId); _selectedListId = x; }" TValue="int" >

..but strive to "do it with @bind-" where you can

like image 123
Caius Jard Avatar answered Nov 03 '25 03:11

Caius Jard



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!