데이터 검증 메시지를 지역화할 때 마땅한 수단이 없어, 고민들 한 번 씩 해보셨을 겁니다.
닷넷의 데이터 어노테이션을 사용하는 방법이 있지만, 여기에는 두 가지 문제가 있습니다.
첫째로, 라이브러리에서 제공하는 영문 메시지 템플릿은 변경 적용하는 것이 힘들다는 것입니다.
이를 위해서는 수 많은 코드가 필요합니다. (몇 몇 오픈 소스 프로젝트 있기는 합니다만, 불편해 보여서 사용해보지 않았습니다.)
둘째로, 커스텀 resx 파일을 작성하는 경우, Key-Value를 먼저 고민해야 해서, 로직 코드 보다 부담이 많아질 수 있습니다.
블레이저는 다행스럽게도 EditForm 요소가 검증 메시지로 동적인 문자열을 사용할 수 있게끔 지원합니다.
이를 IStringLocalizer 와 함께 사용하면 보다 간편하게 지역화된 Validation 메세지를 작성할 수 있습니다.
@*MyComponent.razor*@
@implements IDisposable
@IStringLocalizer<AppTexts> Texts
<EditForm EditContext="_editContext" OnValidSubmit="Submit" FormName="WeekDaysSelection">
<div>
<label>
<InputCheckbox @bind-Value="Model!.HasMon" /> @Texts["월"]
</label>
</div>
<div>
<label>
<InputCheckbox @bind-Value="Model!.HasTue" /> @Texts["화"]
</label>
</div>
@* 생략 *@
<div>
<label>
<InputCheckbox @bind-Value="Model!.HasSun" /> @Texts["일"]
</label>
</div>
<div>
<ValidationMessage For="() => Model!.Selecteds" />
</div>
<div>
<button type="submit">@Texts["확인"]</button>
</div>
</EditForm>
@code {
[SupplyParameterFromForm]
public WeekDays? Model { get; set; }
private EditContext? _editContext;
private ValidationMessageStore? _messageStore;
protected override void OnInitialized()
{
Model ??= new();
_editContext = new(Model);
_editContext.OnValidationRequested += HandleValidationRequested;
_messageStore = new(_editContext);
}
// 데이터 검증 요청 핸들러
private void HandleValidationRequested(object? sender,
ValidationRequestedEventArgs args)
{
_messageStore?.Clear();
var selected = Model!.Selecteds.Count();
if (selected < 1)
{
_messageStore?.Add(() => Model.Selecteds, Texts["하나 이상 선택해야 합니다."]);
}
else if (selected > 5)
{
_messageStore?.Add(() => Model.Selecteds, Texts["5개 보다 많이 선택하면 안됩니다."]);
}
}
private void Submit()
{
// 데이터 검증이 성공했을 때 실행되는 코드
}
public void Dispose()
{
if (_editContext is not null)
{
_editContext.OnValidationRequested -= HandleValidationRequested;
}
}
public class WeekDays
{
public bool HasSun { get; set; }
public bool HasMon { get; set; }
public bool HasTue { get; set; }
public bool HasWed { get; set; }
public bool HasThu { get; set; }
public bool HasFri { get; set; }
public bool HasSat { get; set; }
public IEnumerable<DayOfWeek> Selecteds
{
get
{
if (HasSun) yield return DayOfWeek.Sunday;
if (HasMon) yield return DayOfWeek.Monday;
if (HasTue) yield return DayOfWeek.Tuesday;
if (HasWed) yield return DayOfWeek.Wednesday;
if (HasThu) yield return DayOfWeek.Thursday;
if (HasFri) yield return DayOfWeek.Friday;
if (HasSat) yield return DayOfWeek.Saturday;
}
}
}
EditForm.EditContext
이 객체는 Form 을 통해 사용자가 데이터를 편집하는 상황(Context)을 나타내는데, 유용한 메타 정보를 제공합니다. .
아래 코드는 데이터 모델로 EditContext 를 생성하고, 메타 정보 중 관심 있는 부분, 즉, 데이터 검증 요청 이벤트를 구독하는 코드입니다.
protected override void OnInitialized()
{
Model ??= new();
_editContext = new(Model);
editContext.OnValidationRequested += HandleValidationRequested;
messageStore = new(editContext);
}
마지막 줄은, 생성된 EditContext로 ValidationMessageStore 객체를 초기화하는데, 이렇게 초기화된 메시지 저장소 객체는 아래 요소가 사용합니다.
<div>
<ValidationMessage For="() => Model!.Selecteds" />
</div>
ValidationMessageStore 객체와 ValidationMessage 요소의 연관성은 매직입니다.
검증 처리 과정
EditContext.OnValidationRequested 이벤트의 전파는 EditForm 이 관장하는데, OnValidSubmit 이 null 이 아닐 때 이벤트 전파를 요청합니다.
<EditForm EditContext="_editContext" OnValidSubmit="Submit" ...
만약 아래와 같이 설정하면, 이벤트는 전파되지 않습니다.
<EditForm EditContext="_editContext" OnSubmit="Submit" ...
Form Data가 도착했을 때, EditForm 이 데이터를 검증하는 전반적인 처리 과정은 아래와 같습니다.
OnValidSubmit == null ? ( OnSubmit == null ? Do nothing : OnSubmit 호출)
: EditContext.EditContext.OnValidationRequested 호출
ValidationMessage 요소가 EditContext 객체를 CasCade 받음.
ValidationMessageStore 에서 EditContext에 해당하는 메시지를 찾음. => 있는 경우만, 메시지 렌더링.
ValidationMessage 요소는 ValidationMessageStore 에서 메시지를 찾기 때문에, 새로운 검증 요청 때마다, 이를 비워주지 않으면, 지난 번 메시지 코드가 보여지게 됩니다.
// 데이터 검증 요청 핸들러
private void HandleValidationRequested(object? sender,
ValidationRequestedEventArgs args)
{
_messageStore?.Clear();
요청 핸들러의 실행 런타임
요청 핸들러의 실행은 블레이저의 호스팅 모델에 따라 달라집니다.
Static SSR/Interactive Server 의 경우 서버에서, Interactive Webassembly 인 경우 클라이언트에서 실행됩니다.
Form Data - Model 바인딩
이는 블레이저가 처리합니다.
블레이저는 호스팅 모델에 따라 아래와 같이 모델을 코드에 전달합니다.
-
Static SSR인 경우,
- Enhance Form 인 경우
blazor.web.js 의 fetch 요청을 통해, - 아닌 경우
Html Form submission을 통해
- Enhance Form 인 경우
-
Interactive Server 인 경우, 웹소켓을 통해,
-
Interactive Webassembly 인 경우, wasm - js interop 을 통해 전달합니다.
참고로, 아주 가끔, 바인딩이 안되는 경우가 있습니다.
이 경우, 도움 안되는 에러 메시지만 보여질 뿐이라, 저도 원인을 찾지는 못했습니다.
아무리 봐도, 코드에 문제가 없어 보인다면, 비주얼 스튜디오를 몇 번 열고 닫으면 해결이 됩니다.
안될 때는 릴리스 모드로 빌드해도 안되니, 그냥 VS를 끄고 닫는 게 좋습니다. 될 때까지.
팁이라면, EditForm 을 통해 할당되는 파라미터는 OnParametersSet 보다는 OnInitialized 에서 할당을 처리하는 게 그나마, 발생확률을 줄이는 것 같습니다.
IStringLocalizer<T>
이 객체는 T.resx 파일을 찾고, 파일이 없거나, 있어도 해당 key 가 존재하지 않으면, key 값을 반환합니다.
예제로 설명하면,
@IStringLocalizer<AppTexts> Texts
<EditForm EditContext="_editContext" OnValidSubmit="Submit" FormName="WeekDaysSelection">
<div>
<label>
<InputCheckbox @bind-Value="Model!.HasMon" /> @Texts["월"]
</label>
...
IStringLocalizer<AppTexts>는 서비스 컨테이너로부터 주입받고, 코드에서 Texts[“월”] 을 호출했는데,
AppTexts.resx 파일이 없거나, 있어도, 이 파일에 key "월"에 해당하는 value 가 없으면, "월"을 반환한다는 의미입니다.
이는 AppTexts.resx 파일이 존재하지 않아도 됨을 의미합니다.
다만, 자리 지킴이 파일인 AppTexts.cs 파일은 있어야 합니다.
public class AppTexts { }
IStringLocalizer<AppTexts> 객체의 모든 키를 한글로만 지정하면, 가상의 AppTexts.resx 는 한글 자원 파일과 같기 때문에, UI를 한글로 빠르게 작성할 수 있습니다.
나중에 영문이 필요한 경우에만, AppTexts.en.resx 파일을 작성하면 됩니다.
다만, 여기 저기 분산된 한글 자원을 찾는 게 손이 많이 가는 일이기는 합니다.