17

まず、大きな投稿(最初にいくつかの調査を試みました)と同じ質問に関するテクノロジの組み合わせ(ASP.NET MVC 3、Ninject、およびMvcContrib)について申し訳ありません。

一部のクライアントの注文を処理するために、ASP.NETMVC3を使用してプロジェクトを開発しています。

つまり、抽象クラスから継承されたオブジェクトがいくつかOrderあり、コントローラーに対してPOSTリクエストが行われたときにそれらを解析する必要があります。正しいタイプを解決するにはどうすればよいですか?クラスをオーバーライドする必要がありますDefaultModelBinderか、それともこれを行う他の方法がありますか?誰かが私にこれを行う方法に関するいくつかのコードまたは他のリンクを提供できますか?どんな助けでも素晴らしいでしょう!投稿がわかりにくい場合は、明確にするために変更を加えることができます。

したがって、処理する必要のある注文に対して、次の継承ツリーがあります。

public abstract partial class Order {

    public Int32 OrderTypeId {get; set; }

    /* rest of the implementation ommited */
}

public class OrderBottling : Order { /* implementation ommited */ }

public class OrderFinishing : Order { /* implementation ommited */ }

このクラスはすべてEntityFrameworkによって生成されるため、モデルを更新する必要があるため、変更しません(拡張できることはわかっています)。また、より多くの注文がありますが、すべてがから派生していOrderます。

注文を作成するための汎用ビュー( )があり、このビューは、継承された注文(この場合はと)Create.aspxごとに強く型付けされた部分ビューを呼び出します。クラスでGETリクエスト用のメソッドとPOSTリクエスト用のメソッドを定義しました。2番目は次のようなものです。OrderBottlingOrderFinishingCreate()OrderController

public class OrderController : Controller
{
    /* rest of the implementation ommited */

    [HttpPost]
    public ActionResult Create(Order order) { /* implementation ommited */ }
}

ここで問題があります。フォームからのデータを含むPOSTリクエストを受信すると、MVCのデフォルトのバインダーがオブジェクトをインスタンス化しようとしますOrder。これは、メソッドのタイプがそれであるため問題ありません。しかし、Orderは抽象的であるため、インスタンス化することはできません。

質問:Orderビューによって送信された具体的なタイプをどのように見つけることができますか?

私はすでにStackOverflowでここを検索し、これについて多くのことをグーグルで検索し(私はこの問題に約3日間取り組んでいます!)、いくつかの同様の問題を解決するいくつかの方法を見つけましたが、私の本当のようなものは見つかりませんでした問題。これを解決するための2つのオプション:

  • ASP.NET MVCDefaultModelBinderをオーバーライドし、ダイレクトインジェクションを使用してOrder;がどのタイプであるかを検出します。
  • 注文ごとにメソッドを作成します(美しくなく、維持するのに問題があります)。

問題を解決する正しい方法ではないと思うので、2番目のオプションは試していません。最初のオプションでは、注文のタイプを解決してインスタンス化するためにNinjectを試しました。私のNinjectモジュールは次のようなものです。

private class OrdersService : NinjectModule
{
    public override void Load()
    {
        Bind<Order>().To<OrderBottling>();
        Bind<Order>().To<OrderFinishing>();
    }
}

NinjectのGet<>()メソッドを使用して、タイプの1つを取得しようとしましたが、タイプを解決する方法は1つではないことがわかります。そのため、モジュールが適切に実装されていないことを理解しています。私も両方のタイプにこのように実装しようとしました:Bind<Order>().To<OrderBottling>().WithPropertyInject("OrderTypeId", 2);、しかしそれは同じ問題を抱えています...このモジュールを実装する正しい方法は何でしょうか?

また、MvcContribモデルバインダーを使用してみました。私はこれをしました:

[DerivedTypeBinderAware(typeof(OrderBottling))]
[DerivedTypeBinderAware(typeof(OrderFinishing))]
public abstract partial class Order { }

そしてGlobal.asax.cs私はこれをしました:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterRoutes(RouteTable.Routes);

    ModelBinders.Binders.Add(typeof(Order), new DerivedTypeModelBinder());
}

ただし、これにより例外がスローされます。System.MissingMethodException:抽象クラスを作成できません。したがって、バインダーが正しいタイプに解決されないか、解決できないと思います。

よろしくお願いします!

編集:まず第一に、あなたの答えをマーティンとジェイソンに感謝し、遅れて申し訳ありません!私は両方のアプローチを試しましたが、両方ともうまくいきました!マーティンの答えはより柔軟で、私のプロジェクトのニーズのいくつかを満たしているので、正しいとマークしました。具体的には、各リクエストのIDはデータベースに保存されており、IDを1か所(データベースまたはクラス)でのみ変更すると、それらをクラスに配置するとソフトウェアが破損する可能性があります。マーティンのアプローチは、その点で非常に柔軟です。

@Martin:私のコードで行を変更しました

var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue);

var concreteType = Assembly.GetAssembly(typeof(Order)).GetType(concreteTypeValue.AttemptedValue);

私のクラスは別のプロジェクト(そして別のアセンブリ)にあるからです。これを共有しているのは、外部アセンブリの型を解決できない実行中のアセンブリのみを取得するよりも柔軟性があるように見えるためです。私の場合、すべての注文クラスは同じアセンブリにあります。それは良くも魔法の公式でもありませんが、これを共有するのは面白いと思います;)

4

5 に答える 5

18

私は以前に同様のことを試みましたが、これを処理するものは何も組み込まれていないという結論に達しました。

私が行ったオプションは、独自のモデルバインダーを作成することでした(ただし、デフォルトから継承されているため、コードが多すぎません)。xxxConcreteTypeというタイプの名前のポストバック値を探しました。xxxはバインド先の別のタイプです。これは、バインドしようとしているタイプの値をフィールドにポストバックする必要があることを意味します。この場合、OrderBottlingまたはOrderFinishingのいずれかの値を持つOrderConcreteType。

もう1つの方法は、UpdateModelまたはTryUpdateModelを使用して、メソッドからパラメーターを省略することです。これを呼び出す前に(パラメーターまたはその他の方法で)更新するモデルの種類を判別し、事前にクラスをインスタンス化する必要があります。その後、いずれかのメソッドを使用してポップアップできます。

編集:

これがコードです。

public class AbstractBindAttribute : CustomModelBinderAttribute
{
    public string ConcreteTypeParameter { get; set; }

    public override IModelBinder GetBinder()
    {
        return new AbstractModelBinder(ConcreteTypeParameter);
    }

    private class AbstractModelBinder : DefaultModelBinder
    {
        private readonly string concreteTypeParameterName;

        public AbstractModelBinder(string concreteTypeParameterName)
        {
            this.concreteTypeParameterName = concreteTypeParameterName;
        }

        protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
        {
            var concreteTypeValue = bindingContext.ValueProvider.GetValue(concreteTypeParameterName);

            if (concreteTypeValue == null)
                throw new Exception("Concrete type value not specified for abstract class binding");

            var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue);

            if (concreteType == null)
                throw new Exception("Cannot create abstract model");

            if (!concreteType.IsSubclassOf(modelType))
                throw new Exception("Incorrect model type specified");

            var concreteInstance = Activator.CreateInstance(concreteType);

            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => concreteInstance, concreteType);

            return concreteInstance;
        }
    }
}

アクションメソッドを次のように変更します。

public ActionResult Create([AbstractBind(ConcreteTypeParameter = "orderType")] Order order) { /* implementation ommited */ }

あなたはあなたの見解に以下を置く必要があるでしょう:

@Html.Hidden("orderType, "Namespace.xxx.OrderBottling")
于 2011-03-28T14:34:22.170 に答える
7

アクションが特定のタイプを受け入れるときに動作するカスタムModelBinderを作成できます。また、返したいタイプのオブジェクトを作成できます。CreateModel()メソッドはControllerContextとModelBindingContextを取り、route、url querystring、postによって渡されたパラメーターにアクセスできるようにします。これらのパラメーターを使用して、オブジェクトに値を入力できます。デフォルトのモデルバインダー実装は、同じ名前のプロパティの値を変換して、オブジェクトのフィールドに配置します。

ここでは、値の1つをチェックして作成するタイプを決定し、DefaultModelBinder.CreateModel()メソッドを呼び出して、作成するタイプを適切なタイプに切り替えます。

public class OrderModelBinder : DefaultModelBinder
{
    protected override object CreateModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext,
        Type modelType)
    {
        // get the parameter OrderTypeId
        ValueProviderResult result;
        result = bindingContext.ValueProvider.GetValue("OrderTypeId");
        if (result == null)
            return null; // OrderTypeId must be specified

        // I'm assuming 1 for Bottling, 2 for Finishing
        if (result.AttemptedValue.Equals("1"))
            return base.CreateModel(controllerContext,
                    bindingContext,
                    typeof(OrderBottling));
        else if (result.AttemptedValue.Equals("2"))
            return base.CreateModel(controllerContext,
                    bindingContext,
                    typeof(OrderFinishing));
        return null; // unknown OrderTypeId
    }
}

Global.asax.csのApplication_Start()にこれを追加して、アクションにOrderパラメーターがある場合に使用するように設定します。

ModelBinders.Binders.Add(typeof(Order), new OrderModelBinder());
于 2011-03-29T01:11:44.633 に答える
5

すべての抽象モデルで機能する汎用ModelBinderを構築することもできます。私のソリューションでは、ビューに「ModelTypeName」という非表示のフィールドを追加し、値を目的の具象型の名前に設定する必要があります。ただし、タイププロパティをビューのフィールドに一致させることで、これをよりスマートにし、具体的なタイプを選択できるはずです。

Global.asax.cs Application_Start()の場合:

ModelBinders.Binders.DefaultBinder = new CustomModelBinder();

CustomModelBinder:

public class CustomModelBinder : DefaultModelBinder 
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        if (modelType.IsAbstract)
        {
            var modelTypeValue = controllerContext.Controller.ValueProvider.GetValue("ModelTypeName");
            if (modelTypeValue == null)
                throw new Exception("View does not contain ModelTypeName");

            var modelTypeName = modelTypeValue.AttemptedValue;

            var type = modelType.Assembly.GetTypes().SingleOrDefault(x => x.IsSubclassOf(modelType) && x.Name == modelTypeName);
            if(type == null)
                throw new Exception("Invalid ModelTypeName");

            var concreteInstance = Activator.CreateInstance(type);

            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => concreteInstance, type);

            return concreteInstance;

        }

        return base.CreateModel(controllerContext, bindingContext, modelType);
    }
}
于 2011-09-22T15:48:03.977 に答える
2

その問題に対する私の解決策は、他の抽象クラス、多重継承、コレクション、またはジェネリッククラスを含むことができる複雑なモデルをサポートします。

public class EnhancedModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        Type type = modelType;
        if (modelType.IsGenericType)
        {
            Type genericTypeDefinition = modelType.GetGenericTypeDefinition();
            if (genericTypeDefinition == typeof(IDictionary<,>))
            {
                type = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
            }
            else if (((genericTypeDefinition == typeof(IEnumerable<>)) || (genericTypeDefinition == typeof(ICollection<>))) || (genericTypeDefinition == typeof(IList<>)))
            {
                type = typeof(List<>).MakeGenericType(modelType.GetGenericArguments());
            }
            return Activator.CreateInstance(type);            
        }
        else if(modelType.IsAbstract)
        {
            string concreteTypeName = bindingContext.ModelName + ".Type";
            var concreteTypeResult = bindingContext.ValueProvider.GetValue(concreteTypeName);

            if (concreteTypeResult == null)
                throw new Exception("Concrete type for abstract class not specified");

            type = Assembly.GetExecutingAssembly().GetTypes().SingleOrDefault(t => t.IsSubclassOf(modelType) && t.Name == concreteTypeResult.AttemptedValue);

            if (type == null)
                throw new Exception(String.Format("Concrete model type {0} not found", concreteTypeResult.AttemptedValue));

            var instance = Activator.CreateInstance(type);
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, type);
            return instance;
        }
        else
        {
            return Activator.CreateInstance(modelType);
        }
    }
}

ご覧のとおり、抽象クラスから継承する具象クラスを作成する必要があるという情報を含む( Typeという名前の)フィールドを追加する必要があります。たとえば、クラス:class abstract Contentclass TextContent、ContentのTypeを「TextContent」に設定する必要があります。global.asaxでデフォルトのモデルバインダーを切り替えることを忘れないでください。

protected void Application_Start()
{
    ModelBinders.Binders.DefaultBinder = new EnhancedModelBinder();
    [...]

詳細およびサンプルプロジェクトについては、次のリンクを確認してください。

于 2012-05-11T10:35:35.160 に答える
0

行を変更します。

var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue);

これに:

            Type concreteType = null;
            var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (var assembly in loadedAssemblies)
            {
                concreteType = assembly.GetType(concreteTypeValue.AttemptedValue);
                if (null != concreteType)
                {
                    break;
                }
            }

これは、すべてのアセンブリでタイプをチェックする単純な実装です。よりスマートな方法があると確信していますが、これで十分に機能します。

于 2012-02-21T03:21:14.427 に答える