web-dev-qa-db-ja.com

モデルバインディングコンマ区切りのクエリ文字列パラメーター

カンマ区切りの値であるクエリ文字列パラメータをバインドするにはどうすればよいですか

http://localhost/Action?ids=4783,5063,5305

リストを期待するコントローラーアクションに?

public ActionResult Action(List<long> ids)
{
    return View();
}

注!コントローラーアクションのidsはリスト(またはIEnumerableベースのもの)である必要があるため、string idsは、これらのパラメータが多くのアクションに渡され、文字列を配列に解析すると不要なノイズが追加されるため、回答として受け入れられません。

25
Tx3

Archilsの回答は、私自身のモデルバインダーを実装する方法についていくつかのアイデアを与えました。非常に一般的なCSVサポートの必要がなかったため、ソースコードを少し単純化することができました。受信したデータをList<int>に設定する代わりに、クラスに配置します。

モデルバインダー

public class FarmModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType == typeof(FarmModel))
        {
            var newBindingContext = new ModelBindingContext()
            {
                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
                () => CreateFarmModel(controllerContext, bindingContext),
                typeof(FarmModel)
                ),
                ModelState = bindingContext.ModelState,
                ValueProvider = bindingContext.ValueProvider
            };

            return base.BindModel(controllerContext, newBindingContext);
        }

        return base.BindModel(controllerContext, bindingContext);
    }

    private FarmModel CreateFarmModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var farmsIds = new List<int>();

        var value = bindingContext.ValueProvider.GetValue("farmData");
        if(value != null && value.AttemptedValue != null)
        {
            var array = value.AttemptedValue.Split(new [] {','});
            foreach (var s in array)
            {
                int result;
                if(int.TryParse(s, out result))
                {
                    farmsIds.Add(result);
                }
            }
        }
        return new FarmModel() { FarmIds = farmsIds };
    }
}

モデル

public class FarmModel
{
    public IEnumerable<int> FarmIds { get; set; }
}

カスタムバインダーの追加

System.Web.Mvc.ModelBinders.Binders.Add(typeof(FarmModel), new FarmModelBinder());
2
Tx3

これは、archilの回答で使用されているNathanTaylorのソリューションの改良版です。

  1. Nathanのバインダーは、複雑なモデルのサブプロパティのみをバインドできましたが、私のバインダーは、個々のコントローラー引数をバインドすることもできます。
  2. 私のバインダーは、配列またはIEnumerableの実際の空のインスタンスを返すことにより、空のパラメーターを正しく処理することもできます。

これを接続するには、これを個々のController引数に添付します。

[ModelBinder(typeof(CommaSeparatedModelBinder))]

…または、global.asax.csのApplication_Startでグローバルデフォルトバインダーとして設定します。

ModelBinders.Binders.DefaultBinder = new CommaSeparatedModelBinder();

2番目のケースでは、すべてのIEnumerableを処理しようとし、それ以外はすべてASP.NETMVC標準実装にフォールバックします。

見よ:

public class CommaSeparatedModelBinder : DefaultModelBinder
{
    private static readonly MethodInfo ToArrayMethod = typeof(Enumerable).GetMethod("ToArray");

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        return BindCsv(bindingContext.ModelType, bindingContext.ModelName, bindingContext)
                ?? base.BindModel(controllerContext, bindingContext);
    }

    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
    {
        return BindCsv(propertyDescriptor.PropertyType, propertyDescriptor.Name, bindingContext)
                ?? base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
    }

    private object BindCsv(Type type, string name, ModelBindingContext bindingContext)
    {
        if (type.GetInterface(typeof(IEnumerable).Name) != null)
        {
            var actualValue = bindingContext.ValueProvider.GetValue(name);

            if (actualValue != null)
            {
                var valueType = type.GetElementType() ?? type.GetGenericArguments().FirstOrDefault();

                if (valueType != null && valueType.GetInterface(typeof(IConvertible).Name) != null)
                {
                    var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(valueType));

                    foreach (var splitValue in actualValue.AttemptedValue.Split(new[] { ',' }))
                    {
                            if(!String.IsNullOrWhiteSpace(splitValue))
                                list.Add(Convert.ChangeType(splitValue, valueType));
                    }

                    if (type.IsArray)
                        return ToArrayMethod.MakeGenericMethod(valueType).Invoke(this, new[] { list });
                    else
                        return list;
                }
            }
        }

        return null;
    }
}
34
zvolkov

デフォルトのモデルバインダーは、単純な型リストが次の形式であると想定しています

name=value&name=value2&name=value3

組み込みのバインディングを使用するには、クエリ文字列を次のように変更する必要があります

Action?ids=4783&ids=5063&ids=5305

または、カスタムモデルバインダーを作成します。あなたは見るかもしれません 次の記事 (そこからのコード)

public class CommaSeparatedValuesModelBinder : DefaultModelBinder
{
    private static readonly MethodInfo ToArrayMethod = typeof(Enumerable).GetMethod("ToArray");

    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
    {
        if (propertyDescriptor.PropertyType.GetInterface(typeof(IEnumerable).Name) != null)
        {
            var actualValue = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);

            if (actualValue != null && !String.IsNullOrWhiteSpace(actualValue.AttemptedValue) && actualValue.AttemptedValue.Contains(","))
            {
                var valueType = propertyDescriptor.PropertyType.GetElementType() ?? propertyDescriptor.PropertyType.GetGenericArguments().FirstOrDefault();

                if (valueType != null && valueType.GetInterface(typeof(IConvertible).Name) != null)
                {
                    var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(valueType));

                    foreach (var splitValue in actualValue.AttemptedValue.Split(new[] { ',' }))
                    {
                        list.Add(Convert.ChangeType(splitValue, valueType));
                    }

                    if (propertyDescriptor.PropertyType.IsArray)
                    {
                        return ToArrayMethod.MakeGenericMethod(valueType).Invoke(this, new[] { list });
                    }
                    else
                    {
                        return list;
                    }
                }
            }
        }

        return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
    }
}
23
archil

私の答え から取得:

ここでは、今書いた(そして。Net Core 2.0でテストした)非常に単純なカスタムモデルバインダーを紹介します。

私のモデルバインダー:

public class CustomModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        var value = valueProviderResult.FirstValue; // get the value as string

        var model = value.Split(",");
        bindingContext.Result = ModelBindingResult.Success(model);

        return Task.CompletedTask;
    }
}

私のモデル(そして注意してください、1つのプロパティだけが私のカスタムモデルバインダーアノテーションを持っています):

public class CreatePostViewModel
{
    [Display(Name = nameof(ContentText))]
    [MinLength(10, ErrorMessage = ValidationErrors.MinLength)]
    public string ContentText { get; set; }

    [BindProperty(BinderType = typeof(CustomModelBinder))]
    public IEnumerable<string> Categories { get; set; } // <<<<<< THIS IS WHAT YOU ARE INTERESTER IN

    #region View Data
    public string PageTitle { get; set; }
    public string TitlePlaceHolder { get; set; }
    #endregion
}

つまり、「aaa、bbb、ccc」などのテキストを受け取り、それを配列に変換して、ViewModelに返します。

それがお役に立てば幸いです。

免責事項:私はモデルバインダーの作成の専門家ではありません。15分前にそのことを知りました。あなたの質問を見つけたので(役立つ回答はありません)、助けようとしました。これは非常に基本的なモデルバインダーであり、いくつかの改善が確実に必要です。 公式ドキュメント ページから書き方を学びました。

2