Given the following model which has a name, url, and an arbitrary list of keywords (I want the user to add a series of keywords) ...
public class Picture
{
public Picture()
{
keywords = new List<string>();
}
public string name {get;set:}
public string url {get;set;}
public List<string> keywords{get;set;}
}
... and the following action in my controller ...
[HttpPost]
public ActionResult Edit(FormCollection fc)
{
if (ModelState.IsValid)
{
// do stuff
}
return View(ModelManager.Picture);
}
In the FormCollection I have the following field
fc["keywords"] = "keyword1,keyword2,keyword3"
And I then create a Picture object based on the form collection.
However, I would prefer to use a strongly-typed action such as
[HttpPost]
public ActionResult Edit(Picture p)
But in this approach, my p.keywords property is always empty. Is there some way to help the framework recreate my p.keywords property before it hits my controller's action method?
I thought an Editor Template might work here, but I don't think there is a way to model bind a nested IEnumerable view model member. Your fastest bet may be handling it directly with FormCollection and some string parsing magic. Otherwise, if you have to strongly-type this, maybe a custom model binder like this could help if you can control your keyword element id's:
public class PictureKeywordBinder : IModelBinder
{
public object GetValue(ControllerContext controllerContext,
string modelName, Type modelType,
ModelStateDictionary modelState)
{
Picture picture = new Picture();
//set name, url, other paramaters here
foreach(var item in Request.Form.Keys)
{
if (item.StartsWith("keyword"))
{
picture.keywords.Add(Request.Form[item]);
}
}
//add any errors to model here
return picture;
}
}
Maybe the keyword id's could be setup in a partial view passed the sub model from your parent view:
<% Html.RenderPartial("PictureKeywords", Model.keywords);
Are your keywords seperate text boxes? If so, create an inputs like this and they will be populated by the model binder.
<input name="keywords[0]" type="text">
<input name="keywords[1]" type="text">
<input name="keywords[2]" type="text">
The way I got around this, is to use a hidden input to store the csv string of items, in your case, keywords.
I then hooked into the form submit event (using jQuery) and appended the inputs to form the csv string, which is then stored in the hidden input. This hidden input was strongly typed to a property on my model.
It's a little clunky, but if you have a dynamic number of possible keywords then this works quite well (except if JS is disabled of course)
In what way you are expecting the user to add more keywords? In the form comma separated values(CSV) or by dynamically adding textboxes?
Based on your requirement, i have two solutions with me.
Related
My Post call does not return the correct Model type. It always use the baseObject instead of the correct derived object that I passed in from the Get
RestaurantViewModel.cs
public class RestaurantViewModel{
public Food BaseFoodObject{get;set;}
}
Food.cs
public class Food{
public string Price{get;set;)
}
Bread.cs -- Inherit from Food
public class Bread:Food{
public int Unit{get;set;}
}
Milk.cs -- Inherit from Food
public class Milk:Food{
public string Brand{get;set}
}
Editor For Template for Bread. Display the unit and allow user to edit
Index.html
#Model RestaurantViewModel
#using(Html.BeginForm("SaveFood", "Food"))
{
#Html.EditorFor(m=>m.BaseFoodObject)
<input type="submit" value="Process"/>
}
Bread.cshtml
#Model Bread
<div>
#Html.TextboxFor(bread=>bread.Unit)
</div>
FoodController.cs
public ActionResult Index(){
Bread bread = new Bread(){
Price = "$10",
Unit = 1
}
RestaurantViewModel viewModel = new RestaurantViewModel(){
BaseFoodObject = bread
}
return View(viewModel);
}
public ActionResult Post(RestaurantViewModel viewModelPost)
{
// When I inspect the viewModelPost, there is no attribute for unit
}
Final Result:
1. The display looks correct. EditorFor is smart enough to pick the correct editor template and display the value correctly
2. The Save does not work. The Unit attribute of Bread Object does not get passed in with the RestaurantViewModel. The reason for that is the RestaurantViewModel used the Food object instead of Bread
I hope there is away to modify the EditorFor and tell it to use the Model in the View or the Object Type that I passed in when I display it.
Thanks
Update 1: I solved this problem by using the custom binder and using a factory to decide which object I really want. This helps construct the correct Model which I want
MVC is stateless. A couple of references.
There's a couple of statements in your question that conflict with this, and how MVC binding works eg:
My Post call does not return the correct Model type.
Possibly just terminology, but your Post call does not 'return a model type' - it goes into the model that's defined in the post action, in this case RestaurantViewModel.
instead of the correct derived object that I passed in from the Get
because it is stateless, it knows nothing about the model you passed in from the get... absolutely nothing.
The final html rendered via the getaction+view.cshtml+model is not linked to the postaction. You could just as easily take the rendered html, save it, reboot your PC, reload the rendered html and it will work exactly the same way.
a way to modify the EditorFor and tell it to use the Model in the View or the Object Type that I passed in when I display it
When you use EditorFor it sets an ID and name attribute based on the model it was bound to, so it already does this, but perhaps you are not binding to the model you want to bind to to get the correct id.
So, to the question, if, in 'normal' C# code you were to instantiate a new instance of RestaurantViewModel, what would you expect the type of BaseFoodObject to be?
This is what the ModelBinder is doing - it's creating a new RestaurantViewModel.
As your post action method's signature does not include anything to do with Bread - all the bread properties are ignored.
Some options:
Check for the food properties after binding and read them manually (probably the quickest+easiest but not very "mvc-ish")
public ActionResult Post(RestaurantViewModel viewModelPost)
{
if (!string.IsNullOrEmpty(Request.Form["Unit"]))
// it's a bread form
to make this easier, you could provide a hidden field with the type
if (Request.Form["Type"] == typeof(Bread).Name)
{
var bread = new Bread { Unit = Request.Form["Unit"] }
Add bread to the action so it's bound
public ActionResult Post(RestaurantViewModel viewModelPost, Bread bread)
but then, obviously, it won't work for milk.
So could extend this using an ActionNameSelector to select the correct action
public ActionResult PostBread(RestaurantViewModel viewModelPost, Bread bread)
public ActionResult PostMilk(RestaurantViewModel viewModelPost, Milk milk)
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class FoodSelectorAttribute : ActionNameSelectorAttribute
{
public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
{
... check if provided parameters contains bread/milk
(related link but not a solution to this specific case)
Another option might be to change the Restaurant type to a generic, but would require a few more changes (and ideally use of interfaces), and more details (provided here as an idea, rather than a solution)
The basics would be:
public class RestaurantViewModel<T>
where T: Food
{
}
public ActionResult Post(RestaurantViewModel<Bread> viewModelPost)
public ActionResult Post(RestaurantViewModel<Milk> viewModelPost)
but I've not confirmed if the default ModelBinder would work in this case.
The problem comes with the post. Once you post, all you have is a set of posted data and a parameter of type, RestaurantViewModel. The modelbinder sets all the appropriate fields on Food because that's all it knows. Everything else is discarded. There's nothing that can be done about this. If you need to post fields related to Bread then the type of your property must be Bread. That's the only way it will work.
I've been trying to get my head around how to solve this problem in the cleanest way.
How to get the model binder to NOT create empty objects when submitting empty strings for complex types?
Say I've got this model:
public class CreateCaseModel
{
[Required]
public UserModel Contact { get; set; }
public UserModel Assigned {get; set; }
}
and the user-model:
public class UserModel {
[Required]
public string Id {get;set;}
public string Name {get;set;
}
And in the view i have these inputs:
<input type="hidden" name="Assigned.Id" value="" />
<input type="hidden" name="Contact.Id" value="userId1" />
I post this to a controller something like:
public ActionResult Create(CreateCaseModel model){
if (!ModelState.IsValid)
{
//handle error
}else {
//create
}
}
In the controller now my model.Contact and model.Assigned will never be null.
Only the model.Contact will however contain anything (the Idwill be set)
At this point the modelstate will also be invalid due to the fact that the UserModel.Id field is marked as required.
What I want to happen is for the binder to not create the UserModel object at all if the input is empty.
The entire reason for me needing this is that I'm using the UserModel.Name field elsewhere in the view, and I'd like to group the two fields together. And I want to use the ModelState.IsValidcheck for serverside validation.
I could of course go with extracting the Id and Name fields of each userobjects and put them directly in the CreateCaseModel, but I want to explore if what I describe is even possible.
Grateful for any tips or pointers!
How to get the model binder to NOT create empty objects when submitting empty strings for complex types?
Write your own custom ModelBinder. The default model binder will always attempt to create complex types for recursive modelbinding.
can anyone please show me the code for posting a list on button click from view to controller in mvc3-asp. i have a list in which one of the list element is check box.at first i have to select some of the values from list then submit it to save the selected values only.
Your question is two broad to answer. So i will give you a generic example which you need to customize according to your scenario.
Assuming your view is to assign some tasks to users. Each user can be assigned to one ore more tasks.
So i will create a viewmodel like this.
public class UserViewModel
{
public int UserId { set;get;}
public string Name { set;get;}
public List<Task> Tasks { set;get;}
public UserViewModel()
{
Tasks=new List<UserTask>();
}
}
public class UserTask
{
public int ID { set;get;}
public string Name { set;get;}
public bool IsSelected { set;get;}
}
Now in your GET action, you create an object of UserViewModel and set the Tasks property to a list of available taks (from your database may be)
public ActionResult Add()
{
var vm=new UserViewModel();
vm.Tasks=GetAvailableTasks();
return View(vm);
}
Assuming GetAvailableTasks is a method which returns a list of UserTask objects.
Now create an editor templates. Go to your ~/Views/YourControllerName folder and create a folder called EditorTemplates. Add a new view to the newly created folder and give name as Task.cshtml. Add the below code to that
#model YourNameSpace.UserTask
<p>
#Html.CheckBoxFor(x => x.IsSelected) Model.Name
#Html.HiddenFor(x => x.ID)
</p>
Now go back to your main view and use EditorFor helper method to bring the EditorTemplate.
#model YourNamespace.UserViewModel
<h2>Quiz 24</h2>
#using (Html.BeginForm())
{
#Html.EditorFor(x=>x.Tasks)
<input type="submit" />
}
Now when the form gets submitted, you can get the selected checkbox values like this
[HttpPost]
public ActionResult Save(UserViewModel model)
{
List<int> taskIds=new List<int>();
foreach (var task in model.Tasks)
{
if (task.IsSelected)
{
//you can get the selected task id's here.
taskIds.Add(task.ID);
}
}
//to do : save and redirect (PRG pattern)
}
Here is a blog post explaining how to use EditorTemplates to handle collections on form submit. the post is showing an example to handle radio buttons.
You can actually skip all this rebuilding in the controller. Razor can rebuild the list of any object using a single form. You just need to fallback to the old for and assign a unique name, as well as include the primary key for that collection.
On postback razor will rebuild your ViewModel with the modified list without any extra work.
In the markup it looks something like this
And in the controller after posting back the model is rebuilt with the new data the user has entered, and also lazyBound to ForeignKey models!
Using checkboxes or radio inputs it's slightly different. The problem is that when a checkbox is not ticked, browsers do NOT submit it back, and the model will NOT rebuild. This is not a shortfall of VS but an intended implementation of HTML1 that still lives in HTML5!
If this happens the Index Out of Bounds will occur. So you have include a hidden field for checkboxes with a value of false before the Input Box (if a user checks it it will overwrite the false to true)
But if you are using a StronglyTyped model, like in my case, any check boxes evaluated to null are automatically set to false, known as the default value for a boolean as defined in the C# specification.
In my views I'm using a generic type for the Model, ItemModel<T>.
This allows me to have a basetype on my model and it works fine. Within ItemModel<T> I attach the actual entity of T to a property called 'Item'.
Let's say I'm loading a User item: in my view I would like to do something like this:
<%: Html.TextBoxFor(Model => Model.Item.NickName,
new { id="NickName", name="NickName" })%>
Because the entity is added via the Item property, this generates the input tag's name attribute as 'Item.NickName'. When posting, MVC can no longer relate this to the User.NickName property of the user object in my controller
public ActionResult Login(User user, string redirectUrl)
and therefore nothing gets loaded into my User object. Is there a way to fix this? I've been looking into writing a custom bindingmodel or valueprovider, but that looks like alot of work for something this simple. Plus, I'm really not sure if that's that way to go.
All help is greatly appreciated!
change method of your actionresult from
public ActionResult Login(User user, string redirectUrl)
To
public ActionResult Login(User Item, string redirectUrl)
this way modelbiner will be able to locate properties of User object prefixed with Item
you can create your helper methods to create that textbox
public static MVCString MyTextBox<T>(this HtmlHelper html,T _item,String PropertyName)
{
TagBuilder myTag = new TagBuilder("input");
myTag.Attributes("type")="Text";
myTag.Attributes("id")=PropertyName;
myTag.Attributes("name")=PropertyName;
myTag.Attributes("value")=typeof(_item).GetProperty(PropertyName).GetValue(_item,null).toString();
Return MvcHtmlString.Create(selectTag.ToString())
}
My application writes custom attributes to input controls using Helper classes. And also we are loading the UserControl dynamically so we need to use FormCollection to get the posted values.
Is there a way that we can access attribute values from FormCollection object.
Example:
<input type="text" name="textBox1" value="harsha" customAttr1 = "MyValue" />
My question is how can i access the value of customAttr1 from the above eg from inside the controller.
Thanks for the help in advance..
How is your helper structured? If it's extending HtmlHelper, you can access ViewContext.HttpContext.Request.Form, which is a NameValueCollection; FormCollection is used by the model binder to post values back to an action method. Its not publicly exposed anywhere else.
HTH.
Simple answer is no am afraid, the formCollection only contains the basic Key and Value information.
It might be easier to rehydrate that information once you are in the controller? using some sort of mechanic to identify what you passed in.
An alternative is if you have a list of controls that map to a base type then you could loop through each control.
MVC is a bit magic and can map properties back to a model, even to a list.
If you have a model which has a list of Controls:
public class Control
{
String Value {get; set;}
String Attribute1 {get; set;}
}
public class ControlViewModel
{
IList<Control> Controls {get; set;}
}
then in your view:
for(var i = 0; i<controls.Count;i++)
{
// Obviously this isnt complete right i needs to increment from 0; would be build using your htmlhelpers.
<input id="Controls[i]_Value" name="Controls[i].Value" type="text" value="hello" />
<input id="Controls[i]_Attribute1" name="Controls[i].Attribute1" type="hidden" value="Attribute" />
}
in your httppost action you can then collect the ControlViewModel and the Controls list should populate.
I havent tested this, there are probably plenty of errors, but this should be enough to get started; posts out there that discuss this, if I find any after posting I'll add them.
As Luke already told.. Form Collection is dictionary object and only holds name,value pair.. in order to get that thing into controller you need to pass that custom attribute through ajax.
var form = $("#formid").serialize(),
custom = $("input:text").attr("customAttr1").val();
$.ajax({
type: "POST",
url: "/controller/ProcessData",
data:{collection :form,customAttr: custom },
dataType: "html",
traditional: true
});
in the controller you need to have following syntax:
public ActionResult ProcessData(FormCollection collection ,string customAttr)
{
in case you need to pass multiple custom values you need to post string array from ajax request and make controller signature like:
public ActionResult ProcessData(FormCollection collection ,string[] customArray)
{