MudBlazor 之 MudForm + FluentValidation 紀錄

為工作紀錄。

參考文件

引言

用 MudForm 開發比較進階的UI操作介面。因為滿麻煩的所以留下紀錄未來參考。

開發環境

  • IDE: Visual Studio 2022

  • 平台: .NET6

  • 骨架: Blaozr Server App

  • CSS Framework: MudBlaozr v6.1.9

  • UI Validator: FluentValidation v11.5.1

紀錄:form data model / DTO - 主檔/明細檔

YourFormModel.cs
using FluentValidation;

public class YourFormData
{
  /// 基本欄位
  public PageSetting info { get; set; } = new();

  /// 附件清單
  public List<PageAttachFile> attachList { get; set; } = new();

  /// 外部參考文件清單
  public List<PageRefLink> referList { get; set; } = new();
}

// inner DTO class definition
public class PageSetting { ... } 
public class PageAttachFile { ... }
public class PageRefLink { ... }

public class YourFormDataValidator : AbstractValidator<YourFormData>
{
  public YourFormDataValidator()
  {
    // 這樣也行 - 檢查群組物件
    //RuleFor(m => m.info.Title).NotEmpty();
    //RuleFor(m => m.info.BannerUrl).NotEmpty();
    //RuleFor(m => m.info.Content).NotEmpty();

    // 檢查群組物件
    RuleFor(m => m.info)
      .SetValidator(new PageSettingValidator());

    // 檢查明細清單
    RuleForEach(m => m.attachList)
      .SetValidator(new PageAttachFileValidator());

    // 檢查明細清單
    RuleForEach(m => m.referList)
      .SetValidator(new PageRefLinkValidator());
  }

  public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
  {
    var result = await ValidateAsync(ValidationContext<YourFormData>.CreateWithOptions((YourFormData)model, x => x.IncludeProperties(propertyName)));
    if (result.IsValid)
      return Array.Empty<string>();
    return result.Errors.Select(e => e.ErrorMessage);
  };
}

public class PageSettingValidator : AbstractValidator<PageSetting>
{
  public PageSettingValidator()
  {
    RuleFor(m => m.Title).NotEmpty();
    RuleFor(m => m.BannerUrl).NotEmpty();
    RuleFor(m => m.Content).NotEmpty();
  }

  public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
  {
    var result = await ValidateAsync(ValidationContext<PageSetting>.CreateWithOptions((PageSetting)model, x => x.IncludeProperties(propertyName)));
    if (result.IsValid)
      return Array.Empty<string>();
    return result.Errors.Select(e => e.ErrorMessage);
  };
}

public class PageAttachFileValidator : AbstractValidator<PageAttachFile>
{
  public PageAttachFileValidator()
  {
    RuleFor(m => m.FileDesc).NotEmpty();

    When(c => String.IsNullOrWhiteSpace(c.Pdf) && String.IsNullOrWhiteSpace(c.Odt) && String.IsNullOrWhiteSpace(c.Doc), () =>
    {
      RuleFor(m => m.Pdf).Must(m => false).WithMessage("至少指定一份附件。");
      RuleFor(m => m.Odt).Must(m => false).WithMessage("至少指定一份附件。");
      RuleFor(m => m.Doc).Must(m => false).WithMessage("至少指定一份附件。");
    });
  }

  public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
  {
    var result = await ValidateAsync(ValidationContext<PageAttachFile>.CreateWithOptions((PageAttachFile)model, x => x.IncludeProperties(propertyName)));
    if (result.IsValid)
      return Array.Empty<string>();
    return result.Errors.Select(e => e.ErrorMessage);
  };
}

public class PageRefLinkValidator : AbstractValidator<PageRefLink>
{
  public PageRefLinkValidator()
  {
    RuleFor(m => m.RefDesc).NotEmpty();
    RuleFor(m => m.RefUrl).NotEmpty();
  }

  public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
  {
    var result = await ValidateAsync(ValidationContext<PageRefLink>.CreateWithOptions((PageRefLink)model, x => x.IncludeProperties(propertyName)));
    if (result.IsValid)
      return Array.Empty<string>();
    return result.Errors.Select(e => e.ErrorMessage);
  };
}

紀錄: 前端 Blazor Component

YourFormUI.razor
@inject IDialogServiceEx dlgSvc
@inject YourFormBiz bizSvc

<CustomOverlay Visible=f_loading />
<MudContainer>

  <MudForm @ref=refForm Model=@formData Validation=@(formDataValidator.ValidateValue)>
    <MudField Label="文章識別碼">@formData.info.PageID</MudField>
    
    @* 這樣也行 - 群組物件UI
    <MudTextField Label="文章抬頭" @bind-Value=@formData.info.Title For="()=>formData.info.Title" />
    <MudTextField Label="橫幅圖片" @bind-Value=@formData.info.BannerUrl For="()=>formData.info.BannerUrl" />
    <MudTextField Label="文字段落" @bind-Value=@formData.info.Content For="()=>formData.info.Content" />
    *@
    
    @* 群組物件UI *@
    <MudForm Model=@formInfo Validation=@(infoValidator.ValidateValue)>
      <MudTextField Label="文章抬頭" @bind-Value=@formInfo.Title For="()=>formInfo.Title" />
      <MudTextField Label="橫幅圖片" @bind-Value=@formInfo.BannerUrl For="()=>formInfo.BannerUrl" />
      <MudTextField Label="文字段落" @bind-Value=@formInfo.Content For="()=>formInfo.Content" />
    </MudForm>

    <MudDivider Class="my-2" />

    @* 明細清單UI *@
    <MudTable Items=@formData.attachList Hover Dense=false Elevation="1" Context="attach">
      <HeaderContent>
        <MudTh>FileDesc</MudTh>
        <MudTh>Pdf</MudTh>
        <MudTh>Odt</MudTh>
        <MudTh>Doc</MudTh>
      </HeaderContent>
      <RowTemplate>
        <MudTd DataLabel="FileDesc">
          <MudForm Model=@attach Validation=@(attachValidator.ValidateValue)>
            <MudTextField Label="FileDesc"
                          @bind-Value=@attach.FileDesc
                          For="() => attach.FileDesc" />
          </MudForm>
        </MudTd>
        <MudTd DataLabel="Pdf">
          <MudForm Model=@attach>
            <MudTextField Label="Pdf"
                          @bind-Value=@attach.Pdf
                          Validation=@(attachValidator.ValidateValue)
                          For="(() => attach.Pdf)" />
          </MudForm>
        </MudTd>
        <MudTd DataLabel="Odt">
          <MudForm Model=@attach>
            <MudTextField Label="Odt"
                          @bind-Value=@attach.Odt
                          Validation=@(attachValidator.ValidateValue)
                          For="(() => attach.Odt)" />
          </MudForm>
        </MudTd>
        <MudTd DataLabel="Doc">
          <MudForm Model=@attach>
            <MudTextField Label="Doc"
                          @bind-Value=@attach.Doc
                          Validation=@(attachValidator.ValidateValue)
                          For="(() => attach.Doc)" />
          </MudForm>
        </MudTd>
      </RowTemplate>
    </MudTable>

    <MudDivider Class="my-2" />

    @* 另一個明細清單UI *@
    <MudTable Items=@formData.referList Hover Dense=false Elevation="1" Context="refer">
      <HeaderContent>
        <MudTh>RefDesc</MudTh>
        <MudTh>RefUrl</MudTh>
      </HeaderContent>
      <RowTemplate>
        <MudTd DataLabel="RefDesc">
          <MudForm Model=@refer Validation=@(referValidator.ValidateValue)>
            <MudTextField Label="RefDesc"
                          @bind-Value=@refer.RefDesc
                          For="() => refer.RefDesc" />
          </MudForm>
        </MudTd>
        <MudTd DataLabel="RefUrl">
          <MudForm Model=@refer Validation=@(referValidator.ValidateValue)>
            <MudTextField Label="RefUrl"
                          @bind-Value=@refer.RefUrl
                          For="() => refer.RefUrl" />
          </MudForm>
        </MudTd>
      </RowTemplate>
    </MudTable>

  </MudForm>
  <MudStack Row Class="my-2">
    <MudButton OnClick=HandleSubmit>送出</MudButton>
  </MudStack>
</MudContainer>

@code {
  [CascadingParameter] _YourFuncStateStore BizState { get; set; }

  //## State
  bool f_loading = false;
  YoutFormData formData { get => BizState.formData; set => BizState.formData = value; }
  PageSetting formInfo { get => BizState.formData.info; set => BizState.formData.info = value; }

  MudForm? refForm;
  YoutFormDataValidator formDataValidator = new();
  PageSettingValidator infoValidator = new();
  PageAttachFileValidator attachValidator = new();
  PageRefLinkValidator referValidator = new();

  protected override async Task OnInitializedAsync()
  {
    await base.OnInitializedAsync();
    f_loading = true;
    formData = await Task.Run(() => bizSvc.GetFormData(BizState.dataAim));
    f_loading = false;
  }

  async Task HandleSubmit()
  {
    try
    {
      //# 基本檢查
      await refForm.Validate();
      if (!refForm.IsValid) return;

      //# GO
      f_loading = true;
      ...省略...
    }
    catch (Exception ex)
    {
      dlgSvc.ShowAlert(ex.Message, "更新失敗!");
    }
    finally
    {
      f_loading = false;
    }
  }
}

Last updated