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.
Related
I can't seem to get the edit function of my view to work..i have a page that lists, a page that shows specific detail and on that page, i should be able to edit the information of the form..PROBLEM: when i run the application it says:No parameterless constructor defined for this object. What am i doing wrong...?
In the Home Controller i have:
Edit Functions:
[HttpGet]
public ViewResult EditSchoolDetails(int id)
{
var institution = _educationRepository.GetInstititionById(id);
var model = (Mapper.Map<Institution, InstitutionModel>(institution));
return View(model);
}
post
[HttpPost]
public ActionResult EditSchoolDetails( InstitutionModel institutionModel, int id)
{
if (ModelState.IsValid) {
//_get from repository and add to instituion
var institution = _educationRepository.GetInstititionById(institutionModel.Id);
// Map from the view model back to the domain model
var model = Mapper.Map<Institution, InstitutionModel>(institution);
//UpdateModel(model);
SaveChanges();
return RedirectToAction("ViewSchoolDetails", new {institutionModel = institutionModel, id = id});
}
return View(institutionModel);
}
InstitutionModel
public class InstitutionModel {
public InstitutionModel() {
NAABAccreditations = new List<AccreditationModel>();
}
public int Id { get; set; }
public string Name { get; set; }
public bool IsNAAB { get { return NAABAccreditations.Any(); } }
public string Website { get; set; }
public AddressModel Address { get; set; }
public IEnumerable<AccreditationModel> NAABAccreditations { get; set; }
}
Does the Institution class have a parameterless constructor? If not, that will be the problem. You are passing an InstitutionModel to the the edit view, so the post action should probably take an InstitutionModel too, then you can map back to the original Institution model:
public ActionResult EditSchoolDetails(int id, InstitutionModel institutionModel)
{
if (ModelState.IsValid)
{
//add to database and save changes
Institution institutionEntity = _educationRepository.GetInstititionById(institution.Id);
// Map from the view model back to the domain model
Mapper.Map<InstitutionModel, Institution>(institutionModel, institutionEntity);
SaveChanges();
return RedirectToAction("ViewSchoolDetails",);
}
return View(institutionModel);
}
Notice also how it returns the view model back to the view if the model state isn't valid, otherwise you will lose all your form values!
Here's a similar question too which might help: ASP.NET MVC: No parameterless constructor defined for this object
Is it possible you need to pass a parameter to ViewSchoolDetails? I notice in the return statement you commented out that you were passing it an id, but in the return statement you're using, you're not passing in anything.
EDIT
This (from your comment below):
parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.Web.Mvc.ActionResult ViewSchoolDetails(Int32)
...tells me you need to pass a parameter to ViewSchoolDetails
EDIT 2
I saw your edit, and would say this: if the method you are calling is
public ActionResult ViewSchoolDetails(InstitutionModel institutionModel, int id)
Then you MUST pass it an object of type InstitutionModel and an int as parameters or you will get an exception. Meaning, you need
RedirectToAction("ViewSchoolDetails", new {institutionModel = institutionModel, id = id});
Whenever i get this, i have forgotten to create a parameter-less constructor on my view-model. I always add one now just in case it's needed and i forget.
Does InstitutionModel have one?
I've read many articles which they state that querying should not be placed in the Controller, but I can't seem to see where else I would place it.
My Current Code:
public class AddUserViewModel
{
public UserRoleType UserRoleType { get; set; }
public IEnumerable<SelectListItem> UserRoleTypes { get; set; }
}
public ActionResult AddUser()
{
AddUserViewModel model = new AddUserViewModel()
{
UserRoleTypes = db.UserRoleTypes.Select(userRoleType => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)userRoleType.UserRoleTypeID).Trim(),
Text = userRoleType.UserRoleTypeName
})
};
return View(model);
}
The View:
<li>#Html.Label("User Role")#Html.DropDownListFor(x => Model.UserRoleType.UserRoleTypeID, Model.UserRoleTypes)</li>
How do I retain the View Model and Query and exclude the User Type that should not show up?
I think that you are doing it just fine.
Any way... all you can do to remove the querying logic from controller is having a ServiceLayer where you do the query and return the result.
The MVC pattern here is used correctly... what your are lacking is the other 2 layers (BusinessLayer and DataAccessLayer)... since ASP.NET MVC is the UI Layer.
UPDATE, due to comment:
Using var userroletypes = db.UserRoleTypes.Where(u=> u.UserRoleType != 1);
is OK, it will return a list of UserRoleType that satisfy the query.
Then, just create a new SelectList object using the userroletypes collection... and asign it to the corresponding viewmodel property. Then pass that ViewModel to the View.
BTW, I never used the db.XXXX.Select() method before, not really sure what it does... I always use Where clause.
SECOND UPDATE:
A DropDownList is loaded from a SelectList that is a collection of SelectItems.
So you need to convert the collection resulting of your query to a SelectList object.
var userroletypes = new SelectList(db.UserRoleTypes.Where(u=> u.UserRoleType != 1), "idRoleType", "Name");
then you create your ViewModel
var addUserVM = new AddUserViewModel();
addUserVM.UserRoleTypes = userroletypes;
and pass addUserVM to your view:
return View(addUserVM );
Note: I'm assuming your ViewModel has a property of type SelectList... but yours is public IEnumerable<SelectListItem> UserRoleTypes { get; set; } so you could change it or adapt my answer.
I don't see anything wrong with your code other than this db instance that I suppose is some concrete EF context that you have hardcoded in the controller making it impossible to unit test in isolation. Your controller action does exactly what a common GET controller action does:
query the DAL to fetch a domain model
map the domain model to a view model
pass the view model to the view
A further improvement would be to get rid of the UserRoleType domain model type from your view model making it a real view model:
public class AddUserViewModel
{
[DisplayName("User Role")]
public string UserRoleTypeId { get; set; }
public IEnumerable<SelectListItem> UserRoleTypes { get; set; }
}
and then:
public ActionResult AddUser()
{
var model = new AddUserViewModel()
{
UserRoleTypes = db.UserRoleTypes.Select(userRoleType => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)userRoleType.UserRoleTypeID).Trim(),
Text = userRoleType.UserRoleTypeName
})
};
return View(model);
}
and in the view:
#model AddUserViewModel
<li>
#Html.LabelFor(x => x.UserRoleTypeId)
#Html.DropDownListFor(x => x.UserRoleTypeId, Model.UserRoleTypes)
</li>
I using MVC-Viewmodel in my project, my problem is that even if i CTRL-click two values in my listbox It only grabs one value. I want users to be able to select two values but I dont know why it doesnt happen any tips is appreciated!
Here is my GET n POST action inside my controller:
public ActionResult Create()
{
CreateViewModel model = new CreateViewModel();
List<CoreValue> corevalues = Arep.getallC();
model.CoreValuess = new MultiSelectList(corevalues, "CID", "Cname");
return View(model);
}
[HttpPost]
public ActionResult Create(CreateViewModel model)
{
if (ModelState.IsValid)
{
Question q = new Question();
var CoreValueID = int.Parse(model.Cname);
var getallC = Arep.getbycid(CoreValueID);
q.CoreValue.Add(getallC);
q.QuestionText = model.QuestionText;
Arep.addquestion(q);
Arep.save();
return RedirectToAction("Index");
}
return View(model);
This is inside my CreateViewModel:
public MultiSelectList CoreValues { get; set; }
And this is inside my View:
#Html.ListBoxFor(model => model.Cname,Model.Corevaluess)
What seem to be the problem?
Thanks in Advance!
Best Regards
Spelling errors aside, I believe the following is why this is failing:
In your ListBoxFor method, you are using model.Cname. By this, I believe you mean "choose the cName of selected CoreValues". However (and I'm guessing because I can't see your model), the Cname property on the CreateViewModel is of type string. Because of this, you are only ever going to have one value at a time. You need a property that is of type IEnumerable in order to hold multiple selections.
Update your model to the following:
public class CreateViewModel
{
public IEnumerable<string> SelectedValues { get; set; }
public IEnumerable<CoreValue> CoreValues { get; set; }
}
SelectedValues will be used to contain the selected values on the post. We can also add items to it to signify what should be automatically selected when the view is created.
In your controller do the following:
public ActionResult Create()
{
CreateViewModel model = new CreateViewModel();
model.CoreValues = Arep.getallC();
return View(model);
}
Lastly, update the view:
#Html.ListBoxFor(m => m.SelectedValues, new MultiSelectList(Model.CoreValues, "CID", "Cname"))
Now, whenever you post, you should be able to see the values that a user selected.
EDIT: I'm not completely sure what some of your methods do so I'm taking a guess.
The POST method for Create:
[HttpPost]
public ActionResult Create(CreateViewModel model)
{
if (ModelState.IsValid)
{
foreach(var CoreValueID in model.SelectedValues)
{
Question q = new Question();
var getallC = Arep.getbycid(CoreValueID);
q.CoreValue.Add(getallC);
q.QuestionText = model.QuestionText;
Arep.addquestion(q);
}
Arep.save();
return RedirectToAction("Index");
}
return View(model);
}
you would need to pass selectedvalues as below
List<CoreValue> selectedvalues = Null;
model.CoreValuess = new MultiSelectList(corevalues, "CID", "Cname",selectedvalues);
return View(model);
Refer MSDN link and this helpful Article
As stated above, the ASP.Net Mvc wants to have a list of string, but the same thing can be achieved by using the classic ASP style
Request.Form["CoreValues"]
this will provide as comma separated values.
This is what my data model classes look like:
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Position Position { get; set; }
}
public class Position
{
public string Title { get; set; }
}
I have a Create view where I want to have two text boxes for first name and last name, and then a dropdown box that has the position title. I tried doing it this way:
View (only the relevant part):
<p>
<label for="Position">Position:</label>
<%= Html.DropDownList("Positions") %>
</p>
Controller:
//
// GET: /Employees/Create
public ActionResult Create()
{
ViewData["Positions"] = new SelectList(from p in _positions.GetAllPositions() select p.Title);
return View();
}
//
// POST: /Employees/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Employee employeeToAdd)
{
try
{
employeeToAdd.Position = new Position {Title = (string)ViewData["Positions"]};
_employees.AddEmployee(employeeToAdd);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
However, when I click submit, I get this exception:
System.InvalidOperationException was unhandled by user code
Message="There is no ViewData item of type 'IEnumerable<SelectListItem>' that has the key 'Positions'."
I'm pretty sure I'm doing this wrong. What is the correct way of populating the dropdown box?
You can store:
(string)ViewData["Positions"]};
in a hiddn tag on the page then call it like this
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Employee employeeToAdd, string Positions)
{
In the Create() (WITH POST ATTRIBUTE) employee since the ViewData["Positions"] is not set you are getting this error. This value should form part of your post request and on rebinding after post should fetch it from store or get it from session/cache if you need to rebind this..
Remember ViewData is only available for the current request, so for post request ViewData["Positions"] is not yet created and hence this exception.
You can do one quick test... override the OnActionExecuting method of the controller and put the logic to fetch positions there so that its always availlable. This should be done for data that is required for each action... This is only for test purpose in this case...
// add the required namespace for the model...
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
// add your logic to populate positions here...
ViewData["Positions"] = new SelectList(from p in _positions.GetAllPositions() select p.Title);
}
There may be other clean solutions to this as well probably using a custom model binder...
I believe that ViewData is for passing information to your View, but it doesn't work in reverse. That is, ViewData won't be set from Request.Form. I think you might want to change your code as follows:
// change following
employeeToAdd.Position = new Position {Title = (string)ViewData["Positions"]};
// to this?
employeeToAdd.Position = new Position {Title = (string)Request.Form["Positions"]};
Good luck!
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
}