web-dev-qa-db-ja.com

多相モデルのバインディング

MVCの以前のバージョンでは、この質問は 以前に尋ねた でした。また、問題を回避する方法について このブログエントリ があります。 MVC3が役立つかもしれない何かを導入しているかどうか、または他のオプションがあるかどうか疑問に思っています。

手短に。状況は次のとおりです。抽象基本モデルと2つの具象サブクラスがあります。 EditorForModel()でモデルをレンダリングする強く型付けされたビューがあります。次に、各具体的なタイプをレンダリングするカスタムテンプレートがあります。

問題は投稿時に発生します。ポストアクションメソッドに基本クラスをパラメーターとして使用させると、MVCはその抽象バージョンを作成できません(とにかく望まず、実際の具象型を作成したいと思います)。パラメーターシグネチャによってのみ異なる複数のポストアクションメソッドを作成すると、MVCはそれがあいまいだと文句を言います。

私が知る限り、この問題を解決する方法にはいくつかの選択肢があります。さまざまな理由でそれらのいずれも好きではありませんが、ここにリストします:

  1. Darinがリンク先の最初の投稿で提案しているように、カスタムモデルバインダーを作成します。
  2. リンク先の2番目の投稿が示唆するように、ディスクリミネーター属性を作成します。
  3. タイプに基づいてさまざまなアクションメソッドに投稿する
  4. ???

基本的には非表示の構成であるため、1は好きではありません。コードに取り組んでいる他の開発者の中には、それを知らず、物事が変化したときに物事が壊れる理由を見つけようとして多くの時間を浪費する人もいます。

私は2が好きではありません。しかし、私はこのアプローチに傾いています。

3は、DRYに違反することを意味するため、好きではありません。

他の提案はありますか?

編集:

ダリンの方法を採用することにしましたが、少し変更を加えました。これを抽象モデルに追加しました:

_[HiddenInput(DisplayValue = false)]
public string ConcreteModelType { get { return this.GetType().ToString(); }}
_

次に、DisplayForModel()で非表示が自動的に生成されます。覚えておく必要があるのは、DisplayForModel()を使用していない場合、自分で追加する必要があるということだけです。

56

私は明らかにオプション1(:-))を選択しているので、少し詳しく説明してbreakableになり、具体的なインスタンスをハードコーディングしないようにしますモデルバインダーに。アイデアは、具象タイプを隠しフィールドに渡し、反射を使用して具象タイプをインスタンス化することです。

次のビューモデルがあるとします。

public abstract class BaseViewModel
{
    public int Id { get; set; }
}

public class FooViewModel : BaseViewModel
{
    public string Foo { get; set; }
}

次のコントローラー:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new FooViewModel { Id = 1, Foo = "foo" };
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(BaseViewModel model)
    {
        return View(model);
    }
}

対応するIndexビュー:

@model BaseViewModel
@using (Html.BeginForm())
{
    @Html.Hidden("ModelType", Model.GetType())    
    @Html.EditorForModel()
    <input type="submit" value="OK" />
}

そしてその ~/Views/Home/EditorTemplates/FooViewModel.cshtmlエディターテンプレート:

@model FooViewModel
@Html.EditorFor(x => x.Id)
@Html.EditorFor(x => x.Foo)

これで、次のカスタムモデルバインダーを作成できました。

public class BaseViewModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
        var type = Type.GetType(
            (string)typeValue.ConvertTo(typeof(string)),
            true
        );
        if (!typeof(BaseViewModel).IsAssignableFrom(type))
        {
            throw new InvalidOperationException("Bad Type");
        }
        var model = Activator.CreateInstance(type);
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
        return model;
    }
}

実際の型は、ModelType隠しフィールドの値から推測されます。ハードコーディングされていないため、このモデルバインダーに触れることなく、後で他の子タイプを追加できます。

これと同じテクニックは、ベースビューモデルのコレクションに 簡単に適用 にすることができます。

59
Darin Dimitrov

この問題に対する興味深い解決策を考えたところです。次のようなパラメーターbsedモデルバインディングを使用する代わりに:

[HttpPost]
public ActionResult Index(MyModel model) {...}

代わりにTryUpdateModel()を使用して、コードでバインドするモデルの種類を決定できます。たとえば、私はこのようなことをします:

[HttpPost]
public ActionResult Index() {...}
{
    MyModel model;
    if (ViewData.SomeData == Something) {
        model = new MyDerivedModel();
    } else {
        model = new MyOtherDerivedModel();
    }

    TryUpdateModel(model);

    if (Model.IsValid) {...}

    return View(model);
}

とにかくこれは実際にはるかにうまく機能します。私が何らかの処理をしている場合は、モデルを実際に何でもキャストするか、isを使用して正しいマップを見つけて呼び出す必要があるためですAutoMapper。

1日目からMVCを使用していない私たちは、UpdateModelTryUpdateModelを忘れていますが、その用途はまだあります。

14

密接に関連する問題への答えを見つけるのに良い日がかかりました-それが正確に同じ問題であるかどうかはわかりませんが、他の人が同じ正確な問題の解決策を探している場合に備えてここに投稿しています。

私の場合、さまざまなビューモデルタイプの抽象ベースタイプがあります。メインビューモデルでは、抽象ベースタイプのプロパティがあります。

class View
{
    public AbstractBaseItemView ItemView { get; set; }
}

AbstractBaseItemViewには多くのサブタイプがあり、その多くは独自の排他的なプロパティを定義しています。

私の問題は、モデルバインダーがView.ItemViewにアタッチされたオブジェクトのタイプではなく、AbstractBaseItemViewである宣言されたプロパティタイプのみを見て、バインドすることを決定することですonly抽象型で定義されたプロパティ。使用中のAbstractBaseItemViewの具象型に固有のプロパティを無視します。

これの回避策はきれいではありません:

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

// ...

public class ModelBinder : DefaultModelBinder
{
    // ...

    override protected ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType.IsAbstract && bindingContext.Model != null)
        {
            var concreteType = bindingContext.Model.GetType();

            if (Nullable.GetUnderlyingType(concreteType) == null)
            {
                return new AssociatedMetadataTypeTypeDescriptionProvider(concreteType).GetTypeDescriptor(concreteType);
            }
        }

        return base.GetTypeDescriptor(controllerContext, bindingContext);
    }

    // ...
}

この変更はハッキングを感じ、非常に「体系的」ですが、うまくいくようです-そして、私が理解できる限り、それはかなりのセキュリティリスクを引き起こしませんnot CreateModel( )notを使用すると、何でも投稿して、モデルバインダーをだまして任意のオブジェクトのみを作成できます。

また、宣言されたproperty-typeがabstractタイプの場合にのみ機能します。抽象クラスまたはインターフェース。

関連するメモでは、ここで見たCreateModel()をオーバーライドする他の実装は、おそらく完全に新しいオブジェクトを投稿するときにonly動作するだけで、同じ問題に悩まされることに気付きました宣言されたproperty-typeが抽象型の場合に遭遇しました。そのため、既存モデルオブジェクトの具体的なタイプの特定のプロパティ編集はできませんが、新しいオブジェクトのみを作成できます。

言い換えると、おそらく、この回避策をバインダーに統合して、バインドする前にビューモデルに追加されたオブジェクトを適切に編集できるようにする必要があります...個人的に、私はそれがより安全なアプローチだと感じていますどの具象型を追加するかを制御します。したがって、コントローラ/アクションは、プロパティに空のインスタンスを入力するだけで、間接的にバインドできる具象型を指定できます。

これが他の人に役立つことを願っています...

7
mindplay.dk

Darinの方法を使用して、ビュー内の非表示フィールドを介してモデルタイプを区別します。カスタムRouteHandlerを使用してモデルタイプを区別し、各タイプをコントローラー上の一意の名前のアクションに向けることをお勧めします。たとえば、コントローラーのCreateアクションに対して2つの具象モデルFooとBarがある場合、CreateFoo(Foo model)アクションとCreateBar(Bar model)アクションを作成します。次に、次のようにカスタムRouteHandlerを作成します。

_public class MyRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        var httpContext = requestContext.HttpContext;
        var modelType = httpContext.Request.Form["ModelType"]; 
        var routeData = requestContext.RouteData;
        if (!String.IsNullOrEmpty(modelType))
        {
            var action = routeData.Values["action"];
            routeData.Values["action"] = action + modelType;
        }
        var handler = new MvcHandler(requestContext);
        return handler; 
    }
}
_

次に、Global.asax.csで、RegisterRoutes()を次のように変更します。

_public static void RegisterRoutes(RouteCollection routes) 
{ 
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 

    AreaRegistration.RegisterAllAreas(); 

    routes.Add("Default", new Route("{controller}/{action}/{id}", 
        new RouteValueDictionary( 
            new { controller = "Home",  
                  action = "Index",  
                  id = UrlParameter.Optional }), 
        new MyRouteHandler())); 
} 
_

次に、作成リクエストが届くと、返されたフォームでModelTypeが定義されている場合、RouteHandlerはアクション名にModelTypeを追加し、各具体的なモデルに一意のアクションを定義できるようにします。

4
counsellorben