I have been working my way through Scott Guthrie's excellent post on ASP.NET MVC Beta 1. In it he shows the improvements made to the UpdateModel method and how they improve unit testing. I have recreated a similar project however anytime I run a UnitTest that contains a call to UpdateModel I receive an ArgumentNullException naming the controllerContext parameter.
Here's the relevant bits, starting with my model:
public class Country {
public Int32 ID { get; set; }
public String Name { get; set; }
public String Iso3166 { get; set; }
}
The controller action:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Int32 id, FormCollection form)
{
using ( ModelBindingDataContext db = new ModelBindingDataContext() ) {
Country country = db.Countries.Where(c => c.CountryID == id).SingleOrDefault();
try {
UpdateModel(country, form);
db.SubmitChanges();
return RedirectToAction("Index");
}
catch {
return View(country);
}
}
}
And finally my unit test that's failing:
[TestMethod]
public void Edit()
{
CountryController controller = new CountryController();
FormCollection form = new FormCollection();
form.Add("Name", "Canada");
form.Add("Iso3166", "CA");
var result = controller.Edit(2 /*Canada*/, form) as RedirectToRouteResult;
Assert.IsNotNull(result, "Expected to be redirected on successful POST.");
Assert.AreEqual("Show", result.RouteName, "Expected to redirect to the View action.");
}
ArgumentNullException is thrown by the call to UpdateModel with the message "Value cannot be null. Parameter name: controllerContext". I'm assuming that somewhere the UpdateModel requires the System.Web.Mvc.ControllerContext which isn't present during execution of the test.
I'm also assuming that I'm doing something wrong somewhere and just need to pointed in the right direction.
Help Please!
I don't think it can be done since TryUpdateModel, which UpdateModel uses, references the ControllerContext which is null when invoked from a unit test. I use RhinoMocks to mock or stub the various components needed by the controller.
var routeData = new RouteData();
var httpContext = MockRepository.GenerateStub<HttpContextBase>();
FormCollection formParameters = new FormCollection();
EventController controller = new EventController();
ControllerContext controllerContext =
MockRepository.GenerateStub<ControllerContext>( httpContext,
routeData,
controller );
controller.ControllerContext = controllerContext;
ViewResult result = controller.Create( formParameters ) as ViewResult;
Assert.AreEqual( "Event", result.Values["controller"] );
Assert.AreEqual( "Show", result.Values["action"] );
Assert.AreEqual( 0, result.Values["id"] );
Here's the relevant bit from the Controller.cs source on www.codeplex.com/aspnet:
protected internal bool TryUpdateModel<TModel>( ... ) where TModel : class
{
....
ModelBindingContext bindingContext =
new ModelBindingContext( ControllerContext,
valueProvider,
typeof(TModel),
prefix,
() => model,
ModelState,
propertyFilter );
...
}
I was having this same issue. After reading tvanfosson's solution, I tried a simple solution not involving a mock framework.
Add a default ControllerContext to the controller as follows:
CountryController controller = new CountryController();
controller.ControllerContext = new ControllerContext();
This removed the error just fine for me while unit testing. I hope this may help someone else out.
Or you can create form data proxy, like
public class CountryEdit {
public String Name { get; set; }
public String Iso3166 { get; set; }
}
Plus. Easy create unit tests
Plus. Define white list of fields update from post
Plus. Easy setup validation rules, easy test it.
Minus. You should move date from proxy to you model
So Controller.Action should look, like
public ActionResult Edit(Int32 id, CountryEdit input)
{
var Country = input.ToDb();
// Continue your code
}
Related
I have a navigation bar, with several links, like this:
MenuItem1
This request would hit my action method:
public ActionResult Browse(int departmentId)
{
var complexVM = MyCache.GetComplexVM(departmentId);
return View(complexVM);
}
This is my ComplexVM:
public class ComplexVM
{
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
}
MyCache, is a static list of departments, which I am keeping in memory, so when user passes in DepartmentId, I wouldn't need to get the corresponding DepartmentName from DB.
This is working fine... but it would be nice if I could somehow initialize ComplexVM in custom model binder, instead of initializing it in the Controller... so I still want to use a link (menu item), but this time, a CustomModelBinder binds my parameter, 2, to ComplexVM: it needs to look up the name of department with id = 2 from MyCache and initialize ComplexVM, then ComplexVM would be passed to this action method:
public ActionResult Browse(ComplexVM complexVM)
{
return View(complexVM);
}
I want to hit the above controller without doing a post-back, as I have a lot of menu item links in my navigation bar... not sure if this is possible? Or if this is even a good idea?
I have seen this link, which sort of describes what I want... but I am not sure how the routing would work... i.e. routing id:2 => ComplexVM
Alternatively would it be possible to do this in RouteConfig, something like this:
routes.MapRoute(
name: "Browse",
url: "{controller}/Browse/{departmentId}",
// this does not compile, just want to explain what I want...
defaults: new { action = "Browse", new ComplexVM(departmentId) });
I can achieve this with little change and with one trick
MenuItem1
Controller action
public ActionResult Browse(ComplexVM complexVM)
{
return View(complexVM);
}
View model
public class ComplexVM
{
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
public ComplexVM()
{
this.DepartmentId = System.Convert.ToInt32(HttpContext.Current.Request("id").ToString);
this.DepartmentName = "Your name from cache"; // Get name from your cache
}
}
This is without using model binder. Trick may help.
That is possible. It is also a good idea :) Off-loading parts of the shared responsibility to models / action filters is great. The only problem is because they are using some special classes to inherit from, testing them sometimes might be slightly harder then just testing the controller. Once you get the hang of it - it's better.
Your complex model should look like
// Your model class
[ModelBinder(typeof(ComplexVMModelBinder)]
public class ComplexVMModel
{
[Required]
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
}
// Your binder class
public class ComplexVMModelBinder : IModelBinder
{
// Returns false if you can't bind.
public bool BindModel(HttpActionContext actionContext, ModelBindingContext modelContext)
{
if (modelContext.ModelType != typeof(ComplexVMModel))
{
return false;
}
// Somehow get the depid from the request - this might not work.
int depId = HttpContext.Current.Request.Params["DepID"];
// Create and assign the model.
bindingContext.Model = new ComplexVMModel() { DepartmentName = CacheLookup(), DepId = depId };
return true;
}
}
Then at the beginning of your action method, you check the ModelState to see if it's valid or not. There are a few things which can make the model state non-valid (like not having a [Required] parameter.)
public ActionResult Browse(ComplexVM complexVM)
{
if (!ModelState.IsValid)
{
//If not valid - return some error view.
}
}
Now you just need to register this Model Binder.
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
ModelBinders.Binders.Add(typeof(ComplexVMModel), new ComplexVMModelBinder());
}
Your should be able to use the route config that you've provided.
I'm attempting to create a single Controller class to handle all foreseeable surveys that I'll end up creating in the future. Currently I have a 'Surveys' table with fields: Id, SurveyName, Active. On the 'master' Surveys' Index page I list out every SurveyName found in that table. Each SurveyName is clickable, and when clicked on, the page sends the SurveyName as a string to the receiving controller action. Said controller action looks like this:
//
//GET: /Surveys/TakeSurvey/
public ActionResult TakeSurvey(string surveyName)
{
Assembly thisAssembly = Assembly.GetExecutingAssembly();
Type typeToCreate = thisAssembly.GetTypes().Where(t => t.Name == surveyName).First();
object newSurvey = Activator.CreateInstance(typeToCreate);
ViewBag.surveyName = surveyName;
return View(surveyName, newSurvey);
}
Using reflection I am able to create a new instance of the type (Model) designated by the passed-in string 'surveyName' and am able to pass that Model off to a view with the same name.
EXAMPLE
Someone clicks on "SummerPicnic," the string "SummerPicnic" is passed to the controller. The controller, using reflection, creates a new instance of the SummerPicnic class and passes it to a view with the same name. A person is then able to fill out a form for their summer picnic plans.
This works all fine and dandy. The part that I'm stuck at is trying to save the form passed back by the POST method into the correct corresponding DB table. Since I don't know ahead of time what sort of Model the controller will be getting back, I not only don't know how to tell it what sort of Model to save, but where to save it to, either, since I can't do something ridiculous like:
//
//POST: Surveys/TakeSurvey
[HttpPost]
public ActionResult TakeSurvey(Model survey)
{
if (ModelState.IsValid)
{
_db. + typeof(survey) + .Add(survey);
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View();
}
Is there a way to do this, or should I go about this from a whole different angle? My ultimate goal is to have a single Controller orchestrating every simple-survey, so I don't have to create a separate controller for every single survey I end up making down the road.
An alternative solution I can think of is to have a separate method for every survey, and to have which method to call defined inside of every survey's view. For example, if I had a SummerPicnic survey, the submit button would call an ActionMethod called 'SummerPicnic':
#Ajax.ActionLink("Create", "SummerPicnic", "Surveys", new AjaxOptions { HttpMethod = "POST" })
A survey for PartyAttendance would call an ActionMethod 'PartyAttendance,' etc. I'd rather not have to do that, though...
UPDATE 1
When I call:
_db.Articles.Add(article);
_db.SaveChanges();
This is what _db is:
private IntranetDb _db = new IntranetDb();
Which is...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace Intranet.Models
{
public class IntranetDb : DbContext
{
public DbSet<Article> Articles { get; set; }
public DbSet<ScrollingNews> ScrollingNews { get; set; }
public DbSet<Survey> Surveys { get; set; }
public DbSet<Surveys.test> tests { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
}
}
You can try something like this,
UPDATE:
The built-in UpdateModel will work with generic model see this post, so we got little more work.
[HttpPost]
public ActionResult TakeSurvey(FormCollection form, surveyName)
{
var surveyType = Type.GetType(surveyName);
var surveyObj = Activator.CreateInstance(surveyType);
var binder = Binders.GetBinder(surveyType);
var bindingContext = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => surveyObj, surveyType),
ModelState = ModelState,
ValueProvider = form
};
binder.BindModel(ControllerContext, bindingContext);
if (ModelState.IsValid)
{
// if "db" derives from ObjectContext then..
db.AddObject(surveyType, surveyObj);
db.SaveChanges();
// if "db" derives from DbContext then..
var objCtx = ((IObjectContextAdapter)db).ObjectContext;
objCtx.AddObject(surveyType, surveyObj);
db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View();
}
Check this two know the diff between DbContext and ObjectContext
I ended up with a slightly modified version of Mark's code:
[HttpPost]
public ActionResult TakeSurvey(string surveyName, FormCollection form)
{
//var surveyType = Type.GetType(surveyName);
//var surveyObj = Activator.CreateInstance(surveyType);
// Get survey type and create new instance of it
var thisAssembly = Assembly.GetExecutingAssembly();
var surveyType = thisAssembly.GetTypes().Where(t => t.Name == surveyName).First();
var newSurvey = Activator.CreateInstance(surveyType);
var binder = Binders.GetBinder(surveyType);
var bindingContext = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => newSurvey, surveyType),
ModelState = ModelState,
ValueProvider = form
};
binder.BindModel(ControllerContext, bindingContext);
if (ModelState.IsValid)
{
var objCtx = ((IObjectContextAdapter)_db).ObjectContext;
objCtx.AddObject(surveyName, newSurvey);
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View();
}
I was running into surveyType being 'null' when it was set to Type.GetType(surveyName); so I went ahead and retrieved the Type via Reflection.
The only trouble I'm running into now is here:
if (ModelState.IsValid)
{
var objCtx = ((IObjectContextAdapter)_db).ObjectContext;
objCtx.AddObject(surveyName, newSurvey);
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
When it tries to AddObject I'm getting the exception "The EntitySet name 'IntranetDb.test' could not be found." I just need to figure out to strip off the prefix 'IntranetDb.' and hopefully I'll be in business.
UPDATE
One thing I completely overlooked was passing the Model to the controller from the View...oh bother. I currently have an ActionLink replacing the normal 'Submit' button, as I wasn't sure how else to pass to the controller the string it needs to create the correct instance of Survey model:
<p>
#Ajax.ActionLink("Create", "TakeSurvey", "Surveys", new { surveyName = ViewBag.surveyName }, new AjaxOptions { HttpMethod = "POST" })
#*<input type="submit" value="Create" />*#
</p>
So once I figure out how to turn 'IntranetDb.test' to just 'test' I'll tackle how to make the Survey fields not all 'null' on submission.
UPDATE 2
I changed my submission method from using an Ajax ActionLink to a normal submit button. This fixed null values being set for my Model values after I realized that Mark's bindingContext was doing the binding for me (injecting form values onto the Model values). So now my View submits with a simple:
<input type="submit" value="Submit" />
Back to figuring out how to truncate 'IntranetDb.test' to just 'test'...
Got It
The problem lies in my IntranetDb class:
public class IntranetDb : DbContext
{
public DbSet<Article> Articles { get; set; }
public DbSet<ScrollingNews> ScrollingNews { get; set; }
public DbSet<SurveyMaster> SurveyMaster { get; set; }
public DbSet<Surveys.test> tests { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
}
objCtx.AddObject(surveyName, newSurveyEntry); was looking for an entry (an "EntitySet") in the IntranetDb class called "test." The problem lies in the fact that I don't have an EntitySet by the name of "test" but rather by the name of "tests" with an 's' for pluralization. Turns out I don't need to truncate anything at all, I just need to point to the right object :P Once I get that straight I should be in business! Thank you Mark and Abhijit for your assistance! ^_^
FINISHED
//
//POST: Surveys/TakeSurvey
[HttpPost]
public ActionResult TakeSurvey(string surveyName, FormCollection form)
{
//var surveyType = Type.GetType(surveyName);
//var surveyObj = Activator.CreateInstance(surveyType);
// Create Survey Type using Reflection
var thisAssembly = Assembly.GetExecutingAssembly();
var surveyType = thisAssembly.GetTypes().Where(t => t.Name == surveyName).First();
var newSurveyEntry = Activator.CreateInstance(surveyType);
// Set up binder
var binder = Binders.GetBinder(surveyType);
var bindingContext = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => newSurveyEntry, surveyType),
ModelState = ModelState,
ValueProvider = form // Get values from form
};
var objCtx = ((IObjectContextAdapter)_db).ObjectContext;
// Retrieve EntitySet name for Survey type
var container = objCtx.MetadataWorkspace.GetEntityContainer(objCtx.DefaultContainerName, DataSpace.CSpace);
string setName = (from meta in container.BaseEntitySets
where meta.ElementType.Name == surveyName
select meta.Name).First();
binder.BindModel(ControllerContext, bindingContext); // bind form values to survey object
if (ModelState.IsValid)
{
objCtx.AddObject(setName, newSurveyEntry); // Add survey entry to appropriate EntitySet
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View();
}
It's kind of bloated but it works for now. This post helped me get the EntitySet from the Survey object itself so I didn't need to worry about establishing some sort of EntitySet naming convention.
The main problem I see is to bind to the model to the TakeSurvey POST method. If you want different types of survey models should be handled by this method and MVC should bind to this model before calling the action, I believe you can have a wrapper model class over all such generic model, say SurveyModel and use custom model binder to bind to these models.
public class SurveyModel
{
public string GetSurveyModelType();
public SummerPicnicSurvey SummerPicnicSurvey { get; set; }
public PartyAttendanceSurvey PartyAttendanceSurvey { get; set; }
}
Then write a custom mobel binder to bind this model. From the request form fields we can see what type of survey model is posted and then accordingly fetch all the fields and initialize the SurveyModel class. If SummerPicnicSurvey is posted then class SurveyModel will be set with this class and PartyAttendanceSurvey will be null. Example custom model binder.
From the controller action TakeSurvey POST method, You can update db like this:
[HttpPost]
public ActionResult TakeSurvey(SurveyModel survey)
{
if (ModelState.IsValid)
{
if(survey.GetSurveyModelType() == "SummerPicnicSurvey")
_db.UpdateSummerPicnicSurvey(survey.SummerPicnicSurvey);
else if (survey.GetSurveyModelType() == "PartyAttendanceSurvey")
_db.UpdateSummerPicnicSurvey(survey.PartyAttendanceSurvey);
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View();
}
Instead of SurveyModel encapsulating the other surveys you can have inheritance and use .net as to typecast with a check and use the Model.
Having said this, I think there is no harm in using different methods for each model. This will enable you to unit test the code well. Too many if else is not healthy to maintain. Or you can transfer the generic model SurveyModel to the repository or data access layer and let it handle that in a polymorphic way. I would prefer more small functions and keep the code clean.
Edit: The inheritance way:
public class SurveyModel
{
public virtual bool Save();
}
public partial class SummerPicnicSurvey : SurveyModel
{
public bool Save(SummerPicnicSurvey survey)
{
using(var _dbContext = new MyContext())
{
_dbContex.SummerPicnicSurveys.Add(survey);
_dbContex.SaveChanges();
}
}
}
[HttpPost]
public ActionResult TakeSurvey(SurveyModel survey)
{
if (ModelState.IsValid)
{
survey.Save();
return RedirectToAction("Index", "Home");
}
return View();
}
Any new Survey model type you add has to implement the SaveChanges or Save method, Which would call the proper dbcontext method. The controller action would just call Save on the generic `SurveyModel' reference passed to it. Thus the action will be closed for modification but open for modification. The open-close design principle.
I have a type in an assembly which isn't referenced by the core library but is referenced from the web application. e.g.
namespace MyApp.Models {
public class LatestPosts {
public int NumPosts { get; set; }
}
}
Now i have the following code in the core library:
[HttpPost, ValidateAntiForgeryToken]
public ActionResult NewWidget(FormCollection collection) {
var activator = Activator.CreateInstance("AssemblyName", "MyApp.Models.LatestPosts");
var latestPosts = activator.Unwrap();
// Try and update the model
TryUpdateModel(latestPosts);
}
The code is quite self explanatory but latestPosts.NumPosts property never updates even though the value exists in the form collection.
I'd appreciate it if someone could help explain why this does not work and whether there is an alternative method.
Thanks
Your problem has nothing to do with the fact that the type is in another assembly or that you are dynamically creating it with Activator.Create. The following code illustrates the issue in a much simplified way:
[HttpPost, ValidateAntiForgeryToken]
public ActionResult NewWidget(FormCollection collection)
{
// notice the type of the latestPosts variable -> object
object latestPosts = new MyApp.Models.LatestPosts();
TryUpdateModel(latestPosts);
// latestPosts.NumPosts = 0 at this stage no matter whether you had a parameter
// called NumPosts in your request with a different value or not
...
}
The problem stems from the fact that Controller.TryUpdateModel<TModel> uses typeof(TModel) instead of model.GetType() to determine the model type as explained in this connect issue (which is closed with the reason: by design).
The workaround is to roll your custom TryUpdateModel method which will behave as you would expect:
protected internal bool MyTryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties, IValueProvider valueProvider) where TModel : class
{
if (model == null)
{
throw new ArgumentNullException("model");
}
if (valueProvider == null)
{
throw new ArgumentNullException("valueProvider");
}
Predicate<string> propertyFilter = propertyName => new BindAttribute().IsPropertyAllowed(propertyName);
IModelBinder binder = Binders.GetBinder(typeof(TModel));
ModelBindingContext bindingContext = new ModelBindingContext()
{
// in the original method you have:
// ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(TModel)),
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
ModelName = prefix,
ModelState = ModelState,
PropertyFilter = propertyFilter,
ValueProvider = valueProvider
};
binder.BindModel(ControllerContext, bindingContext);
return ModelState.IsValid;
}
and then:
[HttpPost, ValidateAntiForgeryToken]
public ActionResult NewWidget(FormCollection collection)
{
object latestPosts = new MyApp.Models.LatestPosts();
MyTryUpdateModel(latestPosts, null, null, null, ValueProvider);
// latestPosts.NumPosts will be correctly bound now
...
}
I've never used any Mock frameworks and actually new to ASP.NET MVC, testing and all this related stuff.
I'm trying to figure out how to use Moq framework for testing, but can't make it work. that's what I have at the moment: My repository interface:
public interface IUserRepository {
string GetUserEmail();
bool UserIsLoggedIn();
ViewModels.User CurrentUser();
void SaveUserToDb(ViewModels.RegisterUser viewUser);
bool LogOff();
bool LogOn(LogOnModel model);
bool ChangePassword(ChangePasswordModel model);
}
My Controller constuctor, I'm using Ninject for injection, it works fine
private readonly IUserRepository _userRepository;
public HomeController(IUserRepository userRepository) {
_userRepository = userRepository;
}
Simplest method in controller:
public ActionResult Index() {
ViewBag.UserEmail = _userRepository.GetUserEmail();
return View();
}
And my test method:
[TestMethod]
public void Index_Action_Test() {
// Arrange
string email = "test#test.com";
var rep = new Mock<IUserRepository>();
rep.Setup(r => r.GetUserEmail()).Returns(email);
var controller = new HomeController(rep.Object);
// Act
string result = controller.ViewBag.UserEmail;
// Assert
Assert.AreEqual(email, result);
}
I assume that this test must pass, but it fails with message Assert.AreEqual failed. Expected:<test#test.com>. Actual:<(null)>.
What am I doing wrong?
Thanks
Simple - you do not do Act part correctly. Fisrt you should call Index() action of the controller, and then Assert ViewBag.UserEmail correctness
// Act
controller.Index();
string result = controller.ViewBag.UserEmail;
By the way, advice - Using ViewBag is not the good practice. Define ViewModels instead
I'm trying to use the DataAnnotationsModelBinder in order to use data annotations for server-side validation in ASP.NET MVC.
Everything works fine as long as my ViewModel is just a simple class with immediate properties such as
public class Foo
{
public int Bar {get;set;}
}
However, the DataAnnotationsModelBinder causes a NullReferenceException when trying to use a complex ViewModel, such as
public class Foo
{
public class Baz
{
public int Bar {get;set;}
}
public Baz MyBazProperty {get;set;}
}
This is a big problem for views that render more than one LINQ entity because I really prefer using custom ViewModels that include several LINQ entities instead of untyped ViewData arrays.
The DefaultModelBinder does not have this problem, so it seems like a bug in DataAnnotationsModelBinder. Is there any workaround to this?
Edit: A possible workaround is of course to expose the child object's properties in the ViewModel class like this:
public class Foo
{
private Baz myBazInstance;
[Required]
public string ExposedBar
{
get { return MyBaz.Bar; }
set { MyBaz.Bar = value; }
}
public Baz MyBaz
{
get { return myBazInstance ?? (myBazInstance = new Baz()); }
set { myBazInstance = value; }
}
#region Nested type: Baz
public class Baz
{
[Required]
public string Bar { get; set; }
}
#endregion
}
#endregion
But I'd prefer not to have to write all this extra code. The DefaultModelBinder works fine with such hiearchies, so I suppose the DataAnnotationsModelBinder should as well.
Second Edit: It looks like this is indeed a bug in DataAnnotationsModelBinder. However, there is hope this might be fixed before the next ASP.NET MVC framework version ships. See this forum thread for more details.
I faced the exact same issue today. Like yourself I don't tie my View directly to my Model but use an intermediate ViewDataModel class that holds an instance of the Model and any parameters / configurations I'd like to sent of to the view.
I ended up modifying BindProperty on the DataAnnotationsModelBinder to circumvent the NullReferenceException, and I personally didn't like properties only being bound if they were valid (see reasons below).
protected override void BindProperty(ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor) {
string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
// Only bind properties that are part of the request
if (bindingContext.ValueProvider.DoesAnyKeyHavePrefix(fullPropertyKey)) {
var innerContext = new ModelBindingContext() {
Model = propertyDescriptor.GetValue(bindingContext.Model),
ModelName = fullPropertyKey,
ModelState = bindingContext.ModelState,
ModelType = propertyDescriptor.PropertyType,
ValueProvider = bindingContext.ValueProvider
};
IModelBinder binder = Binders.GetBinder(propertyDescriptor.PropertyType);
object newPropertyValue = ConvertValue(propertyDescriptor, binder.BindModel(controllerContext, innerContext));
ModelState modelState = bindingContext.ModelState[fullPropertyKey];
if (modelState == null)
{
var keys = bindingContext.ValueProvider.FindKeysWithPrefix(fullPropertyKey);
if (keys != null && keys.Count() > 0)
modelState = bindingContext.ModelState[keys.First().Key];
}
// Only validate and bind if the property itself has no errors
//if (modelState.Errors.Count == 0) {
SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) {
OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
}
//}
// There was an error getting the value from the binder, which was probably a format
// exception (meaning, the data wasn't appropriate for the field)
if (modelState.Errors.Count != 0) {
foreach (var error in modelState.Errors.Where(err => err.ErrorMessage == "" && err.Exception != null).ToList()) {
for (var exception = error.Exception; exception != null; exception = exception.InnerException) {
if (exception is FormatException) {
string displayName = GetDisplayName(propertyDescriptor);
string errorMessage = InvalidValueFormatter(propertyDescriptor, modelState.Value.AttemptedValue, displayName);
modelState.Errors.Remove(error);
modelState.Errors.Add(errorMessage);
break;
}
}
}
}
}
}
I also modified it so that it always binds the data on the property no matter if it's valid or not. This way I can just pass the model back to the view withouth invalid properties being reset to null.
Controller Excerpt
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(ProfileViewDataModel model)
{
FormCollection form = new FormCollection(this.Request.Form);
wsPerson service = new wsPerson();
Person newPerson = service.Select(1, -1);
if (ModelState.IsValid && TryUpdateModel<IPersonBindable>(newPerson, "Person", form.ToValueProvider()))
{
//call wsPerson.save(newPerson);
}
return View(model); //model.Person is always bound no null properties (unless they were null to begin with)
}
My Model class (Person) comes from a webservice so I can't put attributes on them directly, the way I solved this is as follows:
Example with nested DataAnnotations
[Validation.MetadataType(typeof(PersonValidation))]
public partial class Person : IPersonBindable { } //force partial.
public class PersonValidation
{
[Validation.Immutable]
public int Id { get; set; }
[Validation.Required]
public string FirstName { get; set; }
[Validation.StringLength(35)]
[Validation.Required]
public string LastName { get; set; }
CategoryItemNullable NearestGeographicRegion { get; set; }
}
[Validation.MetadataType(typeof(CategoryItemNullableValidation))]
public partial class CategoryItemNullable { }
public class CategoryItemNullableValidation
{
[Validation.Required]
public string Text { get; set; }
[Validation.Range(1,10)]
public string Value { get; set; }
}
Now if I bind a form field to [ViewDataModel.]Person.NearestGeographicRegion.Text & [ViewDataModel.]Person.NearestGeographicRegion.Value the ModelState starts validating them correctly and DataAnnotationsModelBinder binds them correctly as well.
This answer is not definitive, it's the product of scratching my head this afternoon.
It's not been properly tested, eventhough it passed the unit tests in the project Brian Wilson started and most of my own limited testing. For true closure on this matter I would love to hear Brad Wilson thoughts on this solution.
The fix for this issue is simple, as Martijn has noted.
In the BindProperty method, you will find this line of code:
if (modelState.Errors.Count == 0) {
It should be changed to:
if (modelState == null || modelState.Errors.Count == 0) {
We are intending to include DataAnnotations support in MVC 2, which will include the DataAnnotationsModelBinder. This feature will be part of the first CTP.