I have a generic "Index" page which lists all the entries for a given table and there is a side-bar which allows filtering the data in the grid. My model is as follows:
public class GenericFormIndexModel
{
public IEnumerable<IGenericForm> Entries { get; set; }
public FormSearchQueryModel Query { get; set; }
}
In the razor file I have an html like this:
#using (Html.BeginForm("Search", controllerName, FormMethod.Post, new { id = "fSearch" }))
{
#Html.HiddenFor(m => m.Query.PageIndex)
#Html.HiddenFor(m => m.Query.PageSize)
#Html.HiddenFor(m => m.Query.SortBy)
...etc
#Html.TextBoxFor(m => m.Query.SerialNumber, null, new { #class = "inputbox right-search-field" })
...etc
and I have defined an action as follows:
[HttpPost]
public virtual ActionResult Search(FormSearchQueryModel queryModel)
{
//Implementation ommited
}
Now, the problem is that the values from the form are indeed submitted, but do not bind to my "queryModel" argument in the action. I can see them in Request.Form["Query.Something"].
I do not wish to submit the entire Model, as it is not necessary to post all the entries and whatever else back. Is it possible to get MVC to bind to a nested property or am I stuck with using Reqest.Form[""] ?
Did you try setting the Prefix property as below,
[HttpPost]
public virtual ActionResult Search([Bind(Prefix="Query")]FormSearchQueryModel queryModel)
{
//Implementation ommited
}
The Bind attribute has other properties like Include, Exclude through which you can control what are the posted values need to be binded.
Related
I'm creating a form for register new users, each user is allowed to have many address. (many address to one user). I found this article but doesn't look right for me, because I have a main property which is user and the address has to be related with it. I would like to create something similar for add many address even in the create screen for new user, which doesn't exist the user primary key for these address be related.
http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/
Could someone send an example how is the better way to do it?
this is something very close to my code:
public class Person{
public int PersonId { get; set; }
public string Name { get; set; }
public virtual ICollection<Address> Addresses { get; set; }
}
public class Address{
public int AddressId { get; set; }
public string Street { get; set; }
[ScriptIgnore]
public virtual Person Person { get; set; }
}
As Klors indicated, create a view models for Address and Person (suggest List<AddressVM> is initialized in PersonVM constructor). In your PersonController
[HttpGet]
public ActionResult Create()
{
PersonVM model = new PersonVM();
model.Addresses.Add(new AddressVM()); // add an empty address assuming at least one required
return View(model, "Edit")
}
[HttpGet]
public ActionResult Edit(int ID)
{
PersonVM model = // get from database/repository
// If Address.Count == 0, add new Address
return View(model);
}
[HttpPost]
public ActionResult Edit(PersonVM model)
{
...
In your view
#model PersonVM
#using (Html.BeginForm("Edit", "Person") {
....
#Html.HiddenFor(m => m.ID)
#Html.TextBoxFor(m => m.Name)
...
#Html.EditorFor(m => m.Addresses) // you may want to create an editor template for AddressVM
<button type="button" id="AddAddress">Add address</button>
The EditorFor() will generate HTML similar to this (note the indexer)
<input type="text" name="Address[0].Street" id="Address_0__Street" ..../>
<input type="text" name="Address[0].Suburb" id="Address_0__Suburb" ..../>
To dynamically add new addresses, you need to use JavaScript to generate similar HTML, but increment the indexer. If you use an editor template, you can wrap the Address controls in a container (say <div class="address">) which make them easy to select as per the script below
Script to dynamically add a new address
$('#AddAddress').click(function() {
var addresses = $('.address');
// Get the number of existing address
var count = addresses.length;
// Get the first address and clone it
var clone = addresses.first().clone();
// Update the index of the clone
clone.html($(clone).html().replace(/\[0\]/g, '[' + count + ']'));
clone.html($(clone).html().replace(/"_0__"/g, '_' + count + '__'));
// Add to the DOM
addresses.last().after(clone);
}
Note this will also clone the values from the first address, so you may want to reset some or all of them, for example (after its been added)
clone.find('input').val('');
If you're using #Html.ValidationMessageFor() methods, note that dynamically added elements will not be validated in the browser unless you parse the form. An example of how to do that can be found here: jquery.validate.unobtrusive not working with dynamic injected elements
Something like this might be closer to what you need. You'll need to imagine the checkboxes are collections of fields for entering your address.
However, if you create a ViewModel for Person that contains a list of ViewModels for Address, you can create strongly typed editor templates and display templates named the same as the ViewModels and they'll automatically be picked up if you use #Html.EditorFor and #Html.DisplayFor which makes working with one to many's easier.
If you had files like so -
~/Models/PersonViewModel.cs
~/Models/AddressViewModel.cs
~/Views/Person/Edit.cshtml
~/Views/Shared/DisplayTemplates/AddressViewModel.cshtml
~/Views/Shared/EditorTemplates/AddressViewModel.cshtml
and a person ViewModel a bit like
public class PersonViewModel
{
public int Id { get; set; }
public string Name { get; set; }
...
public List<AddressViewModel> Addresses { get; set; }
}
If you then have an edit view for person like
#model PersonViewModel
<div>
#using (Html.BeginForm("Edit", "Person", new {id = Model.Id}, FormMethod.Post))
{
<div>
#Html.EditorForModel()
</div>
<div>
<p>#Html.DisplayNameFor(p => p.Addresses)</p>
#Html.EditorFor(p => p.Addresses)
</div>
<p>
<input type="submit" value="Save"/>
</p>
}
</div>
then the editor template should get picked up for the AddressViewModel once for each entry in the list. You'll have to add in some Ajax to allow new addresses to be created like in your example link. As long as the template contains all the fields for the AddressViewModel to work, then your Edit POST controller should just receive a PersonViewModel back as it's parameter.
There are some parts missing from my example, but you should be able to fill in the blanks from tutorials.
In ASP.Net MVC I am opening one view from another view. The first view sends two values to the second view. In the second view the user can send an email.
I am having two problems.
The first problem is that the two values that I send from the first view aren't being shown in my second view.
The second problem is that I can't get the email form to trigger my email function in the controller.
Here's a more detailed explanation.
My first view named ViewOne is using the controller ControllerOne. In ViewOne I have the following code to call the second view, ViewTwo:
#Html.ActionLink("Go to second view", "ViewTwo", "Home", new { firstval = firstval, secondval = secondval }, null)
When the ActionLink is clicked, the following function in the controller HomeController is called:
public ActionResult ViewTwo(string firstval, string secondval)
{
MyModel model = new MyModel();
model.firstval = firstval;
model.secondval = secondval;
var list = new SelectList(new[]
{
new {ID="1",Name="One"},
new{ID="2",Name="Two"},
new{ID="3",Name="Three"},
},
"ID", "Name", 1);
model.myList = list;
return View(model);
}
So in the controller HomeController I attempt to populate the model myModel with the values I get from the first view, ViewOne.
The model MyModel looks like this:
public class MyModel
{
public string firstval { get; set; }
public string secondval { get; set; }
public IEnumerable<SelectListItem> myList { get; set; }
[Required]
[DisplayName("My name")]
public string reporter { get; set; }
[Required]
[DisplayName("Description")]
public string description { get; set; }
[DisplayName("Dropdown")]
public string myDropDownListValue { get; set; }
}
The view ViewTwo looks like this:
#model myapp.Models.MyModel
#{ ViewBag.Title = "Send e-mail"; }
<hgroup class="title">
<h1>#ViewBag.Title</h1>
<h2>#ViewBag.Message</h2>
</hgroup>
#using (Html.BeginForm("sendEmail"))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary()
<fieldset>
<legend>Send e-mail</legend>
<p>First value:</p>
<p>#Html.LabelFor(m => m.firstval)</p>
<p>Second value:</p>
<p>#Html.LabelFor(m => m.secondval)</p>
<p>Reporter</p>
<p>#Html.TextBoxFor(m => m.reporter)</p>
<p>Dropdownlist</p>
<p>#Html.DropDownListFor(m => m.myDropDownListValue, Model.myList as SelectList)</p>
<p>Description:</p>
<p>#Html.TextAreaFor(m => m.description, new { #cols = 150, #rows = 5})</p>
<input type="submit" value="Send e-mail"/>
</fieldset>
}
In the controller HomeController, which is the same controller that has the ViewTwo() function that gets triggered right before the above form gets drawn, I have the following function:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult sendEmail(ContactModel model) // (string keyword, string partofspeech, string reporter, string category, string description, string acceptance)
{
// code to send email
}
So I want this function, sendEmail, to get triggered whenever I submit the form. But that doesn't happen. What happens when I click the submit button (labeled "Send e-mail") is that the view ViewTwo gets reloaded and the ActionResult ViewTwo() in the controller HomeController gets triggered. This is my second (and biggest) problem.
Also, my first problem is that
<p>#Html.LabelFor(m => m.firstval)</p>
Doesn't show the value that gets sent from the first view. It shows the string "firstval". Before the form is drawn I can see in the function ViewTwo() that the value gets correctly sent from the first view.
Any ideas?
EDIT:
Second problem solved. See my reply below.
You have a few options, normally with a postback you would submit the form with an <input type="submit" value="sendEmail" />, the values in the form would be represented in a ViewModel like:
public class EmailFormViewModel()
{
public string value1 {get; set;}
public string reporter {get; set;}
//More properties on the form
}
Your endpoint would look like this:
[HttpPost]
public ActionResult SendEmail(EmailFormViewModel model)
{
//Send the email
}
If you still want to use a hyperlink to submit the form, which natively performs a GET request, you will can catch the click with javascript, and manually send the form via Ajax.
Something like:
$('#sendEmail').click(function(e) {
e.preventDefault();
$.ajax({
type: 'POST',
data: $('#formId').serialize(),
url: '/controllerName/sendemail'
}).done(function(response) {
//Do something on success response
});
});
Update:
You should also decorate your post action sendEmail with [ValidateAntiForgeryToken] and add a #Html.AntiForgeryToken() within the form. This will help protect against cross site forgery requests.
You can build your form, endpoint and model like this:
#using (Html.BeginForm("sendEmail"))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary()
<p>#Html.LabelFor(m => m.value1)</p>
<p>#Html.EditorFor(m => m.value1)</p>
<p>#Html.LabelFor(m => m.reporter)</p>
<p>#Html.EditorFor(m => m.reporter)</p>
<p>#Html.LabelFor(m => m.myDropDownListValue)</p>
<p>#Html.DropDownListFor(m => m.myDropDownListValue, Model.myList as SelectList)</p>
<p>#Html.LabelFor(m => m.myTextAreaValue)</p>
<p>#Html.TextAreaFor(m => m.myTextAreaValue, new { #cols = 150, #rows = 5})</p>
<input type="submit" value="Send Email"/>
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult SendEmail(myModel model)
{
//Send the email
}
public class myModel
{
public IEnumerable<SelectListItem> myList { get; set; }
[DisplayName('Value 1')]
public string value1 { get; set; }
[DisplayName('Reporter')]
public string reporter { get; set; }
[DisplayName('Text Area')]
public string myTextAreaValue { get; set; }
[DisplayName('Dropdown')]
public string myDropDownListValue { get; set; }
}
As long as you are already on the same controller, it will postback to /controllername/sendemail with the form data inside the post. You should also look up attributes on your models, you can enforce descriptions and validations for example. Check here for more details, its MVC 2 but still relevant.
If you really want to be able to GET the values instead of POSTing them, change the form's action to GET and change the target to be sendEmail
Remove the ActionLink and replace it with a simple submit button
I know you said you wanted to keep the ActionLink, but this will achieve the same thing
I managed to solve my first problem. Once I specified which controller the function sendEmail is in, that code finally got triggered. Like so:
#using (Html.BeginForm("sendEmail", "Home"))
Now if I can only figure out why
<p>#Html.LabelFor(m => m.firstval)</p>
isn't working then I'm home safe.
It actually prints out the string "firstval" instead of taking the value of the string variable firstval that I set in the model. (See my first post for more detailed explanation).
EDIT:
I fixed that last problem. The very werid thing is that the above code with LabelFor doesn't work. But if I do this instead:
<p>#Model.firstval</p>
then I get the value. But it doesn't get sent back to the controller when the form is submitted. But that I solved with:
#Html.HiddenFor(m => m.firstval)
So HiddenFor works for me, LabelFor doesn't.
Case closed. I'm throwing the "solved checkmark" to the guy who gave me all that help here above. He did awesome. But the real solution is in this post. :)
I have a Register Primary View which shows two different types of Addresses 1. Home Address 2. Mailing Address
public class RegisterModel
{
public AddressModel HomeAddress { get; set; }
public AddressModel MailAddress { get; set; }
}
public class AddressModel
{
public string Street1 { get; set; }
public string Street2 { get; set; }
public string State { get; set; }
public string City { get; set; }
}
My main Register View is Strongly Typed to RegisterModel as follows
#model MyNamespace.Models.RegisterModel
#{
Layout = "~/Views/_Layout.cshtml";
}
#using (Html.BeginForm(null, null, FormMethod.Post, new { id = "myForm" }))
{
<div id="form">
#Html.Action("MyAddressPartial")
#Html.Action("MyAddressPartial")
</div>
}
MyAddressPartialView as follows : -
#model MyNamespace.Models.AddressModel
#{
Layout = "~/Views/_Layout.cshtml";
}
<div id="Address">
#Html.TextBoxFor(m=>m.Street1 ,new { #id="Street1 "})
#Html.TextBoxFor(m=>m.Street2,new { #id="Street2"})
#Html.TextBoxFor(m=>m.State ,new { #id="State "})
#Html.TextBoxFor(m=>m.City,new { #id="City"})
</div>
My RegisterController:-
// Have to instantiate the strongly Typed partial view when my form first loads
// and then pass it as parameter to "Register" post action method.
// As you can see the #Html.Action("MyAddressPartial") above in main
// Register View calls this.
public ActionResult MyAddressPartial()
{
return PartialView("MyAddressPartialView", new AddressModel());
}
I submit my Main Form to below mentioned action method in same Register Controller.
[HttpPost]
public ActionResult Register(RegisterModel model,
AddressModel homeAddress,
AddressModel mailingAddress)
{
//I want to access homeAddress and mailingAddress contents which should
//be different, but as if now it comes same.
}
I don't want to create a separate class one for MailingAddress and one for HomeAddress. if I do that then I will have to create two separate strongly typed partial views one for each address.
Any ideas on how to reuse the classes and partial views and make them dynamic and read their separate values in Action Method Post.
Edit 1 Reply to scott-pascoe:-
In DisplayTemplates Folder, I added following AddressModel.cshtml
<div>
#Html.DisplayFor(m => m.Street1);
#Html.DisplayFor(m => m.Street2);
#Html.DisplayFor(m => m.State);
#Html.DisplayFor(m => m.City);
</div>
Also In EditorTemplate Folder, I added following AddressModel.cshtml but with EditorFor
<div>
#Html.EditorFor(m => m.Street1);
#Html.EditorFor(m => m.Street2);
#Html.EditorFor(m => m.State);
#Html.EditorFor(m => m.City);
</div>
Now how do i use them in RegisterView and also how i read values in Controller's post Action Method ? What else would have to be modified ? I have added almost entire code above. I am pretty beginner to MVC.
The typical ASP.NET MVC method for doing this is to use EditorTemplates and DisplayTemplates for your custom types.
In ~/Views/Shared, Create two folders, DisplayTemplates, and EditorTemplates.
In the DisplayTemplates folder create a partial view with the name of your Model, ie (AddressModel), and create a DisplayFor Template.
In the EditorTemplates folder create another partial view named AddressModel.cshtml and create an EditorFor Template.
MVC will then automatically use your templates and give you the data that you are asking for.
Use #Html.EditorFor (or #Html.DisplayFor, for display) in your view:
#model MyNamespace.Models.RegisterModel
#{
Layout = "~/Views/_Layout.cshtml";
}
#using (Html.BeginForm(null, null, FormMethod.Post, new { id = "myForm" }))
{
<div id="form">
#Html.EditorFor(m => m.HomeAddress)
#Html.EditorFor(m => MailAddress)
</div>
}
You will not need to have a separate controller action for the parts, just populate the addresses in the RegisterModel before in your controller. Like this:
[HttpGet]
public ActionResult Register() // this will be the page people see first
{
var model = new RegisterModel();
return View(model); // assuming your view is called Register.cshtml
}
[HttpPost]
public ActionResult Register(RegisterModel model){
DosomethingWithHomeAddress(model.HomeAddress);
DosomethingWithMailAddress(model.MailAddress);
model.IsSaved = true; // some way to let the user knwo that save was successful;
// if this is true, display a paragraph on the view
return View(model);
}
Having this structure:
Index.cshtml:
#foreach (var category in Model) {
#Html.DisplayFor(m => category.Products)
}
Products.cshtml:
...
#Html.Partial("_AddToCartProductViewModel", new CheckoutVC.Models.ModelView.AddToCartProductViewModel(Model))
...
_AddToCartProductViewModel.cshtml :
#model CheckoutVC.Models.ModelView.AddToCartProductViewModel
#using (Ajax.BeginForm("AddToCart", "Cart", new AjaxOptions { LoadingElementId = "loading" + Model.IdProduct, OnSuccess = "showMessage", UpdateTargetId = "cart_widget" })) {
#Html.HiddenFor(m => m.IdProduct)
#Html.HiddenFor(m => m.IdPrescriptionType)
#Html.HiddenFor(m => m.PrescriptionRequired)
#Html.HiddenFor(m => m.Quantity)
<span class="p-r-s">#Html.LabelFor(m=>m.IdPrescriptionType)</span>
#Html.DropDownListFor(m=>m.IdPrescriptionType, new SelectList(Model.PrescriptionOptions, "Item1", "Item2"), String.Empty)
<button class="action add #Html.PrescriptionRequiredCss(Model.PrescriptionRequired)" type="submit">agregar al carrito<img class="loading" id="loading#(Model.IdProduct)" alt="" src="#Url.Content("~/Content/images/shared/loading_16x16.gif")" width="16" height="16" /></button>
}
With this AddToCartProductViewModel.cs constructor :
public AddToCartProductViewModel(ProductCheckoutMinVO product, int quantity = 1) {
IdProduct = product.Id;
PrescriptionRequired = product.PrescriptionRequired;
Quantity = 1;
Promotions = product.Promotions;
}
[Required]
[Min(1)]
public int IdProduct { get; set; }
public int Quantity { get; set; }
public bool? PrescriptionRequired { get; set; }
[Min(0)]
public int IdPrescriptionType { get; set; }
MVC generate this Request on submit:
category.Products[0].IdProduct:826
category.Products[0].IdPrescriptionType:0
category.Products[0].PrescriptionRequired:False
category.Products[0].Quantity:1
category.Products[0].IdPrescriptionType:1
X-Requested-With:XMLHttpRequest
Problem is, my controller CartController.cs :
public RedirectToRouteResult AddToCart(AddToCartProductViewModel product, FormCollection form, string returnUrl = null) {
...
}
the FormCollection (form) does receive the parameters while the AddToCartProductViewModel (product) does NOT bind.
I have some ideas why properties are not binding to product and how im doing some magic here and there to get a single-object form populated from some nested loops (in which one would expect a collection object in request [one being the framework]), yet cannot find a elegant solution to have this kind of form-scenario bind to AddToCartProductViewModel.
I can make it work 'somehow' using the properties directly into the AddToCart method but then i lose validation (dataannotations) on the modelview.
How can I make MVC bind these properties to the AddToCartProductViewModel view model
I think you could try:
public RedirectToRouteResult AddToCart([Bind(Prefix="category.Products[0]")]AddToCartProductViewModel product, FormCollectionform, string returnUrl = null) {
...
}
Solving some problem brings up another.
Ended up doing it manually with html tags and leave the data annotations validation out.
may be a messed up viewmodel problem i just don't know and don't have the time or resources to make it work as i would like right now. Maybe I'll try to address in the future.
Don't know if there's a way to close the question or what to do? editor feel free to update this as you pleases. :D
When passing a view model (as below) to a view, how can I ensure that the checkboxes I'm creating (mapped to item "Product" in here) get passed back to the controller?
I've included my view model and "post" product controller below.
Unfortunately, when posted back to the controller, "Products" is null.
namespace MyProject.Models
{
public class ChartViewModel
{
public Chart ChartItem { get; set; }
public IEnumerable<Product> Products { get; set; }
}
}
Controller:
[Authorize]
[ValidateInput(false)]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(ChartViewModel objChartViewModel)
{
if (!TryUpdateModel(objChartViewModel))
{
return View(objChartViewModel);
}
else
{
} return View("Details", objChartViewModel);
}
How the checkboxes are added to my view, mapped to the "Product" object within my view model:
#{
foreach (MyProject.Models.Product objProduct in Model.Products)
{
#Html.CheckBox("product" + objProduct.Id, Model.ChartItem.ChartProducts.Select(t => t.ProductId).Contains(objProduct.Id));
#String.Format("{0} {1}", objProduct.Manufacturer.Name, objProduct.Name);<br />
}
}
You can send your lists (IEnumerable<T>) down to the view but when they don't come back up to the controller. The only properties of your ViewModel that have values are those with exactly matching items in the forms collection. So add a properties to your ViewModel like SelectedProductID.
This sets up a drop down list that sends its selected value back to the controller:
<div class="editor-field">
#Html.DropDownListFor(m => m.SelectedEmployeeID,
new SelectList(Model.EmployeeList, "EmployeeID", "EmployeeName"), "--Please select an Employee--")
#Html.ValidationMessageFor(model => model.SelectedEmployeeID)
</div>
Notice: the property being set is SelectedEmployeeID but this comes from the EmployeeList.
In your case the values many be in the collection (only "checked" checkboxes get sent in a post) so you could do:
string value = collection["myProductID"];
if its there its checked.
sorry for the messiness, this was in a bit of a rush. See this for more info:
MVC 3 form post and persisting model data