Search code examples
c#html-parsingblazornullableblazor-editform

Blazor- EditForm InputCheckbox nullable bools issue work around


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"/>

Solution

  • 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.