DropdownBase's SelectedItem is of type object #1779

Closed
opened 2026-01-29 17:58:29 +00:00 by claunia · 7 comments
Owner

Originally created by @joriverm on GitHub (Jun 4, 2025).

Is your feature request related to a problem? Please describe.
SelectedItem in DropdownBase is of type object, despite TValue being set to a model type.
this makes it impossible to @bind-SelectedItem to a strongly typed model variable without making it object and doing casting in the code.
It makes code kinda complex.
ive edited an example from the demo to show what i mean and where it goes wrong:

@using RadzenBlazorDemos.Models.Northwind

@inherits DbContextPage

<div class="rz-p-sm-12 rz-text-align-center">
    <RadzenListBox @bind-Value=@value @bind-SelectedItem=@selectedItem Data=@customers TextProperty="@nameof(Customer.CompanyName)" ValueProperty="@nameof(Customer.CustomerID)" Style="width: 100%; max-width: 400px; height: 200px"
                   InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", "select company" }})" />
    <br>
    <span>type: @value?.GetType().Name</span>
    <br>
    <span>type: @selectedItem?.GetType().Name</span>
</div>

@code {
    object value = "AROUT";
    Customer selectedItem = null;
    IEnumerable<Customer> customers;

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();

        customers = dbContext.Customers;
    }
}

this will not compile unless selectedItem is made into a object and other code does castings.
having SelectedItem typed/bindable makes it possible to have a list of models, select the value in the list via the ValueProperty (and not override Equals), but get the actual model of the item that is selected.

Describe the solution you'd like
what i would do, but this could be a complex idea, is to make SelectedItem a seperate value type.
Value in DataBoundFormComponent ( 396fa19d7b/Radzen.Blazor/DataBoundFormComponent.cs (L112) ) be changed to TValue, just like its event callback to make it the same (which is already weird that its object, thats not how blazor components usually work). currently binding (@bind-Value) just happens to work because the EventCallBack is of TValue type

on top of that we'd make SelectedItem from DropDownBase (396fa19d7b/Radzen.Blazor/DropDownBase.cs (L278)) be of type TItem which most of the time would be the same as TValue. however, when ValueProperty is set, these are not the same, as shown in the example above. in that case they would have different Types.

this should be doable i think by having Data (396fa19d7b/Radzen.Blazor/DropDownBase.cs (L403-L425)) be of type IEnumerable, or keep it IEnumerable and having 2 implementations. one that does not have ValueProperty and has Value and SelectedItem of type TValue or an implementation that has ValueProperty but has Value of TValue and SelectedItem of TItem

Describe alternatives you've considered
we are implementing a work around right now by binding to Value and on save fetching the SelectedItem ourselves from the Data list with the key we got from value.

Additional context
if more info is requested, i will supply it :)
maybe another example or case if needed!

and if the solution is accepted, i could edit the code and make a PR myself. thats ok!

Originally created by @joriverm on GitHub (Jun 4, 2025). **Is your feature request related to a problem? Please describe.** `SelectedItem` in `DropdownBase` is of type object, despite `TValue` being set to a model type. this makes it impossible to `@bind-SelectedItem` to a strongly typed model variable without making it object and doing casting in the code. It makes code kinda complex. ive edited an example from the demo to show what i mean and where it goes wrong: ``` @using RadzenBlazorDemos.Models.Northwind @inherits DbContextPage <div class="rz-p-sm-12 rz-text-align-center"> <RadzenListBox @bind-Value=@value @bind-SelectedItem=@selectedItem Data=@customers TextProperty="@nameof(Customer.CompanyName)" ValueProperty="@nameof(Customer.CustomerID)" Style="width: 100%; max-width: 400px; height: 200px" InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", "select company" }})" /> <br> <span>type: @value?.GetType().Name</span> <br> <span>type: @selectedItem?.GetType().Name</span> </div> @code { object value = "AROUT"; Customer selectedItem = null; IEnumerable<Customer> customers; protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); customers = dbContext.Customers; } } ``` this will not compile unless `selectedItem` is made into a `object` and other code does castings. having SelectedItem typed/bindable makes it possible to have a list of models, select the value in the list via the `ValueProperty` (and not override `Equals`), but get the actual model of the item that is selected. **Describe the solution you'd like** what i would do, but this could be a complex idea, is to make `SelectedItem` a seperate value type. Value in `DataBoundFormComponent` ( https://github.com/radzenhq/radzen-blazor/blob/396fa19d7b8b924b8918c61d8f34ffc3e9c9d229/Radzen.Blazor/DataBoundFormComponent.cs#L112 ) be changed to TValue, just like its event callback to make it the same (which is already weird that its object, thats not how blazor components usually work). currently binding (`@bind-Value`) just happens to work because the `EventCallBack` is of `TValue` type on top of that we'd make `SelectedItem` from `DropDownBase` (https://github.com/radzenhq/radzen-blazor/blob/396fa19d7b8b924b8918c61d8f34ffc3e9c9d229/Radzen.Blazor/DropDownBase.cs#L278) be of type `TItem` which most of the time would be the same as `TValue`. however, when `ValueProperty` is set, these are not the same, as shown in the example above. in that case they would have different Types. this should be doable i think by having Data (https://github.com/radzenhq/radzen-blazor/blob/396fa19d7b8b924b8918c61d8f34ffc3e9c9d229/Radzen.Blazor/DropDownBase.cs#L403-L425) be of type IEnumerable<TItem>, or keep it IEnumerable and having 2 implementations. one that does not have `ValueProperty` and has `Value` and `SelectedItem` of type `TValue` or an implementation that has `ValueProperty` but has `Value` of `TValue` and `SelectedItem` of `TItem` **Describe alternatives you've considered** we are implementing a work around right now by binding to `Value` and on save fetching the `SelectedItem` ourselves from the Data list with the key we got from value. **Additional context** if more info is requested, i will supply it :) maybe another example or case if needed! and if the solution is accepted, i could edit the code and make a PR myself. thats ok!
Author
Owner

@enchev commented on GitHub (Jun 4, 2025):

The type is object since this is how we implement it in our first version 5 years ago - we can’t change that easily since it’s going to be huge breaking change.

@enchev commented on GitHub (Jun 4, 2025): The type is object since this is how we implement it in our first version 5 years ago - we can’t change that easily since it’s going to be huge breaking change.
Author
Owner

@joriverm commented on GitHub (Jun 6, 2025):

ye i am aware this is technically a breaking change.
I do believe its a breaking change that will improve radzen a lot, and for Value i think it can be changed to TValue with minimal issues because the controls already enforce TValue to be set and its callback is already of type TValue, which makes @bind- work.

as for selectedItem, its a breaking change that can be worked around i think. i'd need to do some testing with blazor controls and generics and see what happens if you make 2 controls
1 that has 2 generics and 1 that has 1 generic, but implements the 2 generic version with TValue & object
if that works, there would be minimal/no breaking change

@joriverm commented on GitHub (Jun 6, 2025): ye i am aware this is technically a breaking change. I do believe its a breaking change that will improve radzen a lot, and for Value i think it can be changed to TValue with minimal issues because the controls already enforce TValue to be set and its callback is already of type TValue, which makes @bind- work. as for selectedItem, its a breaking change that can be worked around i think. i'd need to do some testing with blazor controls and generics and see what happens if you make 2 controls 1 that has 2 generics and 1 that has 1 generic, but implements the 2 generic version with TValue & object if that works, there would be minimal/no breaking change
Author
Owner

@enchev commented on GitHub (Jun 6, 2025):

I’m afraid that we don’t have plans to introduce such breaking changes.

@enchev commented on GitHub (Jun 6, 2025): I’m afraid that we don’t have plans to introduce such breaking changes.
Author
Owner

@joriverm commented on GitHub (Jun 10, 2025):

@enchev : i think youre too quick to close this.

i did the TValue/Value change locally and it didn't break anything, anywhere. if anything it made things more clear and killed possible bugs.

diff --git a/Radzen.Blazor/DataBoundFormComponent.cs b/Radzen.Blazor/DataBoundFormComponent.cs
index 8e147e1f..8ac08ae1 100644
--- a/Radzen.Blazor/DataBoundFormComponent.cs
+++ b/Radzen.Blazor/DataBoundFormComponent.cs
@@ -103,13 +103,13 @@ namespace Radzen
         /// <summary>
         /// The value
         /// </summary>
-        object _value;
+        private T _value = default(T);
         /// <summary>
         /// Gets or sets the value.
         /// </summary>
         /// <value>The value.</value>
         [Parameter]
-        public object Value
+        public T Value
         {
             get
             {
@@ -117,10 +117,14 @@ namespace Radzen
             }
             set
             {
-                if (_value != value)
+                if (value == null || value.Equals("null"))
                 {
-                    _value = object.Equals(value, "null") ? null : value;
+                    _value = default;
+                    return;
                 }
+
+                if (!value.Equals(_value))
+                    _value = value;
             }
         }
 
@@ -185,7 +189,7 @@ namespace Radzen
                 if (_data != value)
                 {
                     _view = null;
-                    _value = null;
+                    _value = default(T);
                     _data = value;
                     StateHasChanged();
                 }
diff --git a/Radzen.Blazor/RadzenAutoComplete.razor.cs b/Radzen.Blazor/RadzenAutoComplete.razor.cs
index 343d0bc5..98a1f73a 100644
--- a/Radzen.Blazor/RadzenAutoComplete.razor.cs
+++ b/Radzen.Blazor/RadzenAutoComplete.razor.cs
@@ -1,14 +1,13 @@
-using Radzen;
-using Radzen.Blazor.Rendering;
-using System.Collections;
+using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.Forms;
-using Microsoft.JSInterop;
-using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.Web;
-using System.Linq;
+using Microsoft.JSInterop;
+using Radzen.Blazor.Rendering;
 using System;
-using System.Threading.Tasks;
+using System.Collections;
 using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
 
 namespace Radzen.Blazor
 {
@@ -181,7 +180,7 @@ namespace Radzen.Blazor
             var value = await JSRuntime.InvokeAsync<string>("Radzen.getInputValue", search);
 
             value = $"{value}";
-            
+
             if (value.Length < MinLength && !OpenOnFocus)
             {
                 await JSRuntime.InvokeVoidAsync("Radzen.closePopup", PopupID);
@@ -250,7 +249,7 @@ namespace Radzen.Blazor
         /// <param name="args">The <see cref="ChangeEventArgs"/> instance containing the event data.</param>
         protected async System.Threading.Tasks.Task OnChange(ChangeEventArgs args)
         {
-            Value = args.Value;
+            Value = args.Value?.ToString();
 
             await ValueChanged.InvokeAsync($"{Value}");
             if (FieldIdentifier.FieldName != null) { EditContext?.NotifyFieldChanged(FieldIdentifier); }
@@ -263,11 +262,11 @@ namespace Radzen.Blazor
         {
             if (!string.IsNullOrEmpty(TextProperty))
             {
-                Value = PropertyAccess.GetItemOrValueFromProperty(item, TextProperty);
+                Value = PropertyAccess.GetItemOrValueFromProperty(item, TextProperty)?.ToString();
             }
             else
             {
-                Value = item;
+                Value = item?.ToString();
             }
 
             await ValueChanged.InvokeAsync($"{Value}");
@@ -336,7 +335,7 @@ namespace Radzen.Blazor
             {
                 var item = parameters.GetValueOrDefault<object>(nameof(SelectedItem));
                 if (item != null)
-                { 
+                {
                     await SelectItem(item);
                 }
             }
@@ -345,7 +344,7 @@ namespace Radzen.Blazor
 
             if (parameters.DidParameterChange(nameof(Value), Value))
             {
-                Value = parameters.GetValueOrDefault<object>(nameof(Value));
+                Value = parameters.GetValueOrDefault<string>(nameof(Value));
             }
 
             if (shouldClose && !firstRender)
diff --git a/Radzen.Blazor/RadzenDataGridHeaderCell.razor b/Radzen.Blazor/RadzenDataGridHeaderCell.razor
index 4fb31cfe..ffd55954 100644
--- a/Radzen.Blazor/RadzenDataGridHeaderCell.razor
+++ b/Radzen.Blazor/RadzenDataGridHeaderCell.razor
@@ -155,7 +155,7 @@
                                     {
                                         <RadzenProgressBarCircular Style="position:absolute;width:100%;" Visible="@isLoading" Value="100" ShowValue="false" Mode="ProgressBarMode.Indeterminate" />
                                         <RadzenListBox AllowVirtualization="@Column.AllowCheckBoxListVirtualization" AllowClear="true" Multiple="true" Style="height: 300px"
-                                        TValue="IEnumerable<object>" Value=@Column.GetFilterValue() Change="@ListBoxChange" 
+                                        TValue="IEnumerable" Value=@(Column.GetFilterValue() as IEnumerable) Change="@ListBoxChange" 
                                         Data=@filterValues Count=@filterValuesCount LoadData="@LoadFilterValues" 
                                         AllowFiltering="@(!string.IsNullOrEmpty(Column.GetFilterProperty()) && (PropertyAccess.GetPropertyType(typeof(TItem), Column.GetFilterProperty()) == typeof(string) || Column.FilterPropertyType == typeof(string)))"
                                         Disabled="@(!Column.CanSetFilterValue())" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"

i checked listboxes, grids, autocomplete, combobox, ...
all worked fine and didn't need any changes at all. the only breaking change i can imagine is if a person somehow had set TValue (specifically) to a type and then used Value with an object variable using one way binding ( not @bind-Value ).
Or if they set TValue (specifically) to a type and passed a different typed object to Value using one way binding, which probably would have been broken to begin with.

are these breaking changes really bothering you ?
i'd say that use case is probably almost never happens, but thats from my experience in using blazor strongly typed and not the collective experience of blazor developers world wide.

As for SelectedItem that would break some stuff as apparently you can't have 2 components with the same name but with different amount of generics/typeparams like you can with regular C# classes and microsoft has shot down a RFC about default generics for razor ( https://github.com/dotnet/aspnetcore/issues/13619 )
i also opened an issue about it with the razor team : https://github.com/dotnet/razor/issues/11933

so i would like to suggest to drop the selecteditem change until this is possible, and only change TValue. is this an ok change to you @enchev ?

@joriverm commented on GitHub (Jun 10, 2025): @enchev : i think youre too quick to close this. i did the TValue/Value change locally and it didn't break anything, anywhere. if anything it made things more clear and killed possible bugs. ``` diff --git a/Radzen.Blazor/DataBoundFormComponent.cs b/Radzen.Blazor/DataBoundFormComponent.cs index 8e147e1f..8ac08ae1 100644 --- a/Radzen.Blazor/DataBoundFormComponent.cs +++ b/Radzen.Blazor/DataBoundFormComponent.cs @@ -103,13 +103,13 @@ namespace Radzen /// <summary> /// The value /// </summary> - object _value; + private T _value = default(T); /// <summary> /// Gets or sets the value. /// </summary> /// <value>The value.</value> [Parameter] - public object Value + public T Value { get { @@ -117,10 +117,14 @@ namespace Radzen } set { - if (_value != value) + if (value == null || value.Equals("null")) { - _value = object.Equals(value, "null") ? null : value; + _value = default; + return; } + + if (!value.Equals(_value)) + _value = value; } } @@ -185,7 +189,7 @@ namespace Radzen if (_data != value) { _view = null; - _value = null; + _value = default(T); _data = value; StateHasChanged(); } diff --git a/Radzen.Blazor/RadzenAutoComplete.razor.cs b/Radzen.Blazor/RadzenAutoComplete.razor.cs index 343d0bc5..98a1f73a 100644 --- a/Radzen.Blazor/RadzenAutoComplete.razor.cs +++ b/Radzen.Blazor/RadzenAutoComplete.razor.cs @@ -1,14 +1,13 @@ -using Radzen; -using Radzen.Blazor.Rendering; -using System.Collections; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; -using Microsoft.JSInterop; -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; -using System.Linq; +using Microsoft.JSInterop; +using Radzen.Blazor.Rendering; using System; -using System.Threading.Tasks; +using System.Collections; using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace Radzen.Blazor { @@ -181,7 +180,7 @@ namespace Radzen.Blazor var value = await JSRuntime.InvokeAsync<string>("Radzen.getInputValue", search); value = $"{value}"; - + if (value.Length < MinLength && !OpenOnFocus) { await JSRuntime.InvokeVoidAsync("Radzen.closePopup", PopupID); @@ -250,7 +249,7 @@ namespace Radzen.Blazor /// <param name="args">The <see cref="ChangeEventArgs"/> instance containing the event data.</param> protected async System.Threading.Tasks.Task OnChange(ChangeEventArgs args) { - Value = args.Value; + Value = args.Value?.ToString(); await ValueChanged.InvokeAsync($"{Value}"); if (FieldIdentifier.FieldName != null) { EditContext?.NotifyFieldChanged(FieldIdentifier); } @@ -263,11 +262,11 @@ namespace Radzen.Blazor { if (!string.IsNullOrEmpty(TextProperty)) { - Value = PropertyAccess.GetItemOrValueFromProperty(item, TextProperty); + Value = PropertyAccess.GetItemOrValueFromProperty(item, TextProperty)?.ToString(); } else { - Value = item; + Value = item?.ToString(); } await ValueChanged.InvokeAsync($"{Value}"); @@ -336,7 +335,7 @@ namespace Radzen.Blazor { var item = parameters.GetValueOrDefault<object>(nameof(SelectedItem)); if (item != null) - { + { await SelectItem(item); } } @@ -345,7 +344,7 @@ namespace Radzen.Blazor if (parameters.DidParameterChange(nameof(Value), Value)) { - Value = parameters.GetValueOrDefault<object>(nameof(Value)); + Value = parameters.GetValueOrDefault<string>(nameof(Value)); } if (shouldClose && !firstRender) diff --git a/Radzen.Blazor/RadzenDataGridHeaderCell.razor b/Radzen.Blazor/RadzenDataGridHeaderCell.razor index 4fb31cfe..ffd55954 100644 --- a/Radzen.Blazor/RadzenDataGridHeaderCell.razor +++ b/Radzen.Blazor/RadzenDataGridHeaderCell.razor @@ -155,7 +155,7 @@ { <RadzenProgressBarCircular Style="position:absolute;width:100%;" Visible="@isLoading" Value="100" ShowValue="false" Mode="ProgressBarMode.Indeterminate" /> <RadzenListBox AllowVirtualization="@Column.AllowCheckBoxListVirtualization" AllowClear="true" Multiple="true" Style="height: 300px" - TValue="IEnumerable<object>" Value=@Column.GetFilterValue() Change="@ListBoxChange" + TValue="IEnumerable" Value=@(Column.GetFilterValue() as IEnumerable) Change="@ListBoxChange" Data=@filterValues Count=@filterValuesCount LoadData="@LoadFilterValues" AllowFiltering="@(!string.IsNullOrEmpty(Column.GetFilterProperty()) && (PropertyAccess.GetPropertyType(typeof(TItem), Column.GetFilterProperty()) == typeof(string) || Column.FilterPropertyType == typeof(string)))" Disabled="@(!Column.CanSetFilterValue())" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive" ``` i checked listboxes, grids, autocomplete, combobox, ... all worked fine and didn't need any changes at all. the only breaking change i can imagine is if a person somehow had set TValue (specifically) to a type and then used Value with an object variable using one way binding ( not @bind-Value ). Or if they set TValue (specifically) to a type and passed a different typed object to Value using one way binding, which probably would have been broken to begin with. are these breaking changes really bothering you ? i'd say that use case is probably almost never happens, but thats from my experience in using blazor strongly typed and not the collective experience of blazor developers world wide. As for SelectedItem that would break some stuff as apparently you can't have 2 components with the same name but with different amount of generics/typeparams like you can with regular C# classes and microsoft has shot down a RFC about default generics for razor ( https://github.com/dotnet/aspnetcore/issues/13619 ) i also opened an issue about it with the razor team : https://github.com/dotnet/razor/issues/11933 so i would like to suggest to drop the selecteditem change until this is possible, and only change TValue. is this an ok change to you @enchev ?
Author
Owner

@enchev commented on GitHub (Jun 10, 2025):

Please submit pull request. We will review it and we will merge it the changes are acceptable.

@enchev commented on GitHub (Jun 10, 2025): Please submit pull request. We will review it and we will merge it the changes are acceptable.
Author
Owner

@enchev commented on GitHub (Jun 12, 2025):

Do you still need this after the merge of https://github.com/radzenhq/radzen-blazor/pull/2179?

@enchev commented on GitHub (Jun 12, 2025): Do you still need this after the merge of https://github.com/radzenhq/radzen-blazor/pull/2179?
Author
Owner

@joriverm commented on GitHub (Jun 13, 2025):

Hi!

No, if changing the type of SelectedItem is a breaking change that is not wanted i'd say that this can be closed, thanks :)
If i ever were to come across a solution that wouldn't cause a breaking change i will reply here or create a new one to propose something ^^;

Thanks!

@joriverm commented on GitHub (Jun 13, 2025): Hi! No, if changing the type of SelectedItem is a breaking change that is not wanted i'd say that this can be closed, thanks :) If i ever were to come across a solution that wouldn't cause a breaking change i will reply here or create a new one to propose something ^^; Thanks!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/radzen-blazor#1779