web-dev-qa-db-ja.com

Asp.Net MVC 2-モデルのプロパティを別の名前付き値にバインドする

(2016年9月21日)更新-Digbyswiftにコメントしてくれてありがとうこのソリューションは引き続きMVC5でも機能します。

更新(2012年4月30日)-これにつまずいた人々への注意検索などからの質問-受け入れられた答えは、私がこれをどうやってやったかではありません-しかし、私はそれを受け入れたままにしました。 私自身の答えには私が使用した最終的な解決策が含まれています 、これは再利用可能であり、あらゆるプロジェクトに適用されます。

MVCフレームワークのv3およびv4でも動作することが確認されています。

次のモデルタイプがあります(クラスとそのプロパティの名前は、IDを保護するために変更されています)。

public class MyExampleModel
{
  public string[] LongPropertyName { get; set; }
}

このプロパティは、チェックボックスの束(> 150)にバインドされ、各チェックボックスの入力名はもちろんLongPropertyNameです。

フォームはHTTP GETを使用してurlに送信し、ユーザーがこれらのチェックボックスのうち3つを選択すると言います-URLにはクエリ文字列?LongPropertyName=a&LongPropertyName=b&LongPropertyName=c

大きな問題は、すべてのチェックボックス(または半分以上)を選択すると、IISのリクエストフィルターによって強制される最大クエリ文字列の長さを超えることです!

私はそれを拡張したくありません-したがって、このクエリ文字列を切り詰める方法が必要です(POSTに変更できることはわかっていますが、それでも、クライアントによって送信されたデータのフラッフ)。

私がやりたいのは、LongPropertyNameを単に 'L'にバインドして、クエリ文字列が?L=a&L=b&L=c butコード内のプロパティ名を変更せずに

問題の型には既にカスタムモデルバインダー(DefaultModelBinderから派生)がありますが、その基本クラスにアタッチされているため、派生クラスのコードをそこに入れたくありません。現在、すべてのプロパティバインディングは、標準のDefaultModelBinderロジックによって実行されます。このロジックでは、System.ComponentModelのTypeDescriptorsやProperty Descriptorsなどを使用しています。

私はこの仕事をするためにプロパティに適用できる属性があるかもしれないとちょっと望んでいました-そこにありますか?または、ICustomTypeDescriptorの実装を検討する必要がありますか?

49
Andras Zoltan

BindAttribute を使用してこれを実現できます。

public ActionResult Submit([Bind(Prefix = "L")] string[] longPropertyName) {

}

更新

「longPropertyName」パラメーターはモデルオブジェクトの一部であり、コントローラーアクションの独立したパラメーターではないため、他にもいくつかの選択肢があります。

モデルとプロパティをアクションの独立したパラメーターとして保持し、アクションメソッドでデータを手動でマージできます。

public ActionResult Submit(MyModel myModel, [Bind(Prefix = "L")] string[] longPropertyName) {
    if(myModel != null) {
        myModel.LongPropertyName = longPropertyName;
    }
}

別のオプションは、(上記のように)パラメーター値の割り当てを手動で実行するカスタムモデルバインダーを実装することですが、それはおそらくやり過ぎです。興味のある方の例を次に示します。 フラグ列挙モデルバインダー

20
Nathan Taylor

Michaelalmの回答とリクエストに応えて、これが私がやったことです。 Nathanによって提案された解決策の1つが機能するはずだったので、私は主に礼儀から元の答えにチェックマークを付けたままにしました。

この出力はDefaultModelBinderクラスの置換であり、グローバルに登録する(すべてのモデルタイプがエイリアスを利用できるようにする)か、カスタムモデルバインダーを選択的に継承できます。

それはすべて、予想どおりに始まります:

/// <summary>
/// Allows you to create aliases that can be used for model properties at
/// model binding time (i.e. when data comes in from a request).
/// 
/// The type needs to be using the DefaultModelBinderEx model binder in 
/// order for this to work.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class BindAliasAttribute : Attribute
{
  public BindAliasAttribute(string alias)
  {
    //ommitted: parameter checking
    Alias = alias;
  }
  public string Alias { get; private set; }
}

そして、このクラスを取得します。

internal sealed class AliasedPropertyDescriptor : PropertyDescriptor
{
  public PropertyDescriptor Inner { get; private set; }

  public AliasedPropertyDescriptor(string alias, PropertyDescriptor inner)
    : base(alias, null)
  {
    Inner = inner;
  }

  public override bool CanResetValue(object component)
  {
    return Inner.CanResetValue(component);
  }

  public override Type ComponentType
  {
    get { return Inner.ComponentType; }
  }

  public override object GetValue(object component)
  {
    return Inner.GetValue(component);
  }

  public override bool IsReadOnly
  {
    get { return Inner.IsReadOnly; }
  }

  public override Type PropertyType
  {
    get { return Inner.PropertyType; }
  }

  public override void ResetValue(object component)
  {
    Inner.ResetValue(component);
  }

  public override void SetValue(object component, object value)
  {
    Inner.SetValue(component, value);
  }

  public override bool ShouldSerializeValue(object component)
  {
    return Inner.ShouldSerializeValue(component);
  }
}

これは、通常DefaultModelBinderによって検出される「適切な」PropertyDescriptorをプロキシしますが、その名前をエイリアスとして提示します。

次に、新しいモデルバインダークラスがあります。

public class DefaultModelBinderEx : DefaultModelBinder
{
  protected override System.ComponentModel.PropertyDescriptorCollection
    GetModelProperties(ControllerContext controllerContext, 
                      ModelBindingContext bindingContext)
  {
    var toReturn = base.GetModelProperties(controllerContext, bindingContext);

    List<PropertyDescriptor> additional = new List<PropertyDescriptor>();

    //now look for any aliasable properties in here
    foreach (var p in 
      this.GetTypeDescriptor(controllerContext, bindingContext)
      .GetProperties().Cast<PropertyDescriptor>())
    {
      foreach (var attr in p.Attributes.OfType<BindAliasAttribute>())
      {
        additional.Add(new AliasedPropertyDescriptor(attr.Alias, p));

        if (bindingContext.PropertyMetadata.ContainsKey(p.Name))
          bindingContext.PropertyMetadata.Add(attr.Alias,
                bindingContext.PropertyMetadata[p.Name]);
      }
    }

    return new PropertyDescriptorCollection
      (toReturn.Cast<PropertyDescriptor>().Concat(additional).ToArray());
  }
}

そして、技術的には、これですべてです。このSOの回答として投稿されたソリューションを使用して、このDefaultModelBinderExクラスをデフォルトとして登録できます。 asp.net MVCのデフォルトモデルバインダーを変更 、または、独自のモデルバインダーのベース。

バインダーの起動方法のパターンを選択したら、次のように単純にモデルタイプに適用します。

public class TestModelType
{
    [BindAlias("LPN")]
    //and you can add multiple aliases
    [BindAlias("L")]
    //.. ad infinitum
    public string LongPropertyName { get; set; }
}

このコードを選択した理由は、カスタムタイプ記述子で機能し、任意のタイプで機能できるものが必要だったからです。同様に、モデルのプロパティ値を取得する際に、値プロバイダーシステムを引き続き使用したかったのです。そのため、DefaultModelBinderがバインドを開始するときに表示するメタデータを変更しました。これは少し長めのアプローチですが、概念的には、メタデータレベルで正確にあなたがやりたいことをやっています。

ValueProviderに複数のエイリアス、またはエイリアスとその名前によるプロパティの値が含まれている場合、潜在的に興味深い、やや面倒な副作用があります。この場合、取得された値の1つのみが使用されます。ただし、objectsを操作しているだけでは、すべてをタイプセーフな方法でマージする方法を考えるのは困難です。ただし、これはフォームポストとクエリ文字列の両方で値を指定するのと似ています(そのシナリオでMVCが何をするのか正確にはわかりません)が、推奨されるプラクティスとは思いません。

もちろん、別の問題は、別のエイリアスに等しいエイリアス、または実際のプロパティの名前を作成してはならないことです。

一般に、CustomModelBinderAttributeクラスを使用して、モデルバインダーを適用します。これに関する唯一の問題は、モデルタイプから派生してバインディングの動作を変更する必要がある場合です。これは、MVCが実行する属性検索でCustomModelBinderAttributeが継承されるためです。

私の場合、これは大丈夫です。新しいサイトフレームワークを開発しており、これらの新しいタイプを満たすために他のメカニズムを使用して、ベースバインダーに新しい拡張性をプッシュできます。しかし、それはすべての人に当てはまるわけではありません。

84
Andras Zoltan

これはあなたのアンドラスに似た解決策でしょうか?回答も投稿してください。

コントローラー方式

public class MyPropertyBinder : DefaultModelBinder
{
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
    {
        base.BindProperty(controllerContext, bindingContext, propertyDescriptor);

        for (int i = 0; i < propertyDescriptor.Attributes.Count; i++)
        {
            if (propertyDescriptor.Attributes[i].GetType() == typeof(BindingNameAttribute))
            {                    
                // set property value.
                propertyDescriptor.SetValue(bindingContext.Model, controllerContext.HttpContext.Request.Form[(propertyDescriptor.Attributes[i] as BindingNameAttribute).Name]);
                break;
            }
        }
    }
}

属性

public class BindingNameAttribute : Attribute
{
    public string Name { get; set; }

    public BindingNameAttribute()
    {

    }
}

ViewModel

public class EmployeeViewModel
{                    

    [BindingName(Name = "txtName")]
    public string TestProperty
    {
        get;
        set;
    }
}

次に、コントローラーでバインダーを使用します

[HttpPost]
public ActionResult SaveEmployee(int Id, [ModelBinder(typeof(MyPropertyBinder))] EmployeeViewModel viewModel)
{
        // do stuff here
}

txtNameフォームの値はTestPropertyに設定する必要があります。

4
michaelalm

だから、私はこれを機能させることができなかった理由を理解しようとして一日のほとんどを費やしました。 _System.Web.Http.ApiController_から呼び出しを行っているので、上記のDefaultPropertyBinderソリューションを使用することはできませんが、代わりにIModelBinderクラスを使用する必要があります。

上記の@AndreasZoltanの基本的な作業を置き換えるために私が書いたクラスは次のとおりです。

_using System.Reflection;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using QueryStringAlias.Attributes;

namespace QueryStringAlias.ModelBinders
{
    public class AliasModelBinder : IModelBinder
    {
        private bool TryAdd(PropertyInfo pi, NameValueCollection nvc, string key, ref object model)
        {
            if (nvc[key] != null)
            {
                try
                {
                    pi.SetValue(model, Convert.ChangeType(nvc[key], pi.PropertyType));
                    return true;
                }
                catch (Exception e)
                {
                    Debug.WriteLine($"Skipped: {pi.Name}\nReason: {e.Message}");
                }
            }
            return false;
        }

        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            Type bt = bindingContext.ModelType;
            object model = Activator.CreateInstance(bt);
            string QueryBody = actionContext.Request.Content.ReadAsStringAsync().Result;
            NameValueCollection nvc = HttpUtility.ParseQueryString(QueryBody);

            foreach (PropertyInfo pi in bt.GetProperties())
            {
                if (TryAdd(pi, nvc, pi.Name, ref model))
                {
                    continue;
                };
                foreach (BindAliasAttribute cad in pi.GetCustomAttributes<BindAliasAttribute>())
                {
                    if (TryAdd(pi, nvc, cad.Alias, ref model))
                    {
                        break;
                    }
                }
            }
            bindingContext.Model = model;
            return true;
        }
    }
}
_

これがWebAPI呼び出しの一部として実行されるようにするには、WebApiConfigのRegiser部分にconfig.BindParameter(typeof(TestModelType), new AliasModelBinder());も追加する必要があります。

このメソッドを使用している場合は、メソッドシグネチャから_[FromBody]_も削除する必要があります。

_    [HttpPost]
    [Route("mytestendpoint")]
    [System.Web.Mvc.ValidateAntiForgeryToken]
    public async Task<MyApiCallResult> Signup(TestModelType tmt) // note that [FromBody] does not appear in the signature
    {
        // code happens here
    }
_

この作業は、QueryStringAliasサンプルを使用して、上記の答えに基づいていることに注意してください。

現時点では、TestModelTypeに複雑なネストされた型がある場合、これはおそらく失敗します。理想的には他にもいくつかあります:

  • 複雑なネストされた型を堅牢に処理する
  • 登録ではなく、クラスの属性を有効にしてIModelBuilderをアクティブにします
  • 同じIModelBuilderをコントローラーとApiControllerの両方で機能させる

しかし今のところ、私は自分のニーズに満足しています。誰かがこの作品を役立ててくれることを願っています。

0
Alex C