19

I have an Action function inside of a Controller, which is being called with AJAX. That Action is taking in 1 parameter. Client side, I construct a JSON object which should serialize into that 1 parameter. The problem I ran into is that the parameter class is declared as abstract. Thus, it cannot be instantiated.

When AJAX hits that Action, I get the following:

Cannot create an abstract class.

Stack Trace:

[MissingMethodException: Cannot create an abstract class.]
System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck) +0
System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache) +98
System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipVisibilityChecks, Boolean skipCheckThis, Boolean fillCache) +241 System.Activator.CreateInstance(Type type, Boolean nonPublic) +69 ...............

Is there any way to pull off such a scenario without creating a different parameter object, "un-declaring" the parameter object as abstract, or digging into mechanics of MVC?

Update

I'm currently working with back-end developers to tweak their objects. Either way, I think that would be the ultimate solution. Thank you all for your answers.

halfer
  • 19,824
  • 17
  • 99
  • 186
Dimskiy
  • 5,233
  • 13
  • 47
  • 66
  • 4
    I think the parameter has to be a concrete type.No abstract types or interface type are allowed. – Bala R May 02 '11 at 19:04

5 Answers5

26

Update: Example now uses a AJAX JSON POST

If you must use an abstract type, you could provide a custom model binder to create the concrete instance. An example is shown below:

Model / Model Binder

public abstract class Student
{
    public abstract int Age { get; set; }
    public abstract string Name { get; set; }
}
public class GoodStudent : Student
{
    public override int Age { get; set; }
    public override string Name { get; set; }
}
public class BadStudent : Student
{
    public override int Age { get; set; }
    public override string Name { get; set; }
}
public class StudentBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var values = (ValueProviderCollection) bindingContext.ValueProvider;
        var age = (int) values.GetValue("Age").ConvertTo(typeof (int));
        var name = (string) values.GetValue("Name").ConvertTo(typeof(string));
        return age > 10 ? (Student) new GoodStudent { Age = age, Name = name } : new BadStudent { Age = age, Name = name };
    }
}

Controller Actions

public ActionResult Index()
{
    return View(new GoodStudent { Age = 13, Name = "John Smith" });
}
[HttpPost]
public ActionResult Index(Student student)
{
    return View(student);
}

View

@model AbstractTest.Models.Student

@using (Html.BeginForm())
{
    <div id="StudentEditor">
        <p>Age @Html.TextBoxFor(m => m.Age)</p>
        <p>Name @Html.TextBoxFor(m => m.Name)</p>
        <p><input type="button" value="Save" id="Save" /></p>
    </div>
}

<script type="text/javascript">
    $('document').ready(function () {
        $('input#Save').click(function () {
            $.ajax({
                url: '@Ajax.JavaScriptStringEncode(Url.Action("Index"))',
                type: 'POST',
                data: GetStudentJsonData($('div#StudentEditor')),
                contentType: 'application/json; charset=utf-8',
                success: function (data, status, jqxhr) { window.location.href = '@Url.Action("Index")'; }
            });
        });
    });

    var GetStudentJsonData = function ($container) {
             return JSON.stringify({
                 'Age': $container.find('input#Age').attr('value'),
                 'Name': $container.find('input#Name').attr('value')
             });
         };
</script>

Added to Global.asax.cs

protected void Application_Start()
{
    ...
    ModelBinders.Binders.Add(new KeyValuePair<Type, IModelBinder>(typeof(Student), new StudentBinder()));
}
Jim G.
  • 15,141
  • 22
  • 103
  • 166
Joseph Sturtevant
  • 13,194
  • 12
  • 76
  • 90
4

You will have to create a child class of the abstract class and pass that instead. Abstract classes are fundamentally not permitted to be instantiated by themselves. However, if you have a C# method like:

protected void Foo(MyAbstractClass param1)

...then you can still pass Foo an instance of a type that derives from MyAbstractClass. So you can create a concrete child class MyChildClass : MyAbstractClass and pass that to your method, and it should still work. You won't have to change the Foo method, but you will need some access to the C# code so you can create MyChildClass.

If you're working with generics -- for example, if your method signature is:

protected void Foo(IEnumerable<MyAbstractClass> param1)

...then it becomes more complicated, and you'll want to look into covariance and contravariance in C# generics.

Justin Morgan - On strike
  • 30,035
  • 12
  • 80
  • 104
  • right what i was thinking, so I wrote a duplicate answer more or less, but +1 :) – Marino Šimić May 02 '11 at 19:17
  • I actually have a List and yes, I do have access to C#, but only inside of my MVC project. I can't touch business/DAL layers. – Dimskiy May 02 '11 at 19:31
  • @Dimskiy - In that case, you may still be able to pass in a `List` populated with `ThatDerivedClass` objects. You *may* be able to pass in a `List` if the method allows covariance, but I'm not certain. It would be good if you could write a C# wrapper to catch the JSON data and repackage it before sending it to your business/DAL layers. – Justin Morgan - On strike May 02 '11 at 19:50
  • Whoever downvoted this, please explain why. I have read and reread it and I see no reason why it's incorrect. – Justin Morgan - On strike Sep 26 '11 at 04:59
4

The framework has no way of knowing which specific implementation you want, neither it will take the responsibility of such decision. So you have two possibilities here:

  1. Use a concrete type as action parameter
  2. Write a custom model binder for this abstract class which based on some request parameters will return a specific instance.
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
0

If you have access to the controller could you add another class that inherits the abstract class without specifing any member, and use that to serialize deserialize, and then return its base onto the other layer?

I know this is not good practice but some hack however I don't know if abstract classes can be serialized in some way.

Marino Šimić
  • 7,318
  • 1
  • 31
  • 61
-2

Nope - it makes no sense to have attempt to deserialize JSON to an object of an abstract class. Can't you make it a proper class?

Will A
  • 24,780
  • 5
  • 50
  • 61
  • These objects come from another layer in the app, they're pretty much given to me. I don't have any power over them. So, I guess I'd just make my own object and then populate the abstract one with the data from my own :( – Dimskiy May 02 '11 at 19:08
  • 1
    @Dimskiy - You could always inherit your class from the abstract object. – Will A May 02 '11 at 19:11