ASP.NET MVC 3: DefaultModelBinder z dziedziczeniem / polimorfizmem

Po pierwsze, przepraszam za duży post (próbowałem najpierw trochę poszperać) i za mieszankę technologii na to samo pytanie (ASP.NET MVC 3, Ninject i MvcContrib).

Opracowuję projekt z ASP.NET MVC 3 do obsługi niektórych zleceń klientów.

W skrócie: mam pewne obiekty odziedziczone z klasy and abstract Order i muszę je przeanalizować, gdy do kontrolera zostanie wysłane żądanie POST. Jak Mogę wybrać właściwy typ? Czy muszę nadpisać DefaultModelBinder zajęcia czy jest jakiś inny sposób na to? Czy ktoś może mi podać jakiś kod lub inne linki Jak to zrobić? Każda pomoc byłaby świetna! Jeśli post jest mylące mogę zrobić wszelkie zmiany, aby to jasne!

Więc mam następujące drzewo dziedziczenia dla zamówień, które muszę obsłużyć:

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 */ }

Wszystkie te klasy są generowane przez Entity Framework, więc nie będę ich modyfikować, ponieważ będę musiał zaktualizować model (wiem, że mogę je rozszerzyć). Również będzie więcej zamówień, ale wszystkie pochodzi z Order.

Mam ogólny widok (Create.aspx) w celu utworzenia porządku i ten widok wywołuje silnie wpisany widok częściowy dla każdego z dziedziczonych rozkazów(w tym przypadku OrderBottling i OrderFinishing). Zdefiniowałem metodę Create() dla żądania GET i inną dla żądania POST na klasie OrderController. Drugi jest następujący:

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

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

Teraz problem: kiedy otrzymuję żądanie POST z danymi z formularza, domyślny binder MVC próbuje utworzyć instancję Order obiektu, który jest OK, ponieważ typ metody jest taki. Ale ponieważ Order jest abstrakcyjny, nie może być utworzony, co powinno zrobić.

Pytanie: Jak mogę dowiedzieć się, który konkretny typ Order jest wysyłany przez Widok?

Szukałem już tutaj na Stack Overflow i sporo na ten temat wygooglowałem (pracuję nad tym problemem od około 3 dni!) i znalazłem kilka sposobów na rozwiązanie podobnych problemów, ale nie mogłem znaleźć czegoś podobnego do mojego prawdziwego problemu. Dwie opcje dla rozwiązanie tego:

  • override ASP.NET MVC DefaultModelBinder i użyj bezpośredniego wtrysku, aby odkryć, który typ jest Order;
  • Utwórz metodę dla każdego zamówienia (nie jest piękna i będzie problematyczna w utrzymaniu).

Nie próbowałem drugiej opcji, ponieważ uważam, że nie jest to właściwy sposób na rozwiązanie problemu. W przypadku pierwszej opcji próbowałem Ninject rozwiązać Typ zamówienia i utworzyć jego instancję. Mój moduł Ninject wygląda następująco:

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

I ' ve próbowałem uzyskać jeden z typów za pomocą metody Ninject Get<>(), ale mówi mi, że są to więcej niż jeden sposób na rozwiązanie tego typu. Rozumiem, że moduł nie jest dobrze zaimplementowany. Próbowałem również zaimplementować tak dla obu typów: Bind<Order>().To<OrderBottling>().WithPropertyInject("OrderTypeId", 2);, ale ma ten sam problem... Jaki byłby właściwy sposób wdrożenia tego modułu?

Próbowałem również użyć segregatora modelowego MvcContrib. Zrobiłem to:

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

I na Global.asax.cs zrobiłem to:

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

    RegisterRoutes(RouteTable.Routes);

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

Ale to rzuca wyjątek: System.MissingMethodException: nie można utworzyć klasy abstrakcyjnej . Więc zakładam, że segregator nie jest lub nie może rozwiązać się na właściwy typ.

[23]} Z góry dziękuję bardzo!

Edit: przede wszystkim dziękuję Martinowi i Jasonowi za odpowiedzi i przepraszam za opóźnienie! Próbowałem obu podejść i oba zadziałały! Zaznaczyłem odpowiedź Martina jako poprawną, ponieważ jest bardziej elastyczna i spełnia niektóre potrzeby mojego projektu. W szczególności Identyfikatory dla każdego requesty są przechowywane w bazie danych i umieszczenie ich na klasie może zepsuć oprogramowanie, jeśli zmienię ID tylko w jednym miejscu (baza danych lub na klasie). Podejście Martina jest w tym względzie bardzo elastyczne.

@Martin: w kodzie zmieniłem linię

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

Do

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

Ponieważ moje klasy gdzie na innym projekcie (i tak, na innym montażu). Dzielę się tym, ponieważ wydaje się to bardziej elastyczne niż uzyskanie tylko wykonującego zestawu, który nie może rozwiązać typy na zespoły zewnętrzne. W moim przypadku wszystkie klasy Orderu są na tym samym zgromadzeniu. Nie jest to ani lepsze, ani magiczna formuła, ale myślę, że warto się tym podzielić;) {]}

Author: jmpcm, 2011-03-28

5 answers

Próbowałem już wcześniej zrobić coś podobnego i doszedłem do wniosku, że nie ma nic, co by sobie z tym poradziło.

Opcja, którą wybrałem, polegała na stworzeniu własnego segregatora modelu (choć odziedziczonego po domyślnym kodzie, więc nie ma za dużo kodu). Szukała wartości zwrotnej posta z nazwą typu o nazwie xxxConcreteType, gdzie xxx był innym typem, do którego była powiązana. Oznacza to, że pole musi zostać wysłane z powrotem z wartością typu, który próbujesz powiązać; w tym przypadku OrderConcreteType z wartością OrderBottling lub OrderFinishing.

Inną alternatywą jest użycie UpdateModel lub TryUpdateModel i pominięcie parametru z metody. Będziesz musiał określić, jakiego rodzaju model aktualizujesz przed wywołaniem tego (przez parametr lub w inny sposób) i utworzyć instancję klasy wcześniej, a następnie możesz użyć jednej z metod do popuplate it

Edit:

Oto kod..
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;
        }
    }
}

Zmień metodę działania, aby wyglądała jak to:

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

Musisz umieścić następujące w swojej opinii:

@Html.Hidden("orderType, "Namespace.xxx.OrderBottling")
 16
Author: Martin Booth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-29 00:44:02

Możesz utworzyć custome ModelBinder, który działa, gdy twoja akcja akceptuje określony typ i może utworzyć obiekt dowolnego typu, który chcesz zwrócić. Metoda CreateModel () pobiera Kontrolercontext i ModelBindingContext, które dają dostęp do parametrów przekazywanych przez route, url querystring i post, których możesz użyć do wypełnienia obiektu wartościami. Domyślna implementacja modelu Binder konwertuje wartości dla właściwości o tej samej nazwie, aby umieścić je w polach obiekt.

To, co robię, to po prostu sprawdzić jedną z wartości, aby określić, jaki typ utworzyć, a następnie wywołać DefaultModelBinder.Metoda CreateModel() przełącza typ, który ma zostać utworzony na odpowiedni typ.

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
    }
}

Ustaw, aby była używana, gdy masz parametr Order na swoich akcjach, dodając go do Application_Start () w funkcji Global.asax.cs:

ModelBinders.Binders.Add(typeof(Order), new OrderModelBinder());
 7
Author: Jason Goemaat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-29 21:24:14

Możesz również zbudować ogólny ModelBinder, który działa dla wszystkich Twoich abstrakcyjnych modeli. Moje rozwiązanie wymaga dodania ukrytego pola do widoku o nazwie "ModelTypeName" z wartością ustawioną na nazwę konkretnego typu, który chcesz. Jednak powinno być możliwe, aby uczynić to mądrzejszym i wybrać konkretny typ, dopasowując właściwości typu do pól w widoku.

W Twojej globalnej.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);
    }
}
 5
Author: Kelly,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-18 02:30:40

Moje rozwiązanie tego problemu obsługuje złożone modele, które mogą zawierać inne klasy abstrakcyjne, wielokrotne dziedziczenie, Kolekcje lub klasy generyczne.

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);
        }
    }
}

Jak widzisz musisz dodać pole (o nazwie typu), które zawiera informację jaka konkretna Klasa dziedzicząca z klasy abstrakcyjnej powinna zostać utworzona. Na przykład klasy: class abstract Content, Klasa TextContent , zawartość powinna mieć Typ ustawiony na "TextContent". Pamiętaj, aby zmienić domyślny model binder w global.asax:

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

Aby uzyskać więcej informacji i przykładowy projekt sprawdź link .

 2
Author: MaciejLisCK,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-05-11 10:42:38

Zmień linię:

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

Do tego:

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

Jest to naiwna implementacja, która sprawdza każdy zespół pod kątem typu. Jestem pewien, że są mądrzejsze sposoby, ale to działa wystarczająco dobrze.

 0
Author: Corey Cole,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-21 03:21:14