5

検証するモデルがあり、問題は生年月日フィールドです。3 つのドロップダウン (日、月、年) で構成する必要があります。

<div id="dob-editor-field" class="model-field-editor">
      @Html.LabelFor(m => m.DateOfBirth, new { @class = "label-div" })
      @Html.Telerik().DropDownList().Name("DobDay").BindTo((SelectList)ViewData["Days"]).HtmlAttributes(new {id = "DobDaySel"})
      @Html.Telerik().DropDownList().Name("DobMonth").BindTo((SelectList)ViewData["Months"]).HtmlAttributes(new { id = "DobMonthSel"})
      @Html.Telerik().DropDownList().Name("DobYear").BindTo((SelectList)ViewData["Years"]).HtmlAttributes(new { id = "DobYearSel" })
      @Html.ValidationMessageFor(m => m.DateOfBirth)
</div>

サーバー側で私はこれを行います

        [HttpPost]
        public ActionResult Register(RegistrationModel regInfo, int DobDay, int DobMonth, int DobYear)
        {
            SetRegisterViewData(DobDay, DobMonth, DobYear);
            if (DobDay == 0 || DobMonth == 0 && DobYear == 0)
            {
                ModelState.AddModelError("DateOfBirth", "Date of birth is required");
            }
            else
            {
                DateTime dt = new DateTime(DobYear, DobMonth, DobDay);
                long ticks = DateTime.Now.Ticks - dt.Ticks;
                int years = new DateTime(ticks).Year;
                if (years < 18)
                {
                    ModelState.AddModelError("DateOfBirth", "You must be at least 18");
                }
            }            
            if (ModelState.IsValid)
            {
                //register user
                return RedirectToAction("Index", "Home");
            }
            return View(regInfo);
        }

質問:

  1. サーバー側 : どうすれば改善できますか? (生年月日、月、年のプロパティRegistrationModelを追加し、DateOfBirthに属性を追加してそれらのプロパティを確認することを考えています)
  2. クライアント側:カスタム属性のクライアント側検証の実行を見ていましたが、混乱しました。それを作る方法は何ですか?

LE: 次のような日付のカスタム モデル バインダーを作成しました。

    public class DobModelBinder : DefaultModelBinder
    {
        protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
        {
            if (propertyDescriptor.Name == "DateOfBirth")
            {
                DateTime dob = DateTime.MinValue;
                var form = controllerContext.HttpContext.Request.Form;
                int day = Convert.ToInt32(form["DobDay"]);
                int month = Convert.ToInt32(form["DobMonth"]);
                int year = Convert.ToInt32(form["DobYear"]);
                if (day == 0 || month == 0 || year == 0)
                {
                    SetProperty(controllerContext, bindingContext, propertyDescriptor, DateTime.MinValue);
                }
                else
                {
                    SetProperty(controllerContext, bindingContext, propertyDescriptor, new DateTime(year, month, day));
                }
            }
            else
            {
                base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
            }
        }
    }

私は次のように登録しました。

ModelBinders.Binders.Add(typeof(DateTime), new DobModelBinder());

私は次のように使用しました:

public ActionResult Register([ModelBinder(typeof(DobModelBinder))]RegistrationModel regInfo)

DateOfBirth は適切にバインドされます。

LE2:

生年月日の検証属性を次のように作成しました。

 public override bool IsValid(object value)
    {
        DateTime date = Convert.ToDateTime(value);
        return date != DateTime.MinValue;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        yield return new ModelClientValidationRule
        {
            ErrorMessage = this.ErrorMessage,
            ValidationType = "dateRequired"
        };
    }
}

public class DateGraterThanEighteen : ValidationAttribute, IClientValidatable
{
    public override bool IsValid(object value)
    {
        DateTime date = Convert.ToDateTime(value);
        long ticks = DateTime.Now.Ticks - date.Ticks;
        int years = new DateTime(ticks).Year;
        return years >= 18;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        yield return new ModelClientValidationRule
        {
            ErrorMessage = this.ErrorMessage,
            ValidationType = "dateGraterThanEighteen"
        };
    }
}

このような属性を適用しました

        [DateGraterThanEighteen(ErrorMessage="You must be at least 18")]
        [DateRequired(ErrorMessage = "Date of birth is required")]
        public DateTime DateOfBirth { get; set; }

LE3:

クライアント側で私はこれを行います:

      $(function () {
            jQuery.validator.addMethod('dobRequired', function (value, element, params) {
                if (!/Invalid|NaN/.test(new Date(value))) {
                    return true;
                }
                else {
                    return false;
                }
            }, '');
            jQuery.validator.unobtrusive.adapters.add('dateRequired', {}, function (options) {
                options.rules['dobRequired'] = true;
                options.messages['dobRequired'] = options.message;
            });
        });

クライアントの検証が機能していないようです。どうすれば修正できますか?これらのアダプターの動作方法にちょっと混乱しています。

4

4 に答える 4

14

カスタム エディター テンプレートを使用できます。

実装の詳細に入る前に、まず最終的なソリューションがどのように見えるかを見てみましょう。

したがって、ビュー モデルを (いつものように) 添付したいメタデータを示すいくつかのデータ注釈属性で装飾することができます。

public class MyViewModel
{
    [DisplayName("Date of birth:")]
    [TrippleDDLDateTime(ErrorMessage = "Please select a valid DOB")]
    [Required(ErrorMessage = "Please select your DOB")]
    [MinAge(18, ErrorMessage = "You must be at least 18 years old")]
    public DateTime? Dob { get; set; }
}

次に、コントローラーを作成できます。

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new MyViewModel();
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(MyViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        return Content(
            string.Format(
                "Thank you for selecting your DOB: {0:yyyy-MM-dd}", 
                model.Dob
            )
        );
    }
}

ビュー ( ~/Views/Home/Index.cshtml):

@model MyViewModel
@using (Html.BeginForm())
{
    @Html.EditorFor(x => x.Dob)
    <button type="submit">OK</button>
}

対応するエディター テンプレートを使用すると、単純なテキスト ボックスの代わりに DateTime フィールドを編集するための 3 つのドロップダウン リストを表示できます ( ~/Views/Shared/EditorTemplates/TrippleDDLDateTime.cshtml)。

@{
    var now = DateTime.Now;
    var years = Enumerable.Range(0, 150).Select(x => new SelectListItem { Value = (now.Year - x).ToString(), Text = (now.Year - x).ToString() });
    var months = Enumerable.Range(1, 12).Select(x => new SelectListItem { Value = x.ToString("00"), Text = x.ToString() });
    var days = Enumerable.Range(1, 31).Select(x => new SelectListItem { Value = x.ToString("00"), Text = x.ToString() });

    var result = ViewData.ModelState[ViewData.TemplateInfo.HtmlFieldPrefix];
    if (result != null)
    { 
        var values = result.Value.RawValue as string[];
        years = new SelectList(years, "Value", "Text", values[0]);
        months = new SelectList(months, "Value", "Text", values[1]);
        days = new SelectList(days, "Value", "Text", values[2]);
        result.Value = null;
    }
}

<div class="trippleddldatetime">
    @Html.Label("")

    @Html.DropDownList("", years, "-- year --")
    @Html.DropDownList("", months, "-- month --")
    @Html.DropDownList("", days, "-- day --")

    @Html.ValidationMessage("")
</div>

[TrippleDDLDateTime]次に、属性を実装する方法を見てみましょう。

public class TrippleDDLDateTimeAttribute : ValidationAttribute, IMetadataAware
{
    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.TemplateHint = "TrippleDDLDateTime";
    }

    public override bool IsValid(object value)
    {
        // It's the custom model binder that is responsible for validating 
        return true;
    }
}

IMetadataAware属性が、ビュー モデル プロパティを作成したカスタム エディター テンプレートに関連付けることができるインターフェイスを実装する方法に注意してください ( TrippleDDLDateTime.cshtml)。

次は[MinAge]属性です。

public class MinAgeAttribute : ValidationAttribute
{
    private readonly int _minAge;
    public MinAgeAttribute(int minAge)
    {
        _minAge = minAge;
    }

    public override bool IsValid(object value)
    {
        if (value == null)
        {
            return true;
        }

        DateTime date = Convert.ToDateTime(value);
        long ticks = DateTime.Now.Ticks - date.Ticks;
        int years = new DateTime(ticks).Year;
        return years >= _minAge;
    }
}

[TrippleDDLDateTime]パズルの最後のピースは、解析を実行するために、属性で装飾されたプロパティに関連付けられるカスタム モデル バインダーを作成することです。

public class TrippleDDLDateTimeModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var metadata = bindingContext.ModelMetadata;
        var trippleDdl = metadata.ContainerType.GetProperty(metadata.PropertyName).GetCustomAttributes(typeof(TrippleDDLDateTimeAttribute), true).FirstOrDefault() as TrippleDDLDateTimeAttribute;
        if (trippleDdl == null)
        {
            return base.BindModel(controllerContext, bindingContext);
        }

        var prefix = bindingContext.ModelName;
        var value = bindingContext.ValueProvider.GetValue(prefix);
        var parts = value.RawValue as string[];
        if (parts.All(string.IsNullOrEmpty))
        {
            return null;
        }

        bindingContext.ModelState.SetModelValue(prefix, value);

        var dateStr = string.Format("{0}-{1}-{2}", parts[0], parts[1], parts[2]);
        DateTime date;
        if (DateTime.TryParseExact(dateStr, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
        {
            return date;
        }

        bindingContext.ModelState.AddModelError(prefix, trippleDdl.ErrorMessage);

        return null;
    }
}

フィールドがカスタム属性で装飾されていない場合、バインダーが単にデフォルトのバインダーを使用する方法に注意してください。このようにして、トリプル ddl の動作を望まない他の DateTime フィールドに干渉しません。DateTime?モデル バインダーは、単純にタイプ inに関連付けられますApplication_Start

ModelBinders.Binders.Add(typeof(DateTime?), new TrippleDDLDateTimeModelBinder());

OK、これまでのところ、サーバー側の検証を実行するソリューションがあります。それは常にあなたが始めるべきことです。それは、停止しても安全で機能するサイトを維持できる場所だからです。

もちろん、時間があれば、クライアント側の検証を実装することでユーザー エクスペリエンスを向上させることができます。クライアント側の検証は必須ではありませんが、帯域幅を節約し、サーバーの往復を回避します。

IClientValidatableそのため、控えめなクライアント側の検証を有効にするための最初のステップであるインターフェイスを実装する 2 つのカスタム属性を作成することから始めます。

[TrippleDDLDateTime]:

public class TrippleDDLDateTimeAttribute : ValidationAttribute, IMetadataAware, IClientValidatable
{
    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.TemplateHint = "TrippleDDLDateTime";
    }

    public override bool IsValid(object value)
    {
        // It's the custom model binder that is responsible for validating 
        return true;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule();
        rule.ErrorMessage = ErrorMessage;
        rule.ValidationType = "trippleddldate";
        yield return rule;
    }
}

[MinAge]:

public class MinAgeAttribute : ValidationAttribute, IClientValidatable
{
    private readonly int _minAge;
    public MinAgeAttribute(int minAge)
    {
        _minAge = minAge;
    }

    public override bool IsValid(object value)
    {
        if (value == null)
        {
            return true;
        }

        DateTime date = Convert.ToDateTime(value);
        long ticks = DateTime.Now.Ticks - date.Ticks;
        int years = new DateTime(ticks).Year;
        return years >= _minAge;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule();
        rule.ErrorMessage = ErrorMessage;
        rule.ValidationType = "minage";
        rule.ValidationParameters["min"] = _minAge;
        yield return rule;
    }
}

OK、GetClientValidationRules両方の属性に を実装しました。あとは、対応する目立たないアダプターを作成するだけです。

もちろん、これは別のjavascriptファイルで行う必要があります。たとえば、次のようになりますtrippleddlAdapters.js

(function ($) {
    $.fn.getDateFromTrippleDdls = function () {
        var year = this.find('select:nth(0)').val();
        var month = this.find('select:nth(1)').val();
        var day = this.find('select:nth(2)').val();
        if (year == '' || month == '' || day == '') {
            return NaN;
        }

        var y = parseInt(year, 10);
        var m = parseInt(month, 10);
        var d = parseInt(day, 10);

        var date = new Date(y, m - 1, d);
        var isValidDate = date.getFullYear() == y && date.getMonth() + 1 == m && date.getDate() == d;
        if (isValidDate) {
            return date;
        }

        return NaN;
    };

    $.validator.unobtrusive.adapters.add('trippleddldate', [], function (options) {
        options.rules['trippleddldate'] = options.params;
        if (options.message) {
            options.messages['trippleddldate'] = options.message;
        }
    });

    $.validator.addMethod('trippleddldate', function (value, element, params) {
        var parent = $(element).closest('.trippleddldatetime');
        var date = parent.getDateFromTrippleDdls();
        console.log(date);
        return !isNaN(date);
    }, '');

    $.validator.unobtrusive.adapters.add('minage', ['min'], function (options) {
        options.rules['minage'] = options.params;
        if (options.message) {
            options.messages['minage'] = options.message;
        }
    });

    $.validator.addMethod('minage', function (value, element, params) {
        var parent = $(element).closest('.trippleddldatetime');
        var birthDate = parent.getDateFromTrippleDdls();
        if (isNaN(birthDate)) {
            return false;
        }

        var today = new Date();
        var age = today.getFullYear() - birthDate.getFullYear();
        var m = today.getMonth() - birthDate.getMonth();
        if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
            age--;
        }
        return age >= parseInt(params.min, 10);
    }, '');
})(jQuery);

最後に、目立たないクライアント側の検証を有効にするために、必要な 3 つのスクリプトをページに含めます。

<script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/trippleddlAdapters.js")" type="text/javascript"></script>
于 2012-07-15T09:56:06.313 に答える
2

最初から、ここに書いていることは MVC 4 でテストされていると言いたいです。

3 つのドロップダウン リストに基づいてカスタム日付セレクターを実装するために、さまざまなソリューションを試しました。すべてが完璧に進みましたが、この投稿の前半で誰かが応答として述べたように、標準の日付バリデーターが標準のメッセージで起動することもありました (これには本当に夢中になりました)。

この問題を修正し、標準の日付バリデーターを完全に無効にしないために、次の解決策を見つけました。

a) カスタム日付モデル属性で、次の GetClientValidationRules バージョンを使用します。

public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        ModelClientValidationRule rule = new ModelClientValidationRule();
        rule.ErrorMessage = this.ErrorMessageString;
        rule.ValidationType = "extendeddate";
        rule.ValidationParameters.Add("isrequired", metadata.IsRequired.ToString().ToLower());
        rule.ValidationParameters.Add("disablestandardvalidation", true.ToString().ToLower());

        yield return rule;
    }

注: 最も重要な行は、最後に追加された検証パラメーターです。これについては、後で説明します。今のところ、これは「data-val-extendeddate-disablestandardvalidation」という HTML 属性に変換されることに注意してください。

b)外部jsファイル(場所は関係ありません-は単なる例ですが、すべての外部ライブラリをロードした後が望ましいです)で、次のブロックを記述します。

$(document).ready(function () {
var currentCulture = $("meta[name='accept-language']").prop("content");

// Set Globalize to the current culture driven by the meta tag (if any)
if (currentCulture) {
    Globalize.culture(currentCulture);
}

$.validator.methods.date = function (value, element) {
    var isDateValidationDisabled = $(element).data("val-extendeddate-disablestandardvalidation");

    if (typeof isDateValidationDisabled != "undefined") {
        return true;
    }

    var val = Globalize.parseDate(value);
    return this.optional(element) || (val);
};

$.validator.methods.number = function (value, element) {
    var val = Globalize.parseFloat(value);
    return this.optional(element) || ($.isNumeric(val));
}; });

注: このコード ブロックでは、アプリケーションで考えられる他のカルチャ用に、globalize jquery プラグインも読み込んでいます。

最後のコード ブロックの最も興味深い部分は、検証済みのコントロールでそのデータ属性を見つけた場合、標準の検証に合格し、true を返していることです。

要するに、なぜこれを行っているのか - 全体として検証する必要がある複雑なコントロールがある場合、標準のバリデーターは、変更された各ドロップダウンリストから日付を抽出しようとするため、機能しません。なぜ標準のバリデータがまだ有効なのかを理解するのに 8 時間かかりました。

幸運を!

PS: あなたが私のコメントを理解してくれたことを願っています - 私は本当にそれを修正したという事実にまだ興奮しています!

于 2013-03-21T15:00:49.607 に答える
2

Darin Dimitrov による解決策を試しましたが、小さな問題がいくつかありました。

問題の 1 つは、デフォルトの MVC 4 Javascript 日付バリデーターとの競合でした。有効な日付であっても、Web サイトのユーザーを混乱させてしまうことがありました。ここで見つけることができるソリューションを発明しました: デフォルトのクライアント側バリデーターを削除するには?

2 つ目の問題は、このソリューションでは 3 つのドロップダウンすべてに対して同じ id 属性が生成されることです。これは良くありません。id は HTML ページごとに一意である必要があります。これが私がそれを修正した方法です:

<div class="trippleddldatetime">
    @Html.DropDownList("", years, "Year:", new { id = @ViewData.TemplateInfo.HtmlFieldPrefix + "_y" })
    @Html.DropDownList("", months, "Month:", new { id = @ViewData.TemplateInfo.HtmlFieldPrefix + "_m" })
    @Html.DropDownList("", days, "Day:", new { id = @ViewData.TemplateInfo.HtmlFieldPrefix + "_d" })
</div>

最後の問題は、コントローラーからドロップダウンに値を事前設定しようとすると、これらのドロップダウンが例外をスローしたことでした。これが私がそれを修正した方法です:

var result = ViewData.ModelState[ViewData.TemplateInfo.HtmlFieldPrefix];
if (result != null && result.Value != null)
{ 
    var values = result.Value.RawValue as string[];
    years = new SelectList(years, "Value", "Text", values[0]);
    months = new SelectList(months, "Value", "Text", values[1]);
    days = new SelectList(days, "Value", "Text", values[2]);
    result.Value = null;
}
else
{
    var currentValue = ViewData.Model;
    if (currentValue != null)
    {
        years = new SelectList(years, "Value", "Text", currentValue.Year);
        months = new SelectList(months, "Value", "Text", currentValue.Month.ToString("00"));
        days = new SelectList(days, "Value", "Text", currentValue.Day.ToString("00"));
    }
}

そして最後の改善 - テキストとしての月名:

var months = Enumerable.Range(1, 12).Select(x => new SelectListItem { Value = x.ToString("00"), Text = System.Threading.Thread.CurrentThread.CurrentUICulture.DateTimeFormat.GetMonthName(x) });
于 2012-09-20T10:11:34.133 に答える
0

年、月、日を選択すると、年と月を検証するための赤い境界線がまだアクティブなままであるというDarin Dmitrov の回答に追加したいと思います。有効な日付が入力されると、他の 2 つのコンポーネントも同期されることが期待されるため、以下のように JavaScript を微調整しました。(removeChildValidationErrors と addChildValidationErrors の 2 つの関数が追加され、日付の検証結果に基づいて呼び出されます。)

(function ($) {
    $.fn.getDateFromTrippleDdls = function () {
        var year = this.find('select:nth(0)').val();
        var month = this.find('select:nth(1)').val();
        var day = this.find('select:nth(2)').val();
        if (year == '' || month == '' || day == '') {
            return NaN;
        }

        var y = parseInt(year, 10);
        var m = parseInt(month, 10);
        var d = parseInt(day, 10);

        var date = new Date(y, m - 1, d);
        var isValidDate = date.getFullYear() == y && date.getMonth() + 1 == m && date.getDate() == d;
        if (isValidDate) {
            return date;
        }

        return NaN;
    };

    $.fn.removeChildValidationErrors = function () {

        var year = this.find('select:nth(0)');
        var month = this.find('select:nth(1)');
        var day = this.find('select:nth(2)');

        $(year).removeClass("input-validation-error");
        $(month).removeClass("input-validation-error");
        $(day).removeClass("input-validation-error");


    };

    $.fn.addChildValidationErrors = function () {

        var year = this.find('select:nth(0)');
        var month = this.find('select:nth(1)');
        var day = this.find('select:nth(2)');

        $(year).addClass("input-validation-error");
        $(month).addClass("input-validation-error");
        $(day).addClass("input-validation-error");


    };

    $.validator.unobtrusive.adapters.add('trippleddldate', [], function (options) {
        options.rules['trippleddldate'] = options.params;
        if (options.message) {
            options.messages['trippleddldate'] = options.message;
        }
    });

    $.validator.addMethod('trippleddldate', function (value, element, params) {
        var parent = $(element).closest('.trippleddldatetime');
        var date = parent.getDateFromTrippleDdls();

        if (!isNaN(date))
        {
          parent.removeChildValidationErrors();
        }
        else
        {
           parent.addChildValidationErrors();
        }


        return !isNaN(date);
    }, '');


})(jQuery);

function removeDefaultDateValidators(selector, validatorToRemove) {
    $('form').each(function () {
        var settings = $(this).validate().settings;
        $(selector, this).each(function () {
            // rules and messages seem to be keyed by element name, not id
            var elmName = $(this).attr('name');
            delete settings.rules[elmName][validatorToRemove];
            delete settings.messages[elmName][validatorToRemove];
        });
    });
}

$(function () {
    removeDefaultDateValidators('select[data-val-trippleddldate]', 'date');
});
于 2015-03-10T03:40:59.467 に答える