Blazor- EditForm InputCheckbox nullable bools issue work around

4.1k Views Asked by At

I am trying to create a bespoke input for editform on Blazor deriving from inputbase however I am struggling to get a grasp of it as I have only recently picked up Blazor this week and C#, in general, this month.

I have found https://www.meziantou.net/creating-a-inputselect-component-for-enumerations-in-blazor.htm (Or find code pasted below) and been able to use it for nullable enumarations inside of an inputselect however trying to replicate it for an input checkbox nullable has come to no avail. I was wondering if anyone has a link or would know how to tweak it to get this to work.

Thank you in advance, I will be on my computer all day virtually so feel free to ask questions, try not to berate me haha.

// file: Shared/InputSelectEnum.cs
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;
using Humanizer;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering;


// Inherit from InputBase so the hard work is already implemented 
// Note that adding a constraint on TEnum (where T : Enum) doesn't work when used in the view, Razor raises an error at build time. Also, this would prevent using nullable types...
namespace OrderServiceFrontEnd.Shared
{
    public sealed class InputSelectEnum<TEnum> : InputBase<TEnum>
    {
        // Generate html when the component is rendered.
        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));

            // Add an option element per enum value
            var enumType = GetEnumType();
            foreach (TEnum value in Enum.GetValues(enumType))
            {
                builder.OpenElement(5, "option");
                builder.AddAttribute(6, "value", value.ToString());
                builder.AddContent(7, GetDisplayName(value));
                builder.CloseElement();
            }

            builder.CloseElement(); // close the select element
        }

        protected override bool TryParseValueFromString(string value, out TEnum result, out string validationErrorMessage)
        {
            // Let's Blazor convert the value for us 
            if (BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out TEnum parsedValue))
            {
                result = parsedValue;
                validationErrorMessage = null;
                return true;
            }

            // Map null/empty value to null if the bound object is nullable
            if (string.IsNullOrEmpty(value))
            {
                var nullableType = Nullable.GetUnderlyingType(typeof(TEnum));
                if (nullableType != null)
                {
                    result = default;
                    validationErrorMessage = null;
                    return true;
                }
            }

            // The value is invalid => set the error message
            result = default;
            validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
            return false;
        }

        // Get the display text for an enum value:
        // - Use the DisplayAttribute if set on the enum member, so this support localization
        // - Fallback on Humanizer to decamelize the enum member name
        private string GetDisplayName(TEnum value)
        {
            // Read the Display attribute name
            var member = value.GetType().GetMember(value.ToString())[0];
            var displayAttribute = member.GetCustomAttribute<DisplayAttribute>();
            if (displayAttribute != null)
                return displayAttribute.GetName();

            // Require the NuGet package Humanizer.Core
            // <PackageReference Include = "Humanizer.Core" Version = "2.8.26" />
            return value.ToString().Humanize();
        }

        // Get the actual enum type. It unwrap Nullable<T> if needed
        // MyEnum  => MyEnum
        // MyEnum? => MyEnum
        private Type GetEnumType()
        {
            var nullableType = Nullable.GetUnderlyingType(typeof(TEnum));
            if (nullableType != null)
                return nullableType;

            return typeof(TEnum);
        }
    }

}

Blazor comp:

<InputSelectEnum @bind-Value="@_Order.IOSAppDetails.PhasedRelease"/>
3

There are 3 best solutions below

2
On BEST ANSWER

You can inherit from the InputBase<bool?> class and handle the bound values with some additional properties.

In this example I did not use the 'code behind' approach although it will look more or less the same.

The component is in a file named NullableBoolCheckBox.razor

@inherits InputBase<bool?>

<input type="checkbox" value="@CurrentValue" @attributes="AdditionalAttributes" class="@CssClass" style="width:15px; height:15px;vertical-align:-2px;"
       @onchange="EventCallback.Factory.CreateBinder<bool?>(this,OnChangeAction,this.CurrentValueAsBool)" />
<label @onclick="SetValueToNull" style="width:15px;height:15px;">[x]</label>

@code {

    bool? _CurrentValueAsBool;
    private bool? CurrentValueAsBool
    {
        get
        {
            if (string.IsNullOrEmpty(CurrentValueAsString))
                _CurrentValueAsBool = null;
            else
            {
                if (bool.TryParse(CurrentValueAsString, out bool _currentBool))
                    _CurrentValueAsBool = _currentBool;
                else
                    _CurrentValueAsBool = null;
            }

            SetCheckBoxCheckedAttribute(_CurrentValueAsBool);

            return _CurrentValueAsBool;
        }
        set => _CurrentValueAsBool = value;
    }


    void SetCheckBoxCheckedAttribute(bool? _currentValueAsBool)
    {
        bool _isChecked = _currentValueAsBool.HasValue ? _currentValueAsBool.Value : false;
        var _attributes = AdditionalAttributes != null ? AdditionalAttributes.ToDictionary(kv => kv.Key, kv => kv.Value) : new Dictionary<string, object>(); ;

        if (!_isChecked)
        {
            _ = _attributes.ContainsKey("checked") ? _attributes["checked"] = false : _attributes.TryAdd("checked", false);
        }
        else
        {
            _ = _attributes.ContainsKey("checked") ? _attributes["checked"] = true : _attributes.TryAdd("checked", true);
        }
        AdditionalAttributes = _attributes;
    }

    protected override bool TryParseValueFromString(string value, out bool? result, out string validationErrorMessage)
    {
        validationErrorMessage = null;

        if (string.IsNullOrEmpty(value))
        {
            result = null;
        }
        else
        {
            if (bool.TryParse(value, out bool _result))
            {
                result = _result;
            }
            else
            {
                validationErrorMessage = "Unable to parse value!";
                result = null;
                return false;
            }
        }
        return true;
    }

    private Action<Nullable<bool>> OnChangeAction { get => (_inputValue) => CurrentValueAsString = _inputValue.HasValue ? _inputValue.Value.ToString() : null; }

    void SetValueToNull(MouseEventArgs e)
    {
        this.CurrentValueAsString = string.Empty;
    }
}

It can be used in the same way as any other component.

for example:

<EditForm Model="someModel">

@*.more fields.*@

        <label>Nullable value:</label><NullableBoolCheckBox @bind-Value="someModel.SomeNullBoolValue" />
        <br />
        <strong>value is: @(someModel.SomeNullBoolValue.HasValue?$"{someModel.SomeNullBoolValue}":"null")</strong>

@*.more fields.*@

</EditForm>

The model linked to the form has a property: public bool? SomeNullBoolValue { get; set; } that is bound to the check box.

It looks like this: enter image description here

You can probably do something like a click count to cycle through the values true, false, null in case you didn't want to reset the value with an [x] label.

1
On

As an alternative to the [x] label setting the value to null:

If you prefer to cycle between true, false, null you could force the OnChangeAction to set the value to null if it was previously false.

Make use of the hidden attribute on the controls to hide and show the <input> and the <label>

@inherits InputBase<bool?>

<input type="checkbox" value="@CurrentValue" @attributes="AdditionalAttributes" class="@CssClass" style="width:15px; height:15px;vertical-align:-2px;"
       @onchange="EventCallback.Factory.CreateBinder<bool?>(this,OnChangeAction,this.CurrentValueAsBool)" hidden="@_HideCheckBox" />
<label @onclick="SetValueToTrue" hidden="@(!_HideCheckBox)" style="width:15px;height:15px;margin-left:-5px;">[?]</label>

@code {

    bool _HideCheckBox { get; set; } = false;

    bool? _CurrentValueAsBool;
    private bool? CurrentValueAsBool
    {
        get
        {
            if (string.IsNullOrEmpty(CurrentValueAsString))
                _CurrentValueAsBool = null;
            else
            {
                if (bool.TryParse(CurrentValueAsString, out bool _currentBool))
                    _CurrentValueAsBool = _currentBool;
                else
                    _CurrentValueAsBool = null;
            }

            SetCheckBoxCheckedAttribute(_CurrentValueAsBool);

            return _CurrentValueAsBool;
        }
        set => _CurrentValueAsBool = value;
    }


    void SetCheckBoxCheckedAttribute(bool? _currentValueAsBool)
    {
        bool _isChecked = _currentValueAsBool.HasValue ? _currentValueAsBool.Value : false;
        var _checkBoxAttributes = AdditionalAttributes != null ? AdditionalAttributes.ToDictionary(kv => kv.Key, kv => kv.Value) : new Dictionary<string, object>(); ;

        if (!_isChecked)
        {
            _ = _checkBoxAttributes.ContainsKey("checked") ? _checkBoxAttributes["checked"] = false : _checkBoxAttributes.TryAdd("checked", false);
            if (!_currentValueAsBool.HasValue)
                _HideCheckBox = true;
        }
        else
        {
            _HideCheckBox = false;
            _ = _checkBoxAttributes.ContainsKey("checked") ? _checkBoxAttributes["checked"] = true : _checkBoxAttributes.TryAdd("checked", true);
        }
        AdditionalAttributes = _checkBoxAttributes;
    }


    protected override bool TryParseValueFromString(string value, out bool? result, out string validationErrorMessage)
    {
        validationErrorMessage = null;

        if (string.IsNullOrEmpty(value))
        {
            result = null;
        }
        else
        {
            if (bool.TryParse(value, out bool _result))
            {
                result = _result;
            }
            else
            {
                validationErrorMessage = "Unable to parse value!";
                result = null;
                return false;
            }
        }
        return true;
    }

    private Action<Nullable<bool>> OnChangeAction
    {
        get => (_inputValue) =>
        {
            //ignore input value if previously false, to force it to null
            if (this.CurrentValueAsString == bool.FalseString)
            {
                _inputValue = null;
                this.CurrentValueAsString = string.Empty;
            }
            else
                this.CurrentValueAsString = _inputValue.HasValue ? _inputValue.Value.ToString() : string.Empty;
        };
    }

    void SetValueToTrue(MouseEventArgs e)
    {
        this.CurrentValueAsString = bool.TrueString;
    }

}

You could change the <label> to an <image> or make use of font icons to make it pretty.

It then looks like this: checkboxcycle

The use of the component stays the same.

@*.other fields.*@

<label>Nullable value:</label><NullableBoolCheckBox @bind-Value="someModel.SomeNullBoolValue" />
<br />
<strong>value is: @(someModel.SomeNullBoolValue.HasValue?$"{someModel.SomeNullBoolValue}":"null")</strong>
<br />

@*.other fields.*@
0
On

Hello people visiting this page doing blazor for the first time in 2024. Been in blazor for four weeks and needed to add this to the component to make examples work! I'm still trying to understand how everything works and this may help someone save 4 hours unlike me.

@rendermode InteractiveServer