WPFのMVVMとレイヤード構成で、FluentValidationを「入力検証の中心」に据えると、次に必ずぶつかるのが「DB参照が必要な重複チェック(ISBNなど)」と「エラー表示(MessageBoxか項目エラーか)」です。本記事では、入力検証と業務ルールを分離しつつ、検証結果・業務エラー・例外を崩さずにViewModelへ渡すための設計を、具体的なResult設計とWPFの表示パターンまで含めて整理します。
WPF(MVVM)+レイヤード構成で検証が難しくなる理由
WPFのMVVMは、画面(View)と状態・操作(ViewModel)を分離しやすい一方で、「検証」と「エラー表示」をどこに置くかが曖昧だと、すぐに次のような歪みが起きます。
- FluentValidationにDB参照(重複チェック)まで書き始めて、Validatorが肥大化する
- サービス層がMessageBoxを呼び出してしまい、UI依存が入りレイヤー分離が崩れる
- 入力不備・業務エラー・例外が混ざって、ViewModel側でif/try-catchが散らばる
- 項目エラーを出したいのに、MessageBox連発になりUXが悪化する
結論から言うと、破綻しないコツは「入力検証」と「業務ルール検証(DBが絡む)」を分けて扱い、結果は“同じ形”でViewModelへ返すことです。
まず整理:どこまでが入力検証で、どこからが業務ルールか
FluentValidationを使うと「検証が書きやすい」ので、何でもValidatorに寄せたくなります。ですが、レイヤード構成で長期運用するなら、境界線を先に引くほうが安全です。
| 分類 | 目的 | 具体例(ISBNの場合) | 主な置き場所 |
|---|---|---|---|
| 入力検証(Validation) | ユーザー入力が「扱える形」か確認 | 必須、桁数、ハイフン除去後の形式、数字チェック | FluentValidation(Validator) |
| 業務ルール検証(Business Rule) | 業務として「許可される状態」か確認 | 同一ISBNは登録不可、同一タイトル+版は不可、廃止済みISBNは不可等 | UseCase / Service(DB参照含む) |
| システム例外(Exception) | 予期せぬ失敗(環境・障害) | DB接続断、タイムアウト、想定外Null、IO失敗 | 境界(UseCase)で捕捉+ログ+Result化/UIは統一表示 |
ここで重要なのは、ISBN重複チェックは「入力が正しいか」ではなく「その操作が業務上許可されるか」なので、基本はサービス層(UseCase)に置く、という考え方です。
レイヤー別の責務を固定すると、迷いが減る
MVVM+レイヤードでよくある構成を、検証・エラーの観点で整理します。
| レイヤー | 主な責務 | 検証・エラーの役割 | やらないこと |
|---|---|---|---|
| UI(View / ViewModel) | 入力と表示、ユーザー操作 | 項目エラー表示、ダイアログ表示、結果に応じた画面遷移 | DB参照、業務ルール判定、例外の握りつぶし |
| アプリ層(UseCase / Application Service) | 処理の流れ(オーケストレーション) | 入力検証+業務チェックの実行、Resultで統一して返す、境界で例外を包む | MessageBoxなどUI表示、WPF依存 |
| ドメイン層 | エンティティ、値オブジェクト、ドメイン規則 | 純粋に表現できる規則(状態遷移、計算、値の不変条件) | DBアクセス、WPF依存、インフラ依存 |
| インフラ層(Repository等) | DB/外部I/O | データ取得・永続化、例外は基本スロー(上位で扱う) | 画面表示、業務判断の最終決定 |
この表の通りに役割を決めると、質問で挙がっている悩みは次の形で解消できます。
- DB参照が必要な重複チェック:UseCase/Service(業務ルール)へ
- MessageBox等の表示:ViewModel(UI層)で、ダイアログサービス経由
- 入力不備・業務エラー・例外:Resultオブジェクトで統一してViewModelへ
- try-catch:境界(UseCase)+アプリ全体(WPFのグローバル例外)で拾う
「DB参照チェックをValidatorに書く」ことの是非
FluentValidationには MustAsync があり、次のように書けます。これ自体は可能です。
RuleFor(x => x.Isbn)
.MustAsync(async (isbn, ct) => !await repo.ExistsIsbnAsync(isbn, ct))
.WithMessage("ISBNは既に登録されています");
ただし、レイヤード構成で運用するなら、次の論点で慎重に判断するのが現実的です。
| 観点 | ValidatorにDB参照を入れる | Service/UseCaseにDB参照を置く |
|---|---|---|
| 責務の純度 | 入力検証と業務ルールが混ざりやすい | 入力検証(形式)と業務検証(許可)を分離できる |
| 依存関係 | ValidatorがRepositoryに依存しがち(設計次第で複雑) | UseCaseがRepositoryを呼ぶのは自然 |
| 実行タイミング | 入力のたびに呼ばれるとDB負荷が増える危険 | 「保存時だけチェック」など制御しやすい |
| テスト | DBモック前提になり、Validator単体テストが重くなりがち | Validatorは純粋に、UseCaseは業務としてテストしやすい |
| 整合性 | 「検証の時点」と「保存の時点」で競合が起き得る(同時登録など) | 保存直前のチェック+DB制約と併用しやすい |
おすすめは、Validatorは「純粋な入力検証」に寄せ、DB参照が絡むものはUseCase/Serviceへです。どうしてもValidatorに入れる場合は、Validatorが直接インフラ実装に触れないように(インターフェース=ポートに依存する、入力中に走らせない、保存時のみ実行する等)工夫が必要です。
解決の中心は「結果オブジェクトの統一」
入力検証(FluentValidation)と業務チェック(DB重複)を分けたとしても、ViewModelが受け取る情報がバラバラだと結局つらくなります。そこで、失敗の種類に関係なく、同じ形(Result)で返すのが強力です。
ポイントは次の3つです。
- 項目単位のエラー(フィールドエラー)を持てる
- 画面全体に出すべきエラー(グローバルエラー)も持てる
- 「入力不備 / 業務エラー / 例外」を区別できる(ログや表示方針が決まる)
ResultとErrorの例(複数エラー前提)
public enum ErrorType
{
Validation, // 入力不備(項目単位になりやすい)
Business, // 業務ルール違反(重複など)
Conflict, // 競合(ユニーク制約など)
NotFound,
Unexpected // 予期しない例外
}
public sealed record Error(
ErrorType Type,
string Code,
string Message,
string? Field = null,
Exception? Exception = null
);
public sealed class Result
{
public bool IsSuccess { get; }
public IReadOnlyList Errors { get; }
private Result(bool isSuccess, IReadOnlyList<Error> errors)
{
IsSuccess = isSuccess;
Errors = errors;
}
public static Result Ok() => new(true, Array.Empty<Error>());
public static Result Fail(params Error[] errors) => new(false, errors);
public static Result Fail(IEnumerable<Error> errors) => new(false, errors.ToList());
}
public sealed class Result : Result
{
public T? Value { get; }
private Result(bool isSuccess, T? value, IReadOnlyList<Error> errors)
: base(isSuccess, errors)
{
Value = value;
}
public static Result<T> Ok(T value) => new(true, value, Array.Empty<Error>());
public static new Result<T> Fail(IEnumerable<Error> errors) => new(false, default, errors.ToList());
}
この形にしておくと、FluentValidationの結果も、DB重複チェックの結果も、例外も、すべて List<Error> に積めます。ViewModelは「Errorsをどう見せるか」だけを考えればよくなります。
ValidationServiceで「FluentValidation結果」と「業務チェック結果」を合流する
質問の前提にある ValidationService は、アプリ層(UseCaseの一部または補助)として非常に相性が良いです。役割は「複数の検証ソースを束ね、UIが扱いやすいResultを返す」ことに固定します。
入力モデル(例:書籍登録)
public sealed class BookEditModel
{
public string Title { get; set; } = "";
public string Isbn { get; set; } = "";
}
FluentValidation(純粋な入力検証に寄せる)
using FluentValidation;
public sealed class BookEditModelValidator : AbstractValidator
{
public BookEditModelValidator()
{
RuleFor(x => x.Title)
.NotEmpty().WithMessage("タイトルは必須です")
.MaximumLength(200).WithMessage("タイトルは200文字以内です");
RuleFor(x => x.Isbn)
.NotEmpty().WithMessage("ISBNは必須です")
.Must(isbn => Normalize(isbn).Length is 10 or 13)
.WithMessage("ISBNは10桁または13桁で入力してください")
.Must(isbn => Normalize(isbn).All(char.IsDigit))
.WithMessage("ISBNは数字のみで入力してください(ハイフンは可)");
}
private static string Normalize(string isbn)
=> (isbn ?? "").Replace("-", "").Trim();
}
ここでは「DBに存在するか」は扱いません。あくまで入力の形だけを保証します。
業務チェック(重複)用のサービス
重複チェックはUseCase/Serviceで行い、Repository経由でDBを参照します。インターフェースはアプリ層(またはドメイン境界)に置くと依存が綺麗になります。
public interface IBookRepository
{
Task<bool> ExistsIsbnAsync(string normalizedIsbn, CancellationToken ct);
Task AddAsync(Book entity, CancellationToken ct);
}
public sealed class BookService
{
private readonly IBookRepository _repo;
public BookService(IBookRepository repo)
{
_repo = repo;
}
public async Task<Result> CheckIsbnUniqueAsync(string isbn, CancellationToken ct)
{
var normalized = (isbn ?? "").Replace("-", "").Trim();
if (await _repo.ExistsIsbnAsync(normalized, ct))
{
return Result.Fail(new Error(
Type: ErrorType.Business,
Code: "ISBN_DUPLICATE",
Message: "このISBNは既に登録されています",
Field: nameof(BookEditModel.Isbn)
));
}
return Result.Ok();
}
}
ここで返しているのは例外ではなくResultです。「期待される失敗(重複)」はResultで返すほうが、MVVMの画面処理が安定します。
ValidationService(合流点)
using FluentValidation;
using FluentValidation.Results;
public sealed class ValidationService
{
private readonly IValidator _validator;
private readonly BookService _bookService;
public ValidationService(IValidator<BookEditModel> validator, BookService bookService)
{
_validator = validator;
_bookService = bookService;
}
public async Task<Result> ValidateForSaveAsync(BookEditModel model, CancellationToken ct)
{
var errors = new List<Error>();
// 1) 入力検証(同期/非同期どちらでもOK)
ValidationResult fv = await _validator.ValidateAsync(model, ct);
if (!fv.IsValid)
{
errors.AddRange(fv.Errors.Select(e =>
new Error(
Type: ErrorType.Validation,
Code: "INPUT_INVALID",
Message: e.ErrorMessage,
Field: e.PropertyName
)));
}
// 入力検証でISBNがそもそも空/不正なら、DB重複チェックはスキップする(無駄なDB負荷回避)
bool isbnLooksValid = !errors.Any(x => x.Field == nameof(BookEditModel.Isbn));
if (isbnLooksValid)
{
// 2) 業務ルール(DB参照)
Result isbnResult = await _bookService.CheckIsbnUniqueAsync(model.Isbn, ct);
if (!isbnResult.IsSuccess)
errors.AddRange(isbnResult.Errors);
}
return errors.Count == 0 ? Result.Ok() : Result.Fail(errors);
}
}
ポイントは、DBチェックを「入力が整ってから」にしていることです。これだけで「入力中にDBへ問い合わせが走って重い」「未入力なのに“重複”と言われる」などの事故が減ります。
保存処理(UseCase)で最終的に守るべきこと
重複チェックをサービス層に置いても、同時登録(別端末や別画面)で「チェック後に重複が発生する」ことは起こり得ます。したがって、実運用では次をセットで考えるのが鉄板です。
- 保存前の業務チェック(ユーザーに優しいエラー)
- DB側のユニーク制約(最終防衛線)
- ユニーク制約違反例外を、ConflictとしてResult化(表示を綺麗にする)
UseCase(アプリ層)の例です。
public sealed class BookUseCase
{
private readonly ValidationService _validation;
private readonly IBookRepository _repo;
public BookUseCase(ValidationService validation, IBookRepository repo)
{
_validation = validation;
_repo = repo;
}
public async Task<Result> CreateAsync(BookEditModel model, CancellationToken ct)
{
// 期待される失敗はResultで返す
Result v = await _validation.ValidateForSaveAsync(model, ct);
if (!v.IsSuccess) return v;
try
{
var normalized = model.Isbn.Replace("-", "").Trim();
var entity = new Book(title: model.Title, normalizedIsbn: normalized);
await _repo.AddAsync(entity, ct);
return Result.Ok();
}
catch (Exception ex)
{
// ここでは例として一括でUnexpectedにしている。
// 実務ではDbのユニーク制約例外を判別しConflictへ変換するとUXがさらに良くなる。
return Result.Fail(new Error(
Type: ErrorType.Unexpected,
Code: "UNEXPECTED",
Message: "保存に失敗しました。時間をおいて再度お試しください。",
Exception: ex
));
}
}
}
ここが「try-catchを置くならどこか?」の実務的な答えになります。境界(UseCase/Serviceの公開メソッド)でまとめて拾い、UIへはResultとして返す。ViewModelが例外処理を抱え込まなくて済みます。
MessageBoxはどこで呼ぶべきか:結論はViewModel(UI層)
サービス層が MessageBox.Show を呼び始めると、次の問題が出ます。
- サービス層がWPF(UI)に依存して、テストしにくい
- 将来、UIを変えたい(WinUI/Web)となったとき総崩れする
- 通知方法(ダイアログ/トースト/画面内表示)が固定され、UX改善が難しくなる
そこで、UI側に IDialogService / IMessageService を置き、ViewModelが呼び出す形にすると綺麗です。
public interface IDialogService
{
void ShowError(string message, string? title = null);
void ShowInfo(string message, string? title = null);
}
実装(WPF側)はMessageBoxでも、独自ダイアログでも構いません。重要なのは「サービス層からUIを呼ばない」ことです。
WPFでのエラー表示:項目エラーと全体エラーを分ける
「重複です」をMessageBoxで出すべきか、TextBoxの下に出すべきかは、ケースで変わります。判断をルール化すると迷いが減ります。
| エラーの性質 | 表示のおすすめ | 例 |
|---|---|---|
| 項目に紐づく(直せば解決) | 項目エラー(赤枠・下部メッセージ) | 必須、形式、ISBN重複、文字数 |
| 画面全体に影響(ユーザー操作では直せない) | ダイアログ+画面上部の通知 | DB接続不可、サーバ障害、権限不足 |
| 致命的だが復旧可能 | ダイアログ(1回)+ログ | 保存時にタイムアウト、予期しない例外 |
ISBN重複は「Isbnという項目に紐づく」ので、基本は項目エラーとして出すのが自然です。MessageBoxは多用するとユーザー体験が落ちやすいため、「操作で直せない」「致命的」なものに寄せると安定します。
ViewModel側:INotifyDataErrorInfoで項目エラーを表示する
WPFで項目単位のエラー表示を綺麗にやるなら、INotifyDataErrorInfo が定番です。ResultのErrorsを、フィールドごとのエラー辞書に変換して保持します。
using System.Collections;
using System.ComponentModel;
public abstract class ValidatableViewModel : INotifyDataErrorInfo, INotifyPropertyChanged
{
private readonly Dictionary<string, List<string>> _errors = new();
public bool HasErrors => _errors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public event PropertyChangedEventHandler? PropertyChanged;
public IEnumerable GetErrors(string? propertyName)
{
if (string.IsNullOrEmpty(propertyName))
return _errors.SelectMany(kv => kv.Value);
return _errors.TryGetValue(propertyName, out var list) ? list : Enumerable.Empty<string>();
}
protected void ClearErrors()
{
var keys = _errors.Keys.ToList();
_errors.Clear();
foreach (var key in keys)
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(key));
OnPropertyChanged(nameof(HasErrors));
}
protected void ApplyErrorsFromResult(Result result)
{
ClearErrors();
foreach (var e in result.Errors.Where(x => !string.IsNullOrEmpty(x.Field)))
{
var field = e.Field!;
if (!_errors.TryGetValue(field, out var list))
_errors[field] = list = new List<string>();
list.Add(e.Message);
}
foreach (var key in _errors.Keys.ToList())
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(key));
OnPropertyChanged(nameof(HasErrors));
}
protected void OnPropertyChanged(string name)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
View側のBindingは、例えば次のようにします。
<TextBox
Text="{Binding Isbn, UpdateSourceTrigger=PropertyChanged}"
ValidatesOnNotifyDataErrors="True" />
ViewModel:保存コマンドでResultを受け取り、表示方針で振り分ける
public sealed class BookEditViewModel : ValidatableViewModel
{
private readonly BookUseCase _useCase;
private readonly IDialogService _dialog;
public string Title { get; set; } = "";
public string Isbn { get; set; } = "";
public BookEditViewModel(BookUseCase useCase, IDialogService dialog)
{
_useCase = useCase;
_dialog = dialog;
}
public async Task SaveAsync(CancellationToken ct)
{
// 画面入力をモデルへ(ここでは簡略化)
var model = new BookEditModel { Title = this.Title, Isbn = this.Isbn };
Result result = await _useCase.CreateAsync(model, ct);
if (result.IsSuccess)
{
ClearErrors();
_dialog.ShowInfo("保存しました");
return;
}
// 項目エラーとして出せるものはINotifyDataErrorInfoへ
ApplyErrorsFromResult(result);
// 画面全体エラー(Fieldなし、またはUnexpected)はダイアログ
var global = result.Errors
.Where(e => string.IsNullOrEmpty(e.Field) || e.Type == ErrorType.Unexpected)
.Select(e => e.Message)
.Distinct()
.ToList();
if (global.Count > 0)
_dialog.ShowError(string.Join(Environment.NewLine, global));
}
}
これで、ISBN重複はTextBoxに紐づくエラーとして表示でき、DB接続障害などはMessageBoxで1回だけ表示、という住み分けができます。
UIブロックを避ける:DBチェックは非同期化し、呼び出し頻度を制御する
DB重複チェックを「入力のたび」に走らせると、UIが重くなりがちです。設計としては次のどれかに寄せるのが実用的です。
- 保存ボタン押下時だけ:最もシンプルで事故が少ない
- フォーカスアウト時(LostFocus)だけ:入力体験は良いが実装が少し増える
- デバウンス(一定時間入力が止まったら):高級だが実装コストが増える
WPFのMVVMでは、非同期コマンド(例:CommunityToolkit.MvvmのAsyncRelayCommand)を使うとUIブロックを避けやすいです。採用ライブラリは自由ですが、重要なのは「DB参照をUIスレッドで同期実行しない」ことです。
検証エラー・業務エラー・例外をレイヤー分離を崩さずに渡す方法
ここまでの設計をまとめると、ViewModelに渡す情報は次の1本に統一できます。
- 成功:
Result.Ok() - 失敗:
Result.Fail(List<Error>)(Validation / Business / Unexpectedなどを内包)
ViewModelはそのResultを「項目エラーにするか」「ダイアログにするか」だけ判断します。レイヤー分離を崩さずに渡すとは、言い換えると「UIで扱いたい形に整える責任をアプリ層に寄せる」ことです。
Resultの運用ルール例
| ケース | 返し方 | ログ | UI表示 |
|---|---|---|---|
| 入力不備 | ErrorType.Validation(Fieldあり) | 通常不要 | 項目エラー |
| ISBN重複 | ErrorType.Business(Fieldあり) | 通常不要(必要なら監査) | 項目エラー(必要なら補助メッセージ) |
| ユニーク制約違反(保存時) | ErrorType.Conflict(Fieldあり/なし) | 必要に応じて | 項目エラー or ダイアログ |
| DB接続不可・タイムアウト | ErrorType.Unexpected(Exception付き) | 必須 | ダイアログ(1回) |
「例外はResultに詰める」設計にする場合、例外オブジェクト自体はUIへ見せない(ユーザーには一般メッセージ)、ただしログには詳細を残す、という運用が安定します。
try-catchはどこに置くべきか:境界とグローバルの二段構え
try-catchの置き場所は、実務では「全部で2箇所」と割り切ると破綻しにくいです。
- アプリ層(UseCase/Service)の公開メソッド:インフラ例外を捕捉しResult化、ログを残す
- アプリ全体(WPFのグローバル例外ハンドラ):漏れた例外を統一ダイアログ+安全終了(または継続)
ViewModelのtry-catchは、原則として「Resultで戻す設計なら最小限」で十分です。例外処理ロジックがViewModelに散らばるほど、保守が難しくなります。
WPFアプリ全体の一般的な例外を拾う方法(グローバルハンドリング)
WPFは例外の発生元によって捕捉できるイベントが異なります。UIスレッド・バックグラウンド・未観測タスクなど、複数の入り口を押さえておくのが現実的です。
| 対象 | 主な入り口 | 拾えるもの |
|---|---|---|
| UIスレッド | Application.DispatcherUnhandledException | UIスレッドで未処理の例外 |
| バックグラウンド | AppDomain.CurrentDomain.UnhandledException | 最終的に未処理の例外(注意:継続困難な場合あり) |
| 未観測Task | TaskScheduler.UnobservedTaskException | awaitされずにGCで観測された例外 |
App.xaml.cs(UI層)での例です。ここでは「ユーザーには統一メッセージ」「内部はログ」という方針に寄せます。
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
this.DispatcherUnhandledException += (s, args) =>
{
// ログにargs.Exception
MessageBox.Show("予期しないエラーが発生しました。アプリを再起動してください。", "エラー");
args.Handled = true; // 継続可能な場合のみ
};
AppDomain.CurrentDomain.UnhandledException += (s, args) =>
{
// ログに((Exception)args.ExceptionObject)
// ここは継続が難しいケースが多い
};
TaskScheduler.UnobservedTaskException += (s, args) =>
{
// ログにargs.Exception
args.SetObserved();
};
}
}
補足として、グローバルで拾えるからといって「UseCaseでのtry-catchを省略」すると、ユーザーに返す文脈(どの操作で失敗したか、どの項目が原因か)が失われます。グローバルは最後のセーフティネットとして考えるのが安全です。
実装を安定させる細かいコツ
DB重複チェックは「いつ呼ぶか」を決めてから実装する
重複チェックをValidatorに入れるかサービスに入れるか以上に、実務で効くのが「呼び出し頻度」です。おすすめは次の順です。
- 基本:保存時(OK/NGが明確で、DB負荷が読みやすい)
- UX改善:ISBN欄のフォーカスアウト時に事前チェック(任意)
- 高度:デバウンス(入力が止まったら)+キャンセル(CancellationToken)
「エラーコード」を入れておくと将来の拡張が楽
エラーメッセージを文字列だけで返すと、表示の差し替え(文言統一、多言語化、ログ集計)が難しくなります。Error.Code を入れておくと、例えば次のことができます。
- 同じエラーの重複表示を抑制(CodeでDistinct)
- UI側でCodeごとに表示スタイルを変える
- ログ集計(どのエラーが多いか)
「DB制約エラー」は業務エラーに変換してユーザーに返す
同時登録などで最終的にユニーク制約で落ちるケースは必ずあります。ここを「予期しないエラー」にしないために、DB例外の判別(プロバイダごとの例外型/エラーコード)を行い、ErrorType.Conflict や ErrorType.Business に変換すると、ユーザーにとって納得感がある表示になります。
この設計にすると何が良くなるか
- Validatorは入力検証に集中でき、読みやすくテストしやすい
- DB参照が必要な重複チェックはサービス層で完結し、呼び出し頻度も制御できる
- ViewModelはResultを表示に変換するだけになり、例外処理が散らばらない
- 項目エラーとダイアログを使い分けでき、UXが安定する
- グローバル例外も拾えて、最悪のクラッシュを減らせる
実務向けのチェックリスト
- FluentValidationには「必須・形式・桁数」などの入力検証だけを入れているか
- DB参照が必要な検証はUseCase/Serviceで行い、保存時に必ず再確認しているか
- 検証結果・業務エラー・例外はResultで統一してViewModelへ返しているか
- 項目エラーはINotifyDataErrorInfo、致命的エラーはダイアログ、という方針があるか
- UseCase境界で例外を捕捉してResult化し、ログも残しているか
- WPF全体でDispatcherUnhandledException等のグローバル例外ハンドラを設定しているか
「FluentValidationに寄せるか、サービスに寄せるか」で悩んだときは、入力の形を保証するものはValidator、業務として許可されるか(DB参照が必要)はServiceという線引きを基準にし、最後はResultで統一してUIへ返す。これがWPF(MVVM)+レイヤード構成で最も破綻しにくい道筋です。

コメント