在剃刀页面中实现自定义模型粘合剂

在剃刀页面中,模型绑定是从HTTP请求映射数据的过程 简单的PageModel属性或处理程序参数。 传入数据可以包含在发布表单值的请求中,查询 字符串值或路由数据。模型粘合剂覆盖的默认收集 每个简单的.NET数据类型..但有时它们是不够的,而且您需要添加 你自己的实施。

我的前一篇文章,我探索了可用的功能简化 the task of 使用Razor Pages表单中的日期和时间数据。输入标签 Helper以HTML5日期和时间控制工作的格式生成值 with, and the default DateTimeModelBinder binds the posted values back to .NET DateTime types, delivering perfect 2-way 数据绑定。大多。

虽然不太可能被广泛使用,但它甚至不支持 some browsers, the 周输入类型 确实是一个目的。它 使用户能够选择一年的特定周。这 week input requires a value in a specific format in order to work: yyyy-Www, where yyyy is the full year -W is literal, and ww represents the ISO 8601周 the year。今天(11月1日,2020年)我们在第44周结束时,这是 represented as 2020-W44.

You can configure a DateTime property to correspond to a week 输入类型 easily just be adding a custom data type string to the DataType attribute:

[BindProperty, DataType("week")] 
public DateTime Week { get; set; }

输入标签帮助器呈现正确的HTML5控制和格式化 价值根据所需标准:

但是,当表单发布时,所选值不受绑定到 Week 页面模型上的财产。

你可以选择 解析原始的发布字符串值,并在任何地方获取一周数 你需要在整个应用程序中,但更好 解决方案是创建自定义模型活页夹,以便为您做这一点 place.

模型粘合剂基础知识

Model binders implement the IModelBinder interface, which contains one member:

Task BindModelAsync(ModelBindingContext bindingContext)

在此方法中,您可以尝试处理传入的值和 assign them to model 属性或参数。创建自定义模型活页夹后, 您要么通过它将其应用于特定财产 ModelBinder attribute, or you can register it globally using a ModelBinderProvider.

Headofyear ModelBinder.

To resolve the issue with binding the week 输入类型 value to a DateTime type, the approach using the ModelBinder attribute is simplest. The following code for a custom WeekOfYearModelBinder is based on the 现有DateTimeModelbinder的源代码:

public class WeekOfYearModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }
 
        var modelName = bindingContext.ModelName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }
 
        var modelState = bindingContext.ModelState;
        modelState.SetModelValue(modelName, valueProviderResult);
 
        var metadata = bindingContext.ModelMetadata;
        var type = metadata.UnderlyingOrModelType;
        try
        {
            var value = valueProviderResult.FirstValue;
 
            object model;
            if (string.IsNullOrWhiteSpace(value))
            {
                model = null;
            }
            else if (type == typeof(DateTime))
            {
                var week = value.Split("-W");
                model = ISOWeek.ToDateTime(Convert.ToInt32(week[0]), Convert.ToInt32(week[1]), DayOfWeek.Monday);
            }
            else
            {
                throw new NotSupportedException();
            }
 
            if (model == null && !metadata.IsReferenceOrNullableType)
            {
                modelState.TryAddModelError(
                    modelName,
                    metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
                        valueProviderResult.ToString()));
            }
            else
            {
                bindingContext.Result = ModelBindingResult.Success(model);
            }
        }
        catch (Exception exception)
        {
            // Conversion failed.
            modelState.TryAddModelError(modelName, exception, metadata);
        }
        return Task.CompletedTask;
    }
}

乍一看的代码似乎令人生畏,但大多数是 相当的样板。这款模特活页夹与它所基于的原始代码之间的唯一真实差异 是日志记录的遗漏,并解析值的方式才能创建有效性 DateTime value:

var week = value.Split("-W");
model = ISOWeek.ToDateTime(Convert.ToInt32(week[0]), Convert.ToInt32(week[1]), DayOfWeek.Monday);

The code that gets the DateTime from a week number is basically the same as in the previous article. It uses the ISOWeek utility class to generate a DateTime from the year and the week number which is obtained by using the string.Split function on the incoming value.

如果模型绑定成功 - 获得合适的值和 分配给模型和 ModelBindingContext.Result is set to a value returned from ModelBindingResult.Success. Otherwise, an entry is added to the Errors collection of ModelState. There is also a 检查,在传入值为null的情况下,查看该模型是否 property is required, and if so, an error is logged with ModelState.

The ModelBinder attribute is used to register the custom model binder 反对它应该用于的特定财产:

[BindProperty, DataType("week"), ModelBinder(BinderType = typeof(WeekOfYearModelBinder))] 
public DateTime Week { get; set; }

现在,当应用程序运行时,将使用此模型粘合剂来使用 Week 在这种情况下的财产。如果要使用自定义 在应用程序中其他地方的属性上的粘合剂,您需要应用 归属于那里。或者,您可以注册模型粘合剂 Startup 它可以使用每个请求。

模型粘合剂提供商

模型活页夹提供商用于全球注册模型粘合剂。他们 负责创建正确配置的模型绑定器。全部 内置模型粘合剂具有相关的粘合剂提供商。但首先,你需要一个 binder:

public class WeekOfYearAwareDateTimeModelBinder : IModelBinder
{
    private readonly DateTimeStyles _supportedStyles;
    private readonly ILogger _logger;
 
    public WeekOfYearAwareDateTimeModelBinder(DateTimeStyles supportedStyles, ILoggerFactory loggerFactory)
    {
        if (loggerFactory == null)
        {
            throw new ArgumentNullException(nameof(loggerFactory));
        }
 
        _supportedStyles = supportedStyles;
        _logger = loggerFactory.CreateLogger<WeekOfYearAwareDateTimeModelBinder>();
    }
 
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }
 
        var modelName = bindingContext.ModelName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
        if (valueProviderResult == ValueProviderResult.None)
        {
            // no entry
            return Task.CompletedTask;
        }
 
        var modelState = bindingContext.ModelState;
        modelState.SetModelValue(modelName, valueProviderResult);
 
        var metadata = bindingContext.ModelMetadata;
        var type = metadata.UnderlyingOrModelType;
 
        var value = valueProviderResult.FirstValue;
        var culture = valueProviderResult.Culture;
 
        object model;
        if (string.IsNullOrWhiteSpace(value))
        {
            model = null;
        }
        else if (type == typeof(DateTime))
        {
            if (value.Contains("W"))
            {
                var week = value.Split("-W");
                model = ISOWeek.ToDateTime(Convert.ToInt32(week[0]), Convert.ToInt32(week[1]), DayOfWeek.Monday);
            }
            else
            {
                model = DateTime.Parse(value, culture, _supportedStyles);
            }
        }
        else
        {
            // unreachable
            throw new NotSupportedException();
        }
 
        // When converting value, a null model may indicate a failed conversion for an otherwise required
        // model (can't set a ValueType to null). This detects if a null model value is acceptable given the
        // current bindingContext. If not, an error is logged.
        if (model == null && !metadata.IsReferenceOrNullableType)
        {
            modelState.TryAddModelError(
                modelName,
                metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
                    valueProviderResult.ToString()));
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Success(model);
        }
 
        return Task.CompletedTask;
    }
}

This is another modified version of the actual DateTimeModelBinder. 这次的差异是添加检查的条件 -W 存在于正在处理的值中。如果它确实如此值 来自一个星期的输入,它使用前一个的代码进行处理 例子。否则,使用原始值解析该值 DateTime model binding algorithm (basically DateTime.Parse). This version retains the logging and DateTimeStyles from the original source that need to be 注入构造函数,使模型的原始行为 binding DateTimes is preserved. Configuration of the 构造函数参数由模型粘合剂照顾 provider:

public class WeekOfYearModelBinderProvider : IModelBinderProvider
{
    internal static readonly DateTimeStyles SupportedStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces;
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
 
        var modelType = context.Metadata.UnderlyingOrModelType;
        if (modelType == typeof(DateTime))
        {
            var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
            return new WeekOfYearAwareDateTimeModelBinder(SupportedStyles, loggerFactory);
        }
 
        return null;
    }
}

此代码再次,基本上是与 内置DateTime Model Binder提供商。唯一的区别在于 粘合剂返回的类型。

剃刀页面是一个坐在MVC框架之上的层。许多 什么让剃刀页面“只是工作”在MVC层。模型绑定是一个 这些功能。因此,配置模型绑定器的访问点是通过 MvcOptions in ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages().AddMvcOptions(options =>
    {
        options.ModelBinderProviders.在 sert(0, new WeekOfYearModelBinderProvider());
    });
}

ModelBinder提供商按顺序进行评估,直到一个匹配 输入模型的数据类型位于。然后用于尝试绑定 模型的传入值。如果绑定不成功,两个中的一个 事情发生了 - 模型值设置为其默认值或a validation error is added to ModelState. Any other model binder providers 被忽略了。所以这个新的模型活页夹提供商在开始时插入 of the collection to ensure that it is used for DateTime types 而不是默认的模型活页夹。

Summary

自定义模型粘合剂不难实现。你可以依靠 锅炉板填充了大多数现有的骨架粘合剂和 调整根据您的需求解析传入值的算法。 You can register them locally using the ModelBinder attribute or globally via MvcOptions.

如果要将传入的字符串绑定到更复杂的类型,则 recommendation is to use a TypeConverter. That will be the 我的下一篇文章的主题。