3

I am trying to make a binder for an abstract class. The binder decides which implementation of the class to use.

public abstract class Pet
{
    public string name { get; set; }
    public string species { get; set; }
    abstract public string talk { get; }
}

public class Dog : Pet
{
    override public string talk { get { return "Bark!"; } }
}
public class Cat : Pet
{
    override public string talk { get { return "Miaow."; } }
}
public class Livestock : Pet
{
    override public string talk { get { return "Mooo. Mooo. Fear me."; } }
}

So I have a controller which takes a Pet, the binder decides (depending on the species string) if it is a Dog, Cat or Livestock.

public class PetBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var values = (ValueProviderCollection)bindingContext.ValueProvider;
        var name = (string)values.GetValue("name").ConvertTo(typeof(string));
        var species = (string)values.GetValue("species").ConvertTo(typeof(string));
        if (species == "dog")
        {
            return new Dog { name = name, species = "dog" };
        }
        else if (species == "cat")
        {
            return new Cat { name = name, species = "cat" };
        }
        else
        {
            return new Livestock { name = name, species = species };
        }
    }
}


public class HomeController : Controller
{
    public JsonResult WorksFine(Pet pet)
    {
        return Json(pet);
    }

    public JsonResult DoesntWork(List<Pet> pets)
    {
        return Json(pets);
    }
}

This works well, but as soon as the pet is in another structure (like List<Pet> or another object), I get a NullReferenceException (on the line var name = (string)values.GetValue("name").ConvertTo(typeof(string)); in the PetBinder). What am I doing wrong?

I added a Person class to test. It also gave me a NullReferenceException.

public class Person
{
    public string name { get; set; }
    public Pet pet { get; set; }
}
public class HomeController : Controller
{
    public JsonResult PersonAction(Person p)
    {
        return Json(p);
    }
}

ccurrens said the reason var name = (string)values.GetValue("name").ConvertTo(typeof(string)); returned null was because it couldn't get the values from a list.

I see they are named [n].name and [n].species when in a List<Pet>, but when in the Person object they are named pet.name and pet.species and when they are in a single Pet, they are just named name and species.

Solution

To get the parameter names with the right prefix ([n] or pet or anything else) for GetValue, I used the following code:

bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
string prefix = ((hasPrefix)&&(bindingContext.ModelName!="")) ? bindingContext.ModelName + "." : "";

If anyone is interested, I ended up inheriting from DefaultModelBinder using something similar to this answer. Here is the full code I used:

public class DefaultPetBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext,ModelBindingContext bindingContext,Type modelType)
    {
        //https://stackoverflow.com/questions/5460081/asp-net-mvc-3-defaultmodelbinder-inheritance-problem

        bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
        string prefix = ((hasPrefix)&&(bindingContext.ModelName!="")) ? bindingContext.ModelName + "." : "";

        // get the parameter species
        ValueProviderResult result;
        result = bindingContext.ValueProvider.GetValue(prefix+"species");


        if (result.AttemptedValue.Equals("cat"))
            return base.CreateModel(controllerContext,bindingContext,typeof(Cat));
        else if (result.AttemptedValue.Equals("dog"))
            return base.CreateModel(controllerContext,bindingContext,typeof(Dog));
        return base.CreateModel(controllerContext, bindingContext, typeof(Livestock)); // livestock
    }  

}

1 Answer 1

0

In the line you're getting your error, values could be null or what GetValue("name") returns could be null.

I'm assuming when you're calling the List<Pet> method, ValueProvider is returning the entire List instead of each individual Pet, so it can't get the value "name" since it doesn't exist in the List class.

I can't be more sure without seeing more code.

Sign up to request clarification or add additional context in comments.

1 Comment

Do you mean you would like to see the stack trace? Except for adding the PetBinder in Global.asax, there is nothing else. I made a Person class with a name and a single Pet, and in the debugger I saw that the properties in the ValueProvider were named pet.name, so I put GetValue(bindingContext.ModelName + ".name"), which returns undefined when in a list (the properties are [n].name and [n].species)and the right answer in the Person object. How am I supposed to get the properties in a way that works all the time?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.