Pretty new to MVC so hopefully this is a simple question.
I have written a custom binding attribute that requires access to the httpContext. In order to inject a mock httpContext during unit tests, I have written an InjectingMetadataProvider that populates the Context property on any of my custom attributes.
I have managed to get this to work in the following test:
[TestMethod]
public void Marker_ShouldBind_Id()
{
// Arrange
var formCollection = new NameValueCollection
{
{ "id", "2" }
};
var context = new Mock<HttpContextBase>();
context.Setup(c => c.User).Returns((IPrincipal)null);
var metaProvider = new InjectingMetadataProvider(context.Object);
ModelMetadataProviders.Current = metaProvider; //why do I need this?
var bindingContext = new ModelBindingContext
{
ModelName = string.Empty,
ValueProvider = new NameValueCollectionValueProvider(formCollection, null),
ModelMetadata = metaProvider.GetMetadataForType(null, typeof(Marker)),
};
var binder = new DefaultModelBinder();
// Act
var marker = (Marker)binder.BindModel(new ControllerContext(), bindingContext);
// Assert
marker.Id.Should().Be(2);
}
However, if I comment out the line that sets my InjectingMetadataProvider to ModelMetadataProviders.Current, then my InjectingMetadataProvider.CreateMetadata() override gets handed a blank list of attributes, and so the test fails because my custom attributes don't get their context set.
Why do I need to set it to Current when I'm using it explicitly anyway? I don't want to be setting static stuff in my tests.
I may be doing something stupid because I'm feeling in the dark a bit at the moment due to my unfamiliarity with the framework.
Inside the DefaultModelBinder, a new binding context is created when calling BindComplexElementalModel. Notice that it gets the metadata from the ModelMetadataProviders.Current, and not your custom model metadata provider.
internal ModelBindingContext CreateComplexElementalModelBindingContext(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) {
BindAttribute bindAttr = (BindAttribute)GetTypeDescriptor(controllerContext, bindingContext).GetAttributes()[typeof(BindAttribute)];
Predicate<string> newPropertyFilter = (bindAttr != null)
? propertyName => bindAttr.IsPropertyAllowed(propertyName) && bindingContext.PropertyFilter(propertyName)
: bindingContext.PropertyFilter;
ModelBindingContext newBindingContext = new ModelBindingContext() {
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, bindingContext.ModelType),
ModelName = bindingContext.ModelName,
ModelState = bindingContext.ModelState,
PropertyFilter = newPropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
return newBindingContext;
}
Related
Note sure but i am looking stupid for this.I have created a simple model binder as shown below.
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
Customer obj = (Customer)base
.BindModel(controllerContext, bindingContext);
obj.CustomerName = request.Form["Text1"];
return obj;
}
I have a required field validator on the Customer model
public class Customer
{
private string _CustomerName;
[Required]
public string CustomerName
{
get { return _CustomerName; }
set { _CustomerName = value; }
}
}
in Global.asax i have tied up the model with the binder
ModelBinders.Binders.Add(typeof(Customer), new MyBinder());
But when i check the ModelState.IsValid its always false. What am i missing here ?
By directly accessing the property, you're bypassing the data annotations binding invoked by the default model binder (which happens as part of BindModel method).
You'll either need to let the base handle this behavior by having the request item have the same name as your CustomerName property, or invoke it yourself: http://odetocode.com/blogs/scott/archive/2011/06/29/manual-validation-with-data-annotations.aspx
Here is a snippet from the above linked site (adapted for your code):
var cust = new Customer();
var context = new ValidationContext(cust, serviceProvider: null, items: request);
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(cust, context, results);
if (!isValid)
{
foreach (var validationResult in results)
{
Console.WriteLine(validationResult.ErrorMessage);
}
}
I have problem with testing classes, which using UpdateModel() method.
I get System.NullReferenceException.
I use NUnit.
This is my method from HomeController:
public ActionResult ProjectsEdit(Projects model)
{
var projects = db.Projects.First();
projects.Content = model.Content;
UpdateModel(projects);
db.SaveChanges();
return RedirectToAction("Projects");
}
Here is test class:
[Test]
public void ProjectsEditPostTest()
{
var routeData = new RouteData();
var httpContext = MockRepository.GenerateStub<HttpContextBase>();
//var httpContext = new FakeHttpContext("Edit");
FormCollection formParameters = new FormCollection();
ControllerContext controllerContext =
MockRepository.GenerateStub<ControllerContext>(httpContext,
routeData,
controller);
controller.ControllerContext = controllerContext;
// Act
string newContent = "new content";
Projects projects = new Projects { ID = 1, Content = newContent };
controller.ProjectsEdit(projects);
// Assert
Assert.AreEqual(newContent, controller.db.Projects.First().Content);
}
What should I do to make it works?
Just add the following line in the Assert phase:
controller.ValueProvider = formParameters.ToValueProvider();
It assigns a value provider to the controller on which the UpdateModel method relies. This value provider is associated to the FormCollection variable you have defined and which allows you to pass some values.
You may also check a similar answer which uses MvcContrib.TestHelper to simplify the Arrange phase.
Is there any way to get access to the current running request's FormCollection, ViewData, ModelState, etc. when running in an ASP.NET MVC application other than if you are directly working in the View? I'd like to be able to call some custom handlers from within the view, but access these collections without having to pass them. I'm thinking something similar to HttpContext.Current in webforms?
Try,
var wrapper=new HttpContextWrapper(System.Web.HttpContext.Current);
var routeData = RouteTable.Routes.GetRouteData(wrapper);
Controller con = (Controller)ControllerBuilder.Current.GetControllerFactory().CreateController(new RequestContext(wrapper, routeData), routeData.Values["controller"].ToString());
var viewData = con.ViewData;
var modelState= con.ModelState;
var form=new FormCollection();
var controllerContext = new ControllerContext(wrapper, routeData, con);
Predicate<string> propertyFilter = propertyName => new BindAttribute().IsPropertyAllowed(propertyName);
IModelBinder binder = Binders.GetBinder(typeof(FormCollection));
ModelBindingContext bindingContext = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => form, typeof(FormCollection)),
ModelName = "form",
ModelState = modelState,
PropertyFilter = propertyFilter,
ValueProvider = ValueProviderFactories.Factories.GetValueProvider(controllerContext)
};
form = (FormCollection)binder.BindModel(controllerContext, bindingContext);
There is a ViewContext object that lets you link back to most of what you're asking for, but you really have to ask yourself why you're doing all of this in the view. (IMHO anyway)
Edit: I may have misread your question. There is a ControllerContext in the controller and a ViewContext in the view. Most of the extensibility points in MVC have some sort of Context object that lets you get at the Request and it's data.
I have a few questions regarding custom model binding, model state, and data annotations.
1) Is it redundant to do validation in the custom model binder if I have data annotations on my model, because that's what I thought the point of data annotations were.
2) Why is my controller treating the model state as valid even when it's not, mainly I make the Name property null or too short.
3) Is it ok to think of custom model binders as constructor methods, because that's what they remind me of.
First here is my model.
public class Projects
{
[Key]
[Required]
public Guid ProjectGuid { get; set; }
[Required]
public string AccountName { get; set; }
[Required(ErrorMessage = "Project name required")]
[StringLength(128, ErrorMessage = "Project name cannot exceed 128 characters")]
[MinLength(3, ErrorMessage = "Project name must be at least 3 characters")]
public string Name { get; set; }
[Required]
public long TotalTime { get; set; }
}
Then I'm using a custom model binder to bind some properties of the model. Please don't mind that it's quick and dirty just trying to get it functioning and then refactoring it.
public class ProjectModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
var p = new Project();
p.ProjectGuid = System.Guid.NewGuid();
p.AccountName = controllerContext.HttpContext.User.Identity.Name;
p.Name = controllerContext.HttpContext.Request.Form.Get("Name");
p.TotalTime = 0;
//
// Is this redundant because of the data annotations?!?!
//
if (p.AccountName == null)
bindingContext.ModelState.AddModelError("Name", "Name is required");
if (p.AccountName.Length < 3)
bindingContext.ModelState.AddModelError("Name", "Minimum length is 3 characters");
if (p.AccountName.Length > 128)
bindingContext.ModelState.AddModelError("Name", "Maximum length is 128 characters");
return p;
}
}
Now my controller action.
[HttpPost]
public ActionResult CreateProject([ModelBinder(typeof(ProjectModelBinder))]Project project)
{
//
// For some reason the model state comes back as valid even when I force an error
//
if (!ModelState.IsValid)
return Content(Boolean.FalseString);
//_projectRepository.CreateProject(project);
return Content(Boolean.TrueString);
}
EDIT
I Found some code on another stackoverflow question but I'm not sure at which point I would inject the following values into this possible solution.
What I want to inject when a new object is created:
var p = new Project();
p.ProjectGuid = System.Guid.NewGuid();
p.AccountName = controllerContext.HttpContext.User.Identity.Name;
p.Name = controllerContext.HttpContext.Request.Form.Get("Name");
p.TotalTime = 0;
How do I get the above code into what's below (Possible solution):
public class ProjectModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(Project))
{
ModelBindingContext newBindingContext = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
() => new Project(), // construct a Project object,
typeof(Project) // using the Project metadata
),
ModelState = bindingContext.ModelState,
ValueProvider = bindingContext.ValueProvider
};
// call the default model binder this new binding context
return base.BindModel(controllerContext, newBindingContext);
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
}
}
You will find things work much easier if you inherit from the DefaultModelBinder, override the BindModel method, call the base.BindModel method and then make the manual changes (setting the guid, account name and total time).
1) It is redundant to validate as you have done it. You could write code to reflect the validation metadata much like the default does, or just remove the data annotations validation since you are not using it in your model binder.
2) I don't know, it seems correct, you should step through the code and make sure your custom binder is populating all of the applicable rules.
3) It's a factory for sure, but not so much a constructor.
EDIT: you couldn't be any closer to the solution, just set the properties you need in the model factory function
public class ProjectModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(Project))
{
ModelBindingContext newBindingContext = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
() => new Project() // construct a Project object
{
ProjectGuid = System.Guid.NewGuid(),
AccountName = controllerContext.HttpContext.User.Identity.Name,
// don't set name, thats the default binder's job
TotalTime = 0,
},
typeof(Project) // using the Project metadata
),
ModelState = bindingContext.ModelState,
ValueProvider = bindingContext.ValueProvider
};
// call the default model binder this new binding context
return base.BindModel(controllerContext, newBindingContext);
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
}
Or you could alternately override the CreateModel method:
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
{
if (modelType == typeof(Project))
{
Project model = new Project()
{
ProjectGuid = System.Guid.NewGuid(),
AccountName = controllerContext.HttpContext.User.Identity.Name,
// don't set name, thats the default binder's job
TotalTime = 0,
};
return model;
}
throw new NotSupportedException("You can only use the ProjectModelBinder on parameters of type Project.");
}
I am trying to do the following.
Use the default model binder to bind an object from query string values.
If that fails, I then try and bind the object from cookie values.
However I am using dataannotations on this object and I am having the following problems.
If there are no querystring parameters the default model binder doesn't even register any validation errors on required fields. It apparently doesn't even fire these validators if the property itself is not in the query string collection. How can I change this behavior? I would like the required fields to be errors if they aren't in the query string.
If I do have model validation errors, I would like to then load the model from the cookie and then revalidate the object. I am not sure how to get the model binder to validate an object I have populated myself.
Here is what I have so far.
public class MyCarBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var myCar = base.BindModel(controllerContext, bindingContext);
if (!bindingContext.ModelState.IsValid)
{
myCar = MyCar.LoadFromCookie();
// Not sure what to do to revalidate
}
return myCar;
}
}
Any help on how to properly do this would be greatly appreciated.
Well, I solved it myself. Posting the solution here in case anyone has a comments or might like to use it.
public class MyCarBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var queryStringBindingContext = new ModelBindingContext()
{
FallbackToEmptyPrefix = bindingContext.FallbackToEmptyPrefix,
ModelMetadata = bindingContext.ModelMetadata,
ModelName = bindingContext.ModelName,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = new QueryStringValueProvider(controllerContext),
ModelState = new ModelStateDictionary()
};
var myCar = base.BindModel(controllerContext, queryStringBindingContext);
if (queryStringBindingContext.ModelState.IsValid)
return myCar;
// try to bind from cookie if query string is invalid
var cookieHelper = new Helpers.ControllerContextCookieHelper(controllerContext);
NameValueCollection nvc = cookieHelper.GetCookies(Helpers.CookieName.MyCar);
if (nvc == null)
{
bindingContext.ModelState.Merge(queryStringBindingContext.ModelState);
return myCar;
}
var cookieBindingContext = new ModelBindingContext()
{
FallbackToEmptyPrefix = bindingContext.FallbackToEmptyPrefix,
ModelMetadata = bindingContext.ModelMetadata,
ModelName = bindingContext.ModelName,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = new NameValueCollectionValueProvider(nvc, CultureInfo.InvariantCulture),
ModelState = new ModelStateDictionary()
};
var myCarFromCookie = base.BindModel(controllerContext, cookieBindingContext);
if (cookieBindingContext.ModelState.IsValid)
{
MyCar temp = myCarFromCookie as MyCar;
if (temp != null)
temp.FromCookie = true;
return myCarFromCookie;
}
else
{
bindingContext.ModelState.Merge(queryStringBindingContext.ModelState);
return myCar;
}
}
}