5

I've defined a custom select component in Blazor like this:

public class BetterInputSelect<TItem> : InputBase<TItem>
{
  [Parameter]
  public IEnumerable<TItem> Data { get; set; } = new List<TItem>();

  protected override void BuildRenderTree(RenderTreeBuilder builder)
  {
    builder.OpenElement(0, "select");
    builder.AddMultipleAttributes(1, AdditionalAttributes);
    builder.AddAttribute(2, "class", CssClass);
    builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValueAsString));
    builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder<string>(
      this, value => CurrentValueAsString = value, CurrentValueAsString!, null));

    foreach (var item in this.Data)
    {
      builder.OpenElement(5, "option");
      builder.AddAttribute(6, "value", item!.ToString());
      builder.AddContent(7, this.FindDisplayName(item));
      builder.CloseElement();
    }

    builder.CloseElement();
  }

  protected override bool TryParseValueFromString(string? value, out TItem result, out string validationErrorMessage)
  {
    // Check for enums first.
    if (typeof(TItem).IsEnum && BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out TItem? parsedValue))
    {
      result = parsedValue!;
      validationErrorMessage = null!;
      return true;
    }

    // Other types here
    // ...

    result = default!;
    validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
    return false;
  }

  private string FindDisplayName(TItem value)
  {
    return value switch
    {
      null => string.Empty,
      Enum @enum => @enum.GetDescription(),
      _ => value.ToString() ?? string.Empty
    };
  }
}

which can then be used like this:

<BetterInputSelect Data="@Reasons" @bind-Value="@Reason" />

where Reasons and Reason are defined like this:

public SomeReason Reason { get; set; }

private IEnumerable<SomeReason>? Reasons { get; set; }

...

public enum SomeReason 
{
  ...
}

This works great as long as the value that is bound by @bind-Value is not nullable. When I do:

public SomeReason? Reason { get; set; }

I get a compile time error:

[CS0411] The type arguments for method 
'TypeInference.CreateBetterInputSelect_0<TItem>(RenderTreeBuilder, int, int, IEnumerable<TItem>, int, TItem, int, EventCallback<TItem>, int, Expression<Func<TItem>>)' 
cannot be inferred from the usage. Try specifying the type arguments explicitly.

Can I simply not bind to a nullable property, or is there a way to make the compiler happy that I've missed?

EDIT

And if I try to define the type explicitly like this:

<BetterInputSelect
  Data="@Reasons" 
  @bind-Value="@Reason" 
  TItem="SomeReason" />

then I get the following compiler errors:

[CS1503] Argument 1: cannot convert from 'SomeReason?' to 'SomeReason'
[CS1503] Argument 2: cannot convert from 'Microsoft.AspNetCore.Components.EventCallback<SomeReason?>' to 'Microsoft.AspNetCore.Components.EventCallback'
[CS0266] Cannot implicitly convert type 'SomeReason?' to 'SomeReason'. An explicit conversion exists (are you missing a cast?)
[CS1662] Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type
8
  • I would be surprised if this worked with non-nullable value. Types can't be inferred even from constructor parameters or property assignments, where the parameter type is known at compile time. You should specify the type of the TItem property explicitly Commented May 25, 2021 at 15:20
  • Thanks for your reply, I tried to define it explicitly in the <BetterInputSelect ... tag, but couldn't find the correct syntax. Commented May 25, 2021 at 15:46
  • @BartKiers what syntax did you try? It should be something like TItem="SomeReason" Commented May 25, 2021 at 16:57
  • 1
    try this:: <BetterInputSelect Data="@Reasons" @bind-Value="@Reason" TItem="SomeReason?" /> (SomeReason? instead of SomeReason) Commented May 25, 2021 at 19:21
  • 1
    @DamilolaAdegunwa that was indeed part of the solution: many thanks! Commented May 26, 2021 at 5:57

2 Answers 2

4

The solution was as follows:

In BetterInputSelect<TItem>:

// Was:
//   [Parameter]
//   public IEnumerable<TItem> Data { get; set; } = new List<TItem>();

[Parameter]
public IEnumerable<TItem?> Data { get; set; } = new List<TItem?>();

and in the view I did:

<BetterInputSelect
  Data="@(Reasons as IEnumerable<SomeReason?>)" 
  TItem="SomeReason?"
  @bind-Value="@Reason" />

which works when @Reason is nullable- and non-nullable. The non-nullable would of course not need a cast and explicit type definition:

<BetterInputSelect
  Data="@Reasons" 
  @bind-Value="@Reason" />
Sign up to request clarification or add additional context in comments.

2 Comments

Just wonder what is better in your component than the built-in InputSelect. I'm of the opinion that it degrades rather than elevates when you pass IEnumerable<TItem> to the custom component instead of using ChildContent...
The built-in InputSelect can (AFAIK) only hold enums and strings. It only displays the value of enums (where I want to extract custom descriptions from enums, and other objects).
0

I believe that with Net7 / Net8, @(Reasons as IEnumerable<SomeReason?>) doesn't work anymore. I have done this a bit differently now:

Parameters:

[Parameter]
[EditorRequired]
public List<TItem?> Items { get; set; } = new();

[Parameter]
public TItem? SelectedValue { get; set; }

[Parameter]
public EventCallback<TItem?> SelectedValueChanged { get; set; }

protected async Task OnSelectedValueChanged(ChangeEventArgs args)
{
    if (args.Value is not string value)
    {
        return;
    }

    try
    {
        await this.SelectedValueChanged.InvokeAsync((TItem)Convert.ChangeType(value, typeof(TItem)));
    }
    catch
    {
        // ignored
    }
}

Razor:

<select value="@this.SelectedValue" @onchange="@this.OnSelectedValueChanged" @key="@this.Items">
</select>

Usage (Razor):

<MyComponent Items="@this.Items" @bind-SelectedValue="@this.SelectedItem" />

Usage (C#):

protected List<SomeEnum?> Items { get; set; } = new();

protected SomeEnum? SelectedItem { get; set; }

protected override async Task OnInitializedAsync()
{
    this.Items = ((SomeEnum[])Enum.GetValues(typeof(SomeEnum))).Select(t => (SomeEnum?)t).ToList();
}

public Enum SomeEnum
{
    One,
    Two
}

If you have set <Nullable>enable</Nullable> and <TreatWarningsAsErrors>true</TreatWarningsAsErrors> in your project, you probably need to ignore CS8669 by adding it to <NoWarn>CS8669</NoWarn>... (Seems like Visual Studio doesn't know that the nullable context should be enabled for razor syntax things as well)...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.