Foolproof

バリデーションライブラリC#MVC条件付きバリデーション属性クライアントサイド検証

ライブラリ

Foolproof

概要

FoolproofはASP.NET MVC向けの条件付きバリデーションライブラリです。標準のData Annotationsでは実現困難な条件付きバリデーションを属性ベースで簡単に実装できます。RequiredIf、RequiredIfNot、RequiredIfTrue、RequiredIfFalseなどの豊富な条件付きバリデーション属性を提供し、クライアントサイド検証も自動的に生成します。複雑なビジネスルールを宣言的に表現でき、フォームの動的な検証要件に対応できます。

詳細

Foolproofは、ASP.NET MVCにおいて条件付きバリデーションを実現するための専用ライブラリです。他のフィールドの値に基づいて動的にバリデーションルールを適用でき、複雑なフォームロジックを属性レベルで定義できます。クライアントサイド検証の自動生成により、サーバーサイドとクライアントサイドで一貫した検証を実現します。jQuery Unobtrusive Validationと連携し、リアルタイムでのユーザー体験を向上させます。RequiredIfEmpty、RequiredIfNotEmpty、RequiredIfRegExMatchなどの多様な条件パターンをサポートします。

主な特徴

  • 条件付きバリデーション: 他のフィールドの値に基づく動的検証
  • 豊富な条件パターン: True/False、正規表現、空値チェックなど
  • クライアントサイド対応: 自動的なJavaScript検証生成
  • MVC統合: ASP.NET MVCとの完全な統合
  • 宣言的構文: 属性による直感的な条件定義
  • 既存コード互換: Data Annotationsとの共存が可能

メリット・デメリット

メリット

  • 複雑な条件付きバリデーションを簡潔に記述
  • クライアントサイド検証の自動生成
  • 属性ベースの宣言的プログラミング
  • ASP.NET MVCとの深い統合
  • 豊富な条件パターンの提供
  • 既存のData Annotationsとの互換性

デメリット

  • ASP.NET MVCに依存(他のフレームワークでは使用不可)
  • 複雑すぎる条件では可読性が低下
  • 動的なバリデーションルールの実装が困難
  • Entity Frameworkとの統合に制約
  • 現在のメンテナンス状況が不明確
  • 最新の.NET Coreバージョンへの対応状況

参考ページ

書き方の例

基本的な条件付きバリデーション

using System;
using System.ComponentModel.DataAnnotations;
using Foolproof;

// 基本的な条件付きバリデーション
public class UserRegistrationModel
{
    [Required(ErrorMessage = "名前は必須項目です")]
    [Display(Name = "名前")]
    public string Name { get; set; }

    [Required(ErrorMessage = "メールアドレスは必須項目です")]
    [EmailAddress(ErrorMessage = "有効なメールアドレスを入力してください")]
    [Display(Name = "メールアドレス")]
    public string Email { get; set; }

    [Display(Name = "結婚している")]
    public bool IsMarried { get; set; }

    // 結婚している場合のみ、配偶者の名前が必須
    [RequiredIfTrue("IsMarried", ErrorMessage = "結婚している場合は配偶者の名前を入力してください")]
    [Display(Name = "配偶者の名前")]
    public string SpouseName { get; set; }

    [Display(Name = "年齢")]
    [Range(0, 120, ErrorMessage = "年齢は0歳から120歳の間で入力してください")]
    public int Age { get; set; }

    // 18歳未満の場合、保護者の同意が必要
    [RequiredIf("Age", Operator.LessThan, 18, ErrorMessage = "18歳未満の場合は保護者の同意が必要です")]
    [Display(Name = "保護者の同意")]
    public bool ParentalConsent { get; set; }

    [Display(Name = "運転免許証を持っている")]
    public bool HasDriverLicense { get; set; }

    // 運転免許証を持っている場合、免許証番号が必須
    [RequiredIfTrue("HasDriverLicense", ErrorMessage = "運転免許証を持っている場合は免許証番号を入力してください")]
    [Display(Name = "免許証番号")]
    public string DriverLicenseNumber { get; set; }

    [Display(Name = "住所タイプ")]
    public string AddressType { get; set; }

    // 住所タイプが「自宅」の場合、住所が必須
    [RequiredIf("AddressType", "自宅", ErrorMessage = "住所タイプが「自宅」の場合は住所を入力してください")]
    [Display(Name = "住所")]
    public string Address { get; set; }

    [Display(Name = "電話番号タイプ")]
    public string PhoneType { get; set; }

    // 電話番号タイプが空でない場合、電話番号が必須
    [RequiredIfNotEmpty("PhoneType", ErrorMessage = "電話番号タイプを選択した場合は電話番号を入力してください")]
    [Display(Name = "電話番号")]
    public string PhoneNumber { get; set; }

    [Display(Name = "国籍")]
    public string Country { get; set; }

    // 国籍が「日本」でない場合、ビザ情報が必須
    [RequiredIfNot("Country", "日本", ErrorMessage = "日本国籍でない場合はビザ情報を入力してください")]
    [Display(Name = "ビザ情報")]
    public string VisaInfo { get; set; }
}

// 雇用情報モデル
public class EmploymentModel
{
    [Required(ErrorMessage = "名前は必須項目です")]
    [Display(Name = "名前")]
    public string Name { get; set; }

    [Display(Name = "雇用状況")]
    public string EmploymentStatus { get; set; }

    // 雇用状況が「正社員」の場合、会社名が必須
    [RequiredIf("EmploymentStatus", "正社員", ErrorMessage = "正社員の場合は会社名を入力してください")]
    [Display(Name = "会社名")]
    public string CompanyName { get; set; }

    // 雇用状況が「正社員」の場合、年収が必須
    [RequiredIf("EmploymentStatus", "正社員", ErrorMessage = "正社員の場合は年収を入力してください")]
    [Display(Name = "年収")]
    public decimal? AnnualSalary { get; set; }

    // 雇用状況が「学生」の場合、学校名が必須
    [RequiredIf("EmploymentStatus", "学生", ErrorMessage = "学生の場合は学校名を入力してください")]
    [Display(Name = "学校名")]
    public string SchoolName { get; set; }

    // 雇用状況が「退職」の場合、退職理由が必須
    [RequiredIf("EmploymentStatus", "退職", ErrorMessage = "退職の場合は退職理由を入力してください")]
    [Display(Name = "退職理由")]
    public string RetirementReason { get; set; }

    [Display(Name = "副業をしている")]
    public bool HasSideJob { get; set; }

    // 副業をしている場合、副業内容が必須
    [RequiredIfTrue("HasSideJob", ErrorMessage = "副業をしている場合は副業内容を入力してください")]
    [Display(Name = "副業内容")]
    public string SideJobDescription { get; set; }

    [Display(Name = "リモートワーク希望")]
    public bool WantsRemoteWork { get; set; }

    // リモートワークを希望する場合、在宅勤務環境の説明が必須
    [RequiredIfTrue("WantsRemoteWork", ErrorMessage = "リモートワークを希望する場合は在宅勤務環境を説明してください")]
    [Display(Name = "在宅勤務環境")]
    public string RemoteWorkEnvironment { get; set; }
}

// 注文情報モデル
public class OrderModel
{
    [Required(ErrorMessage = "顧客名は必須項目です")]
    [Display(Name = "顧客名")]
    public string CustomerName { get; set; }

    [Display(Name = "配送方法")]
    public string DeliveryMethod { get; set; }

    // 配送方法が「宅配」の場合、配送先住所が必須
    [RequiredIf("DeliveryMethod", "宅配", ErrorMessage = "宅配の場合は配送先住所を入力してください")]
    [Display(Name = "配送先住所")]
    public string DeliveryAddress { get; set; }

    // 配送方法が「宅配」の場合、配送希望日が必須
    [RequiredIf("DeliveryMethod", "宅配", ErrorMessage = "宅配の場合は配送希望日を入力してください")]
    [Display(Name = "配送希望日")]
    public DateTime? DeliveryDate { get; set; }

    [Display(Name = "支払い方法")]
    public string PaymentMethod { get; set; }

    // 支払い方法が「クレジットカード」の場合、カード番号が必須
    [RequiredIf("PaymentMethod", "クレジットカード", ErrorMessage = "クレジットカードの場合はカード番号を入力してください")]
    [Display(Name = "クレジットカード番号")]
    public string CreditCardNumber { get; set; }

    // 支払い方法が「銀行振込」の場合、振込口座が必須
    [RequiredIf("PaymentMethod", "銀行振込", ErrorMessage = "銀行振込の場合は振込口座を入力してください")]
    [Display(Name = "振込口座")]
    public string BankAccount { get; set; }

    [Display(Name = "ギフト包装希望")]
    public bool WantsGiftWrap { get; set; }

    // ギフト包装を希望する場合、メッセージカードが必須
    [RequiredIfTrue("WantsGiftWrap", ErrorMessage = "ギフト包装を希望する場合はメッセージカードを入力してください")]
    [Display(Name = "メッセージカード")]
    public string GiftMessage { get; set; }

    [Display(Name = "緊急性")]
    public string Urgency { get; set; }

    // 緊急性が「緊急」の場合、緊急連絡先が必須
    [RequiredIf("Urgency", "緊急", ErrorMessage = "緊急の場合は緊急連絡先を入力してください")]
    [Display(Name = "緊急連絡先")]
    public string EmergencyContact { get; set; }
}

複雑な条件付きバリデーション

using System;
using System.ComponentModel.DataAnnotations;
using Foolproof;

// 複雑な条件を持つ保険申請モデル
public class InsuranceApplicationModel
{
    [Required(ErrorMessage = "申請者名は必須項目です")]
    [Display(Name = "申請者名")]
    public string ApplicantName { get; set; }

    [Required(ErrorMessage = "年齢は必須項目です")]
    [Range(0, 120, ErrorMessage = "年齢は0歳から120歳の間で入力してください")]
    [Display(Name = "年齢")]
    public int Age { get; set; }

    [Display(Name = "性別")]
    public string Gender { get; set; }

    [Display(Name = "既往歴がある")]
    public bool HasMedicalHistory { get; set; }

    // 既往歴がある場合、詳細が必須
    [RequiredIfTrue("HasMedicalHistory", ErrorMessage = "既往歴がある場合は詳細を入力してください")]
    [Display(Name = "既往歴の詳細")]
    public string MedicalHistoryDetails { get; set; }

    [Display(Name = "職業")]
    public string Occupation { get; set; }

    // 職業が「パイロット」「レーサー」「登山家」の場合、特別な書類が必要
    [RequiredIfRegExMatch("Occupation", @"パイロット|レーサー|登山家", 
        ErrorMessage = "危険な職業の場合は特別な書類が必要です")]
    [Display(Name = "特別書類")]
    public string SpecialDocuments { get; set; }

    [Display(Name = "保険金額")]
    [Range(1000000, 100000000, ErrorMessage = "保険金額は100万円から1億円の間で入力してください")]
    public decimal InsuranceAmount { get; set; }

    // 保険金額が5000万円以上の場合、所得証明が必要
    [RequiredIf("InsuranceAmount", Operator.GreaterThanOrEqualTo, 50000000, 
        ErrorMessage = "保険金額が5000万円以上の場合は所得証明が必要です")]
    [Display(Name = "所得証明")]
    public string IncomeProof { get; set; }

    [Display(Name = "喫煙者")]
    public bool IsSmoker { get; set; }

    // 喫煙者の場合、1日の喫煙本数が必須
    [RequiredIfTrue("IsSmoker", ErrorMessage = "喫煙者の場合は1日の喫煙本数を入力してください")]
    [Display(Name = "1日の喫煙本数")]
    public int? CigarettesPerDay { get; set; }

    [Display(Name = "飲酒習慣")]
    public string DrinkingHabits { get; set; }

    // 飲酒習慣が「毎日」の場合、1日の飲酒量が必須
    [RequiredIf("DrinkingHabits", "毎日", ErrorMessage = "毎日飲酒する場合は1日の飲酒量を入力してください")]
    [Display(Name = "1日の飲酒量")]
    public string AlcoholConsumption { get; set; }

    [Display(Name = "運動習慣")]
    public string ExerciseHabits { get; set; }

    // 運動習慣が「なし」の場合、理由が必須
    [RequiredIf("ExerciseHabits", "なし", ErrorMessage = "運動習慣がない場合は理由を入力してください")]
    [Display(Name = "運動をしない理由")]
    public string NoExerciseReason { get; set; }

    [Display(Name = "保険金受取人")]
    public string Beneficiary { get; set; }

    // 保険金受取人が「その他」の場合、関係性の説明が必須
    [RequiredIf("Beneficiary", "その他", ErrorMessage = "保険金受取人が「その他」の場合は関係性を説明してください")]
    [Display(Name = "受取人との関係")]
    public string BeneficiaryRelationship { get; set; }

    [Display(Name = "海外在住経験")]
    public bool HasOverseasExperience { get; set; }

    // 海外在住経験がある場合、国と期間が必須
    [RequiredIfTrue("HasOverseasExperience", ErrorMessage = "海外在住経験がある場合は国と期間を入力してください")]
    [Display(Name = "海外在住詳細")]
    public string OverseasDetails { get; set; }
}

// 医療情報モデル
public class MedicalInfoModel
{
    [Required(ErrorMessage = "患者名は必須項目です")]
    [Display(Name = "患者名")]
    public string PatientName { get; set; }

    [Display(Name = "症状の種類")]
    public string SymptomType { get; set; }

    // 症状の種類が「アレルギー」の場合、アレルゲンが必須
    [RequiredIf("SymptomType", "アレルギー", ErrorMessage = "アレルギーの場合はアレルゲンを入力してください")]
    [Display(Name = "アレルゲン")]
    public string Allergen { get; set; }

    // 症状の種類が「感染症」の場合、感染経路が必須
    [RequiredIf("SymptomType", "感染症", ErrorMessage = "感染症の場合は感染経路を入力してください")]
    [Display(Name = "感染経路")]
    public string InfectionRoute { get; set; }

    [Display(Name = "緊急度")]
    public string Urgency { get; set; }

    // 緊急度が「緊急」の場合、症状発症時刻が必須
    [RequiredIf("Urgency", "緊急", ErrorMessage = "緊急の場合は症状発症時刻を入力してください")]
    [Display(Name = "症状発症時刻")]
    public DateTime? SymptomOnsetTime { get; set; }

    [Display(Name = "妊娠中")]
    public bool IsPregnant { get; set; }

    // 妊娠中の場合、妊娠週数が必須
    [RequiredIfTrue("IsPregnant", ErrorMessage = "妊娠中の場合は妊娠週数を入力してください")]
    [Display(Name = "妊娠週数")]
    public int? PregnancyWeeks { get; set; }

    [Display(Name = "服用中の薬")]
    public string CurrentMedication { get; set; }

    // 服用中の薬が「あり」の場合、薬名が必須
    [RequiredIf("CurrentMedication", "あり", ErrorMessage = "服用中の薬がある場合は薬名を入力してください")]
    [Display(Name = "薬名")]
    public string MedicationNames { get; set; }

    [Display(Name = "手術歴")]
    public bool HasSurgeryHistory { get; set; }

    // 手術歴がある場合、手術内容が必須
    [RequiredIfTrue("HasSurgeryHistory", ErrorMessage = "手術歴がある場合は手術内容を入力してください")]
    [Display(Name = "手術内容")]
    public string SurgeryDetails { get; set; }

    [Display(Name = "家族歴")]
    public string FamilyHistory { get; set; }

    // 家族歴が「あり」の場合、詳細が必須
    [RequiredIf("FamilyHistory", "あり", ErrorMessage = "家族歴がある場合は詳細を入力してください")]
    [Display(Name = "家族歴の詳細")]
    public string FamilyHistoryDetails { get; set; }

    [Display(Name = "血液型")]
    public string BloodType { get; set; }

    // 血液型が「不明」の場合、血液型検査の希望が必須
    [RequiredIf("BloodType", "不明", ErrorMessage = "血液型が不明の場合は血液型検査の希望を入力してください")]
    [Display(Name = "血液型検査希望")]
    public bool WantsBloodTypeTest { get; set; }
}

// 複雑な条件の組み合わせ例
public class ComplexValidationModel
{
    [Display(Name = "年齢")]
    [Range(0, 120)]
    public int Age { get; set; }

    [Display(Name = "性別")]
    public string Gender { get; set; }

    [Display(Name = "職業")]
    public string Occupation { get; set; }

    [Display(Name = "年収")]
    public decimal? Income { get; set; }

    // 年齢が65歳以上かつ性別が男性の場合、年金受給証明が必要
    [RequiredIfTrue("IsElderly", ErrorMessage = "65歳以上の男性は年金受給証明が必要です")]
    [Display(Name = "年金受給証明")]
    public string PensionProof { get; set; }

    // 計算プロパティ(バリデーション用)
    public bool IsElderly => Age >= 65 && Gender == "男性";

    // 職業が「会社員」で年収が1000万円以上の場合、所得証明が必要
    [RequiredIfTrue("IsHighIncomeEmployee", ErrorMessage = "年収1000万円以上の会社員は所得証明が必要です")]
    [Display(Name = "所得証明")]
    public string IncomeProof { get; set; }

    // 計算プロパティ(バリデーション用)
    public bool IsHighIncomeEmployee => Occupation == "会社員" && Income >= 10000000;

    [Display(Name = "結婚状況")]
    public string MaritalStatus { get; set; }

    [Display(Name = "子供の数")]
    public int NumberOfChildren { get; set; }

    // 結婚していて子供がいる場合、配偶者の職業が必要
    [RequiredIfTrue("IsMarriedWithChildren", ErrorMessage = "結婚していて子供がいる場合は配偶者の職業を入力してください")]
    [Display(Name = "配偶者の職業")]
    public string SpouseOccupation { get; set; }

    // 計算プロパティ(バリデーション用)
    public bool IsMarriedWithChildren => MaritalStatus == "既婚" && NumberOfChildren > 0;

    [Display(Name = "住所タイプ")]
    public string AddressType { get; set; }

    [Display(Name = "居住年数")]
    public int YearsOfResidence { get; set; }

    // 賃貸で居住年数が1年未満の場合、前住所が必要
    [RequiredIfTrue("IsShortTermRenter", ErrorMessage = "賃貸で居住年数が1年未満の場合は前住所を入力してください")]
    [Display(Name = "前住所")]
    public string PreviousAddress { get; set; }

    // 計算プロパティ(バリデーション用)
    public bool IsShortTermRenter => AddressType == "賃貸" && YearsOfResidence < 1;
}

ASP.NET MVCでの実装例

using System;
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
using Foolproof;

// MVCコントローラーでの使用例
public class RegistrationController : Controller
{
    // GET: Registration
    public ActionResult Create()
    {
        var model = new UserRegistrationModel();
        return View(model);
    }

    // POST: Registration
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(UserRegistrationModel model)
    {
        if (ModelState.IsValid)
        {
            // カスタムバリデーション(ビジネスロジック)
            if (model.IsMarried && string.IsNullOrEmpty(model.SpouseName))
            {
                ModelState.AddModelError("SpouseName", "結婚している場合は配偶者の名前を入力してください");
            }

            if (model.Age < 18 && !model.ParentalConsent)
            {
                ModelState.AddModelError("ParentalConsent", "18歳未満の場合は保護者の同意が必要です");
            }

            // 重複チェック
            if (IsEmailDuplicate(model.Email))
            {
                ModelState.AddModelError("Email", "このメールアドレスは既に登録されています");
            }

            if (ModelState.IsValid)
            {
                // データベースに保存
                SaveUserRegistration(model);
                
                TempData["Success"] = "登録が完了しました";
                return RedirectToAction("Success");
            }
        }

        // バリデーションエラーがある場合は再度フォームを表示
        return View(model);
    }

    // Ajax バリデーション
    [HttpPost]
    public JsonResult ValidateEmail(string email)
    {
        bool isValid = !IsEmailDuplicate(email);
        return Json(new { isValid = isValid, message = isValid ? "" : "このメールアドレスは既に登録されています" });
    }

    private bool IsEmailDuplicate(string email)
    {
        // 実際の実装ではデータベースをチェック
        var existingEmails = new[] { "[email protected]", "[email protected]" };
        return Array.Exists(existingEmails, e => e.Equals(email, StringComparison.OrdinalIgnoreCase));
    }

    private void SaveUserRegistration(UserRegistrationModel model)
    {
        // 実際の実装ではデータベースに保存
        // 簡略化のため、ここでは何もしない
    }

    public ActionResult Success()
    {
        return View();
    }
}

// Razorビューでの使用例(Views/Registration/Create.cshtml)
/*
@model UserRegistrationModel

@{
    ViewBag.Title = "ユーザー登録";
}

<h2>ユーザー登録</h2>

@using (Html.BeginForm("Create", "Registration", FormMethod.Post, new { @class = "form-horizontal" }))
{
    @Html.AntiForgeryToken()
    
    <div class="form-group">
        @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.Age, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Age, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.Age, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <div class="checkbox">
                @Html.EditorFor(model => model.IsMarried)
                @Html.LabelFor(model => model.IsMarried, "結婚している")
            </div>
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.SpouseName, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.SpouseName, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.SpouseName, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <div class="checkbox">
                @Html.EditorFor(model => model.HasDriverLicense)
                @Html.LabelFor(model => model.HasDriverLicense, "運転免許証を持っている")
            </div>
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.DriverLicenseNumber, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.DriverLicenseNumber, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.DriverLicenseNumber, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.AddressType, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.DropDownListFor(model => model.AddressType, new SelectList(new[] { "自宅", "勤務先", "その他" }), "選択してください", new { @class = "form-control" })
            @Html.ValidationMessageFor(model => model.AddressType, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.Address, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Address, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.Address, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.Country, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.DropDownListFor(model => model.Country, new SelectList(new[] { "日本", "アメリカ", "イギリス", "その他" }), "選択してください", new { @class = "form-control" })
            @Html.ValidationMessageFor(model => model.Country, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.VisaInfo, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.VisaInfo, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.VisaInfo, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="登録" class="btn btn-primary" />
        </div>
    </div>
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    <script src="~/Scripts/mvcfoolproof.unobtrusive.min.js"></script>
}
*/