I'm using AngularJS to manipulate a fairly complex parent object with children that need to behave quite differently server-side. Based on this answer, which appears pretty solid, I've created the test case below. The issue I'm running into is that whenever I enter the CreateModel
function, any call to bindingContext.ValueProvider.GetValue(key)
returns null. I've checked all values in the debugger. The object type appears to be loaded, but no values have yet been bound.
My Models:
public class Menagerie
{
public Menagerie()
{
Critters = new List<Creature>();
}
public string MakeNoise()
{
return String.Join(" ", Critters.Select(c => c.MakeNoise()));
}
public List<Creature> Critters { get; set; }
}
public class Tiger : Creature
{
public Tiger() { }
public override CreatureType Type => CreatureType.Tiger;
public override string Sound => "ROAR";
}
public class Kitty : Creature
{
public Kitty() { }
public override CreatureType Type => CreatureType.Kitty;
public override string Sound => "meow";
}
public class Creature
{
public Creature() { }
public string Name { get; set; }
public virtual CreatureType Type { get; set; }
public virtual string Sound { get; }
public string MakeNoise()
{
return $"{Name} says {Sound ?? "nothing"}.";
}
public static Type SelectFor(CreatureType type)
{
switch (type)
{
case CreatureType.Tiger:
return typeof(Tiger);
case CreatureType.Kitty:
return typeof(Kitty);
default:
throw new Exception();
}
}
}
public enum CreatureType
{
Tiger,
Kitty,
}
public class CreatureModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
CreatureType creatureType = GetValue<CreatureType>(bindingContext, "Type");
Type model = Creature.SelectFor(creatureType);
Creature instance = (Creature)base.CreateModel(controllerContext, bindingContext, model);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, model);
return instance;
}
private T GetValue<T>(ModelBindingContext bindingContext, string key)
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(key); // valueResult is null
bindingContext.ModelState.SetModelValue(key, valueResult);
return (T)valueResult.ConvertTo(typeof(T)); // NullReferenceException
}
}
My script:
(function () {
'use strict';
angular.module('app', []).controller('CritterController', ['$scope', '$http', function ($scope, $http) {
$http.post('/a/admin/get-menagerie', { }).success(function (data) {
$scope.menagerie = data.menagerie;
});
$scope.makeNoise = function () {
$http.post('/a/admin/make-noise', { menagerie: $scope.menagerie }).success(function (data) {
$scope.message = data.message;
});
}
}]);
})();
Things I've tried
I've tried just using a string to indicate the class name, as in this answer and this one. However, the call to bindingContext.ValueProvider.GetValue(key)
still returns null, resulting in a NullReferenceException
.
I also checked to ensure the model is binding properly. When I change my CreatureModelBinder
to the following, everything maps fine. But each creature loses its inherited type and becomes a Creature
.
public class CreatureModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
return base.CreateModel(controllerContext, bindingContext, modelType);
}
} // MakeNoise returns: "Shere Khan says nothing. Mr. Boots says nothing. Dr. Evil says nothing."