블레이저 - 양 방향 바인딩이 가능한 Flag Enum 입력 콘트롤.

Blazor에서 Data Validation을 지원하는 InputSelect 컴포넌트는 enum 복수 선택할 때 배열로 처리하기 때문에, flag enum 형식의 속성에 데이터 바인딩을 직접 거는 것이 불가능합니다.

그래서, EditForm에서 간편하게 바인딩할 수 있는 체크박스 컴포넌트를 작성해봤습니다.

// InputFlaggedEnum.razor

@typeparam TEnum

@inject IStringLocalizer<TypeNames> TypeLocalizer

<div class="btn-group" role="group" >
    @foreach (var key in _memberInts.Keys)
    {
        <input type="checkbox" class="btn-check" id="@key.ToString()" autocomplete="off"
            @onchange="@(() => OnCheckChanged(key))" checked="@_checkedStataus[key]" />

        <label class="btn btn-outline-success" for="@key.ToString()">@TypeLocalizer[key.ToString()]</label>
    }
</div>

@code {

    [Parameter]
    [EditorRequired]
    public TEnum? Options { get; set; }

    [Parameter]
    public TEnum? Value { get; set; }
    private int _valueInt;

    [Parameter]
    public EventCallback<TEnum> ValueChanged { get; set; }

    private Dictionary<TEnum, int> _memberInts = new();
    private Dictionary<TEnum, bool> _checkedStataus = new();

    private async void OnCheckChanged(TEnum selected)
    {
        _checkedStataus[selected] = !_checkedStataus[selected];

        var memberInt = _memberInts[selected];

        if (_checkedStataus[selected])
        {
            Value = StringToEnum( (_valueInt |= memberInt).ToString() );
        }
        else
        {
            Value = StringToEnum( (_valueInt &= (_valueInt ^ memberInt)).ToString() );
        }

        await ValueChanged.InvokeAsync(Value);
    }

    protected override void OnInitialized()
    {
        base.OnInitialized();

        if(Options != null)
        {
            var enumType = Options.GetType();

            if (enumType.IsEnum)
            {
                var optionInt = EnumToInt(Options);

                foreach (var member in Enum.GetValues(enumType))
                {
                    if (System.Numerics.BitOperations.PopCount((uint)(int)member) != 1)
                        continue;

                    var memberNum = (int)member;
                    if ((memberNum & optionInt) == memberNum)
                    {
                        _memberInts[(TEnum)member] = memberNum;
                        _checkedStataus[(TEnum)member] = false;
                    }
                }

                if (Value != null)
                {
                    _valueInt = EnumToInt(Value);

                    if (_valueInt != 0)
                    {
                        foreach (var memberInt in _memberInts)
                        {
                            if ((_valueInt & memberInt.Value) == memberInt.Value)
                            {
                                _checkedStataus[memberInt.Key] = true;
                            }
                        }
                    }
                }
            }
        }
    }

    private int EnumToInt(TEnum value)
    {
        return (int)Enum.Parse(typeof(TEnum), value.ToString());
    }

    private TEnum StringToEnum(string numOrConstant)
    {
        return (TEnum)Enum.Parse(typeof(TEnum), numOrConstant);
    }
}
[Flags]
public enum DayOfWeekFlag
{
    Non = 0, // == defualtOf(DayOfWeekFlag)
    Sun = 1 << DayOfWeek.Sunday,
    Mon = 1 << DayOfWeek.Monday,
    Tue = 1 << DayOfWeek.Tuesday,
    Wed = 1 << DayOfWeek.Wednesday,
    Thu = 1 << DayOfWeek.Thursday,
    Fri = 1 << DayOfWeek.Friday,
    Sat = 1 << DayOfWeek.Saturday,
    End = 0b100_0001,
    All = 0b111_1111,
}

사용하기

// index.razor
@attribute [Route(Routes.Index)]

@inject IStringLocalizer<TypeNames> TypeLocalizer

@code {
    private DayOfWeekFlag DayOfWeek { get; set; } = DayOfWeekFlag.End;
}

@{
    var options = DayOfWeekFlag.All;
}
<InputFlaggedEnum Options="@options" @bind-Value="@DayOfWeek"/>


<h3>
     @string.Join("", DayOfWeek
                        .ToString()
                        .Split(", ")
                        .Select( x => TypeLocalizer[x].ToString())
                        .ToArray())
</h3>

초기값

image

체크 변경

image

옵션 변경

// index.razor
...
@{
    var options = DayOfWeekFlag.All ^ DayOfWeekFlag.End;
}
...

변경된 초기값

image

10개의 좋아요

좋은 글 감사합니다! :+1:

1개의 좋아요

저는 조금 간편한 방식을 찾아서 적용해봤습니다

// eTicketStateFilter.cs

public enum eTicketStateFilter
{
    /// <summary>
    /// 취소
    /// </summary>
    PROC01 = 1 << 0,

    /// <summary>
    /// 처리완료
    /// </summary>
    PROC03 = 1 << 2,

    /// <summary>
    /// 접수완료
    /// </summary>
    PROC05 = 1 << 4,

    /// <summary>
    /// 방문예정
    /// </summary>
    PROC07 = 1 << 6,

    /// <summary>
    /// 처리중
    /// </summary>
    PROC09 = 1 << 8,

    /// <summary>
    /// 연기
    /// </summary>
    PROC11 = 1 << 10,
}

// ListPage.razor
  eTicketStateFilter options = (eTicketStateFilter)paramState;
  
  if (options.HasFlag(eTicketStateFilter.PROC01)) checked_proc01 = true;
  if (options.HasFlag(eTicketStateFilter.PROC03)) checked_proc03 = true;
  if (options.HasFlag(eTicketStateFilter.PROC05)) checked_proc05 = true;
  if (options.HasFlag(eTicketStateFilter.PROC07)) checked_proc07 = true;
  if (options.HasFlag(eTicketStateFilter.PROC09)) checked_proc09 = true;
  if (options.HasFlag(eTicketStateFilter.PROC11)) checked_proc11 = true;

물론 동적인 Dictionary방식은 아니지만, bool값의 종류와 개수가 고정적이라서 HasFlag로 구현해봤습니다.

2개의 좋아요

특정 enum 에만 쓸 수 있는 전용 콘트롤을 만드는 것도 나쁘지 않은 선택 같습니다.

저도 범용 콘트롤을 만들어 놓고도, 가끔은 전용 콘트롤을 별도로 사용하는게 더 나은 경우가 있더군요.

다만, enum 의 멤버의 이름을 semantic 하지 않게 정의한 이유가 궁금합니다.
저라면, enum 을 아래와 같이 정의했을 것 같은데 말이죠.

[Flags]
public enum eTicketState
{
    Requested = 0, // 상태 기본 값
    Accepted = 1 << 2,
    Processing = 1 << 4,
    Published = 1 << 6,    
    Cancelled = 1 << 8, // 고객의 취소
    Postponed  = 1 << 10, // 고객의 연기
    Used = 1 << 30, // 정상적 티켓 사용.
    Invalidated = 1 << 31, // 노쇼, 훼손, 분실, 재발행 등. 음수가 됨.
}
4개의 좋아요

DB에 정의해놨던 상수테이블 값을 그대로 적용하느라 그런 것인데, 왜 상수테이블에 값을 저렇게 해놨었는지는…
분명 뭔가 이유가 있었던거 같은데 기억이 안 납니다… :sweat_smile:
저도 웬만해선 01, 02 이런식으로 하지않고 구체적으로 명시하는 편인데
아마 프로세스에서 확정되지 않은 절차를 임시로 명명하느라 그렇게 정했던거 같습니다.
차차 정리해봐야죠… :thinking:

2개의 좋아요

이 코드에 심각한 문제가 있었네요.

아래는 부트스트랩의 버튼 체크 박스를 그대로 사용한 것인데,

<div class="btn-group" role="group" >
    @foreach (var key in _memberInts.Keys)
    {
        <input type="checkbox" class="btn-check" id="@key.ToString()" autocomplete="off"
            @onchange="@(() => OnCheckChanged(key))" checked="@_checkedStataus[key]" />

        <label class="btn btn-outline-success" for="@key.ToString()">@TypeLocalizer[key.ToString()]</label>
    }
</div>

아마 고수 분들은 이미 눈치채셨겠지만, 태그의 id가 문제입니다.
이 콘트롤을 하나의 페이지에 여러 개 사용하면 동일한 id 로 인해 오작동을 합니다.

모든 input+label 조합이 고유의 id 로 엮이도록 하기 위해, 아래와 같이 부모 콘트롤의 해시코드를 저장한 후에,

@code{
//...
private int _hash;

override void OnInitialized()
{
//...
    _hash = this.GetHashCode();
}

// ...
}

이 값을 베이스로 id 값을 부여합니다.

//...
        <input type="checkbox" class="btn-check" id="@(_hash + (int)key)" autocomplete="off"
            //...

        <label class="btn btn-outline-success" for="@(_hash + (int)key)">@TypeLocalizer[key.ToString()]
            //...

이 걸 못 찾고 몇 번을 쓰고 지웠는지 ㅠㅠ

다시 보자 id 값!!

6개의 좋아요