I would like to be able to post any serialized object to an action method and instantiate a new object of the posted type in order to use TryUpdateModel. They didn't teach me any of this stuff in the QBasic help file... How can I instantiate the unknown type based on the posted data?
If it would help, I could theoretically include the name of the type as a string in the posted data. I was hoping to avoid that because it seemed like I would need the full name of the type.
public void Save(object/dynamic whatever, string typename) {
//Instantiate posted type
//TryUpdateModel
context.Entry(Thing).State = EntityState.Modified;
context.SaveChanges();
}
Here is an example of a serialized object
Thing.Id=1&Thing.Name=blah&Thing.OptionID=1&Thing.ListItems.index=1&Thing.ListItems%5B1%5D.Id=1&Thing.ListItems%5B1%5D.Name=whatever&Thing.ListItems%5B1%5D.OptionID=2&Thing.ListItems%5B1%5D.ThingID=1&Thing.ListItems%5B1%5D.EntityState=16
From Fiddler
Thing.Id 1
Thing.Name blah
Thing.OptionID 1
Thing.ListItems.index 1
Thing.ListItems[1].Id 1
Thing.ListItems[1].Name whatever
Thing.ListItems[1].OptionID 2
Thing.ListItems[1].ThingID 1
Thing.ListItems[1].EntityState 16
You could write a custom model binder which uses reflection and the typeName parameter:
public class MyModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var typeValue = bindingContext.ValueProvider.GetValue("typename");
if (typeValue == null)
{
throw new Exception("Impossible to instantiate a model. The \"typeName\" query string parameter was not provided.");
}
var type = Type.GetType(
(string)typeValue.ConvertTo(typeof(string)),
true
);
var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;
}
}
and then simply:
[HttpPost]
public ActionResult Save([ModelBinder(typeof(MyModelBinder))] object model)
{
context.Entry(model).State = EntityState.Modified;
context.SaveChanges();
return View();
}
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 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 couple of action methods with parameters of IList type.
public ActionResult GetGridData(IList<string> coll)
{
}
The default behavior is when no data are passed to action method the parameter is null.
Is there any way to get an empty collection rather then null application wide ?
Well, you could either do this:
coll = coll ?? new List<string>();
Or you would need to implement a ModelBinder that will create an empty list instead of returning null. E.g.:
public EmptyListModelBinder<T> : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = base.BindModel(controllerContext, bindingContext) ?? new List<T>();
}
}
And wired up as:
ModelBinders.Binders.Add(typeof(IList<string>), new EmptyListModelBinder<string>());
I'd probably stick with the argument check though...
simply do it yourself
public ActionResult GetGridData(IList<string> coll)
{
if(coll == null)
coll = new List<String>();
//Do other stuff
}
I've found a lot of information on implementing a custom model binder for validation purposes but I haven't seen much about what I'm attempting to do.
I want to be able to manipulate the values that the model binder is going to set based on attributes on the property in the view model. For instance:
public class FooViewModel : ViewModel
{
[AddBar]
public string Name { get; set; }
}
AddBar is just
public class AddBarAttribute : System.Attribute
{
}
I've not been able to find a clean way to find the attributes on a view model property in the custom model binder's BindModel method. This works but it feels like there should be a simpler solution:
public class FooBarModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = base.BindModel(controllerContext, bindingContext);
var hasBarAttribute = false;
if(bindingContext.ModelMetadata.ContainerType != null)
{
var property = bindingContext.ModelMetadata.ContainerType.GetProperties()
.Where(x => x.Name == bindingContext.ModelMetadata.PropertyName).FirstOrDefault();
hasBarAttribute = property != null && property.GetCustomAttributes(true).Where(x => x.GetType() == typeof(AddBarAttribute)).Count() > 0;
}
if(value.GetType() == typeof(String) && hasBarAttribute)
value = ((string)value) + "Bar";
return value;
}
}
Is there a cleaner way to view the attributes on the view model property or a different kind of attribute I could be using? The DataAnnotation attributes really seem to be for a different problem.
UPDATE
Craig's answer got me to the right place but I thought I'd put some examples in here for others.
The metadata provider I ended up with looks like
public class FooBarModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var metaData = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if(attributes.OfType<AddBarAttribute>().Any())
metaData.AdditionalValues.Add("AddBarKey", true);
return metaData;
}
}
The model binder looks like:
public class FooBarModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = base.BindModel(controllerContext, bindingContext);
if(bindingContext.ModelMetadata.AdditionalValues.ContainsKey("AddBarKey"))
value = ((string)value) + "Bar";
return value;
}
}
The "correct" way (per the guy who wrote it) is to write a model metadata provider. There's an example at the link. Not precisely "simple," but it works, and you'll be doing what the rest of MVC does.
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.");
}