Postback on partially loaded view brings null model - asp.net-mvc

My application have page with few search boxes. When user clicks on respective search button a result grid is expected to load. That grid have few editable controls in which user can modifications and hit save button if needed to save the data.
We have implemented the functionality using AjaxForm and partial view.
Search area containing search boxes and search button is created using Ajax.BeginForm, on submit of which a postback Search method is called from controller. SearchModel is passed to this method.
Partial View is created to show results and Ajax form in step 1 loads it successfully from controller postback method. SearchModel.Results property is passed to view as its model.
This partial view showing results have a Save button (again a Ajax form) which invokes another method in controller but gets model null in controller.
have tried lot of tricks but was unsuccessful. Any working example demonstrated anywhere or suggestion to make this working ? There are lot of examples on web where usage of AjaxForm to load data is explained but didn’t found any for multiple (or nested?)
Thanks in advance.
Edit - Feb 24
Here is the sample I created using Visual Studio default MVC template, which is similar to the actual criteria explained above and have the same problem on Submit of Partial page ..
Views:
Index.cshtml
#using MvcApplication1.Models
#model SearchModel
#{
ViewBag.Title = "Home Page";
}
<script src="#Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" ></script>
<h2>#ViewBag.Message</h2>
<p>
To learn more about ASP.NET MVC visit http://asp.net/mvc.
</p>
#Ajax.BeginForm("Search", "Home", new AjaxOptions { HttpMethod = "Post",
InsertionMode = InsertionMode.Replace,UpdateTargetId = "SearchResults"})
{
<table>
<tr>
<td>
First Name:
</td>
<td>
#Html.TextBoxFor(m => m.SearchString)
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Submit" />
</td>
</tr>
</table>
}
<div id="SearchResults" style="color: Green;"></div>
Partial View - _SearchResult.cshtml
#using MvcApplication1.Models
#model MvcApplication1.Models.SearchModel
#{
ViewBag.Title = "Result Partial";
}
<h2>
testPartial</h2>
#Ajax.BeginForm("SearchResult", "Home", new AjaxOptions
{
HttpMethod = "Post"
})
{
<table>
#foreach (ResultModel item in Model.Result)
{
<tr>
<td>
Name:
</td>
<td>
#Html.DisplayFor(m => item.Name)
</td>
</tr>
<tr>
<td>
Address:
</td>
<td>
#Html.TextAreaFor(m => item.Address)
</td>
</tr>
}
<tr>
<td colspan="2">
<input type="submit" value="Submit" />
</td>
</tr>
</table>
}
Models:
SearchModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace MvcApplication1.Models
{
public class SearchModel
{
public string SearchString { get; set; }
public List<ResultModel> Result { get; set; }
}
}
ResultModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace MvcApplication1.Models
{
public class ResultModel
{
public string Name { get; set; }
public string Address { get; set; }
}
}
Controller:
HomeController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcApplication1.Models;
namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "Welcome to ASP.NET MVC!";
return View();
}
public ActionResult About()
{
return View();
}
[HttpPost]
public ActionResult Search(SearchModel model)
{
//dummy search for result
List<ResultModel> result = new List<ResultModel>();
ResultModel res1 = new ResultModel();
res1.Name = model.SearchString + " 1";
res1.Address = "Dummy address";
result.Add(res1);
ResultModel res2 = new ResultModel();
res2.Name = model.SearchString + " 2";
res2.Address = "Rummy address";
result.Add(res2);
//assign seach results to model
model.Result = result;
return PartialView("_SearchResult", model);
}
[HttpPost]
public ActionResult SearchResult(SearchModel model)
{
//do something with results
List<ResultModel> res = model.Result; // null here !!
return RedirectToAction("Index");
}
}
}
SO, above sample will
Show you a Search box on home page - Index.cshtml
When you click on submit it will load a partial view displaying results below search
When you edit search results and hit submit on result form, please have a breakpoint set up there, you will see model being returned is null.
Hope, this explain my problem.

Now that you have shown your actual code with a complete example your question can be answered.
The reason you are getting null is because you are not respecting the standard naming convention for your input fields that the default model binder expects. Please read the following article from Phil Haack to familiarize yourself with those conventions.
The problem with your code is the fact that you used a foreach loop inside your partial to render the results instead of using an editor template. So replace your code inside _SearchResult.cshtml with the following:
#using MvcApplication1.Models
#model SearchModel
#{
ViewBag.Title = "Result Partial";
}
<h2>testPartial</h2>
#using(Ajax.BeginForm("SearchResult", "Home", new AjaxOptions()))
{
<table>
#Html.EditorFor(x => x.Result)
<tr>
<td colspan="2">
<input type="submit" value="Submit" />
</td>
</tr>
</table>
}
and then define a custom editor template for the ResultModel type (~/Views/Shared/EditorTemplates/ResultModel.cshtml - warning, the name and location of your editor template is important because it works by convention):
#using MvcApplication1.Models
#model ResultModel
<tr>
<td>
Name:
</td>
<td>
#Html.DisplayFor(m => m.Name)
#Html.HiddenFor(m => m.Name)
</td>
</tr>
<tr>
<td>
Address:
</td>
<td>
#Html.TextAreaFor(m => m.Address)
</td>
</tr>
Things to notice:
I have wrapped the Ajax.BeginForm helper in a using statement. You should do the same inside your Index.cshtml view
I have added a hidden field for the Name property inside the custom editor template (#Html.HiddenFor(m => m.Name)) in order to send this value to the server when the form is submitted because you only had a TextArea for the Address field meaning that the Name would never have been sent to your server.

Related

Validating MVC Form with Multiple Tab

I have this form with couple tabs that I needed to validate. Here is the issue I am encountered. I wanted to direct the user back to the "Tab Page" where required fields were not entered. For an example, if the user didn't select any of the Financial Section of selection then the Financial Tab page will shows the error message.
Here is the View page
#using CalFresh.Models
#model CalFresh.Models.calfreshByWorkUnitID
#using (Html.BeginForm())
{
<form id="msform" class="form-inline" method="post">
<div id="TabsSetMain">
<table>
<tr>
<td>Some reviewer info</td>
</tr>
</table>
</div>
#*--------------------- Sub Tabs (Household/Financial/Medical)-----
<div id="TabsSet1">
<ul id="progressbar">
<li>Household</li>
<li>Financial</li>
<li>Medical</li>
</ul>
</div>
#*-------------------- Household Tab -----------------
<div id="tabs-household">
<table>
<tr>
<td>Was the SSN Verified?</td>
#{
List<SelectListItem> listItems = new List<SelectListItem>();
listItems.Add(new SelectListItem
{
Text = " -- Select One -- ",
Value="",
Selected = true
});
listItems.Add(new SelectListItem
{
Text = "Yes",
Value="Yes"
});
listItems.Add(new SelectListItem
{
Text = "No",
Value="No"
});
}
<td>
#Html.DropDownListFor(model=>model.tempHouseHoldSSNVerification, listItems, new {#class="form-control", value=Model.tempHouseHoldSSNVerification})
#Html.ValidationMessageFor(model=>model.tempHouseHoldSSNVerification, null, new {style="color:red"})
</td>
</tr>
</table>
</div>
#*-------------------- Financial Tab -----------------
<div id=tabs-financial">
<table>
<tr>
<td>
Was an error found for earned income calculation?
</td>
<td>
#Html.DropDownListFor(model=>model.tempFinancialEarnIncome, listItems, new {#class="form-control", value=Model.tempHouseHoldSSNVerification})
#Html.ValidationMessageFor(model=>model.tempFinancialEarnIncome, null, new {style="color:red"})
</td>
</tr>
</table>
</div>
</form>
}
<script>
$(function () {
$("#TabsSetMain").tabs();
});
</script>
<script>
$(function () {
$("#TabsSet1").tabs();
});
</script>
Here is the class page
using System;
using System.ComponentModel.DataAnnotations;
namespace CalFresh.Models
{
public class calfreshByWorkUnitID
{
[Required(ErrorMessage = "Please select Household SSN verification.")]
public string tempHouseHoldSSNVerification { get; set; }
[Required(ErrorMessage = "Please select Financial Income Calculations.")]
public string tempFinancialEarnIncome { get; set; }
}
}
Here is the Controller code
using System;
using System.Configuration;
using System.Linq;
using System.Web.Mvc;
using System.Net;
using System.Data;
using CalFresh.Models;
namespace CalFresh.Controllers
{
public string pubTempHouseHoldSSNVerification;
public string pubtempFinancialEarnIncome;
[HttpPost]
public ActionResult Add(calfreshByWorkUnitID customerinfo)
pubTempHouseHoldSSNVerification = customerinfo.tempHouseHoldSSNVerification;
pubtempFinancialEarnIncome = customerinfo.tempFinancialEarnIncome;
if (ModelState.IsValid)
{
#* Process my insert script here.
}
}
Model state is available in the view via ViewData.ModelState. I don't know what tab library you are using, but somewhere you will need to check which fields are invalid in the model state, figure out which tab they are on, and set focus on that tab.
You should be able to get the fields with errors using something like
ViewData.ModelState.Where(fld => fld.Value.Errors.Count > 0)
or to check individual fields,
ViewData.ModelState.IsValidField("FIELD_NAME")
If you can't set the tab that has focus using css or some sort of tag server side, you'll need to dump the fields with errors into a javascript variable and go from there.
<script>
var errors = '#string.Join(",", ViewData.ModelState.Where(x => x.Value.Errors.Count > 0).Select(fld => fld.Key))';
....
</script>

Binding input model in partial view help needed

I want to pass an input model from a partial view to a controller. I'm rather new to MVC so still trying to understand how the default model binder works.
Via AJAX (listBox) a controller passes back a partial view and inserts into table id=searchResults.
#model ViewModels.LocationViewModel
#using (Ajax.BeginForm("ProcessSearch", "SearchR", new AjaxOptions
{
HttpMethod = "GET",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "searchResults",
}))
{
<div>#Html.ListBoxFor(xxx)</div>
<input id="Search" type="submit" value="Search" />
}
Here is the controller and ViewModel that populates the partial view
public class OrderViewModel
{
public string Description { get; set; }
public string Status { get; set; }
}
public ActionResult ProcessSearch(SearchViewModel search)
{
select new OrderViewModel{
Status=f.STATUS,
Description=f.DESCRIPTION}).ToList();
return PartialView(model);
}
In the same main view I have this form that I want I want to bind to yet another view model. I simply don't understand how to implement the default binder from the model of the partial view. I apologize if I didn't explain this correctly. I hope it makes sense.
#using (Html.BeginForm("Testing", "SearchR", FormMethod.Post))
{
<div>#Html.DropDownListFor(yyy)</div>
<input id="Reshop" type="submit" value="Reshop" />
}
<table id="searchResults"></table>
public ActionResult Testing(RSOrderViewModel rOrder)
{
return Content("hey");
}
public class RSOrderViewModel
{
public string yyy { get; set; }
public IEnumerable<OrderViewModel> sovm { get; set; }
}
#model List<ViewModels.OrderViewModel>
#{ViewData.TemplateInfo.HtmlFieldPrefix = "sovm";
}
<table id="searchResults">
<tr>
<th>Order Id</th>
<th>Order Detail</tr>
#for (int i = 0; i < Model.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(x => x[i].OrderId)
</td>
<td>
#Html.DisplayFor(x => x[i].OrderDetail)
</td>
</tr>
}
</table>
The table is outside the second form. So when you POST to the Testing action all that is sent to the controller is the value of the dropdown list. If you want to send the collection that's stored in this table you will have to either use AJAX or put the table inside the form:
#using (Html.BeginForm("Testing", "SearchR", FormMethod.Post))
{
<div>#Html.DropDownListFor(yyy)</div>
<table id="searchResults"></table>
<input id="Reshop" type="submit" value="Reshop" />
}
Now of course putting the table inside the form doesn't mean that it will send anything to the server when you submit the form. You need to put input fields (hidden if you don't want them to be visible to the user) that will contain the values that will be POSTed back. Also those input field names must follow the standard convention for binding to a list.
You haven't actually shown how does the partial view look like but here's an example of how it might look so that the convention is respected. For example let's suppose that you have 2 properties in your OrderViewModel that you want to be bound: OrderId and OrderDetail:
#model IEnumerable<OrderViewModel>
#{
// We set the name prefix for input fields to "sovm"
// because inside your RSOrderViewModel the collection
// property you want to bind to this table is called sovm
// and in order to respect the convention the input names
// must be in the following form "sovm[0].OrderId", "sovm[1].OrderId", ...
ViewData.TemplateInfo.HtmlFieldPrefix = "sovm";
}
<thead>
<tr>
<th>Order Id</th>
<th>Order detail</th>
</tr>
</thead>
<tbody>
#Html.EditorForModel()
</tbody>
and then you could have an editor template which will be rendered for each element of the model (~/Views/Shared/EditorTemplates/OrderViewModel.cshtml):
#model OrderViewModel
<tr>
<td>#Html.EditorFor(x => x.OrderId)</td>
<td>#Html.EditorFor(x => x.OrderDetail)</td>
</tr>
The name of the template is the name of the type used in the collection you want to bind to (IEnumerable<OrderViewModel> sovm { get; set; } => OrderViewModel.cshtml). It must also be placed inside the ~/Views/Shared/EditorTemplates folder if it can be reused between multiple controllers or if it is specific to the current controller inside the ~/Views/XXX/EditorTemplates folder where XXX is the current controller.

How do I return List or Collection to Controller from View in MVC 3?

Someone please help me return this list properly from my view. I don't see why I'm returning null for my fieldModelList I try to pass to the controller...
Here is my view:
#model List<Regions.SOA.UI.CopyBookSchemaCreator.Models.FieldModel>
<script type="text/javascript" src="~/Scripts/jquery-ui-1.8.11.min.js"></script>
#using (Html.BeginForm("GetResponse", "TestMethods", FormMethod.Post))
{
<table id="tblMethods">
<tr>
<th>
Property Name
</th>
<th>
Request
</th>
</tr>
#foreach (FieldModel fieldModel in Model)
{
<tr>
<td>
#Html.DisplayFor(m => fieldModel.PropertyName)
</td>
<td>
#Html.TextBoxFor(m => fieldModel.PropertyValue)
</td>
</tr>
}
</table>
<div>
<input type="submit"/>
</div>
and here is my controller:
[HttpPost]
public ActionResult GetResponse(List<FieldModel> fieldModelList)
{
return GetResponse(fieldModelList);
}
I am hitting the HttpPost method but if I place a breakpoint just inside it, I am returning null for the fieldModelList right off the bat, which I was hoping would be a list of the values I entered into the texboxes on the view that is of model FieldModel...
I think something is wrong with my logic versus my syntax, or as maybe as well as my syntax, but basically what I want to do is return back a list of type FieldModel with each corresponding PropertyName and PropertyValue to the controller. I noticed I am not passing any kind of id parameter in my BeginForm statement in the view. Do I need one here?
Just in case, here is my model class for FieldModel:
namespace Regions.SOA.UI.CopyBookSchemaCreator.Models
{
public class FieldModel
{
[Display(Name = "Property")]
public string PropertyName { get; set; }
[Display(Name = "Value")]
public string PropertyValue { get; set; }
}
}
Phil Haack wrote an article some time ago explaining how to bind collections (ICollection) to view models. It goes into additional detail about creating an editor template, which you could certainly do as well.
Basically, you need to prefix the HTML elements' name attributes with an index.
<input type="text" name="[0].PropertyName" value="Curious George" />
<input type="text" name="[0].PropertyValue" value="H.A. Rey" />
<input type="text" name="[1].PropertyName" value="Ender's Game" />
<input type="text" name="[1].PropertyValue" value="Orson Scott Card" />
Then, your controller could bind the collection of FieldModel
[HttpPost]
public ActionResult GetResponse(List<FieldModel> fieldModelList)
{
return GetResponse(fieldModelList);
}
I'm not 100% sure the following would name the attributes correctly (I'd recommend using the editor template) but you could easily use the htmlAttributes argument and give it a name using the index.
#for(int i = 0;i < Model.Count;i++)
{
<tr>
<td>
#Html.DisplayFor(m => m[i].PropertyName)
</td>
<td>
#Html.TextBoxFor(m => m[i].PropertyValue)
</td>
</tr>
}
Editor Template
If you wanted to go as far as adding an editor template, add a partial view named FieldModel.ascx to /Views/Shared that is strongly typed to a FieldModel
#model Regions.SOA.UI.CopyBookSchemaCreator.Models.FieldModel
#Html.TextBoxFor(m => m.PropertyName) #* This might be a label? *#
#Html.TextBoxFor(m => m.PropertyValue)
And, then the part of your view responsible for rendering the collection would look like:
#for (int i = 0; i < Model.Count; i++) {
#Html.EditorFor(m => m[i]);
}

Model passed to a partial view is null upon form submit

I have the following model:
#model SmartSEOModel
public class SmartSEOModel
{
public SmartSEOSettingsModel SmartSEOSettingsModel { get; set; }
public SEOTemplateModel SEOTemplateModel { get; set; }
}
In my view I have a partial view which I call like this:
#using (Html.BeginForm())
{
some razor code here
<div id="pnlSmartSEO">
#Html.Partial(ViewNames.SmartSEOController_SEOTemplate, Model.SEOTemplateModel)
</div>
}
In the partial view there are some form fields bound to the SEOTemplateModel.
The problem is that when I receive the SmartSEOModel in my HttpPost action, the SEOTemplateModel is null. As if the SEOTemplateModel has been passed by copying it to the partial view.
Please advise why this is and how to workaround it.
Many thanks
My partial view looks like this:
#Html.Telerik().TabStrip().Name("SmartSEO").Items(x =>
{
x.Add().Text(T("Admin.SmartSEO").Text).Content(GetSmartSEOUI().ToHtmlString()).Selected(true);
})
#helper GetSmartSEOUI()
{
#(Html.LocalizedEditor<SEOTemplateModel, SEOTemplateLocalizedModel>("SmartSEO-Localized",
#<table class="adminContent">
<tr>
<td class="adminTitle">
#Html.NopLabelFor(model => model.Locales[item].CategoryTitleSEOTemplate):
</td>
<td class="adminData">
#Html.EditorFor(model => model.Locales[item].CategoryTitleSEOTemplate)
</td>
</tr>
</table>,
#<table class="adminContent">
<tr>
<td class="adminTitle">
#Html.NopLabelFor(model => model.CategoryTitleSEOTemplate):
</td>
<td class="adminData">
#Html.EditorFor(model => model.CategoryTitleSEOTemplate)
</td>
</tr>
</table>
))
}
My HttpPost action looks like this:
[HttpPost]
public ActionResult Configure(SmartSEOModel smartSEOModel)
{
var seoTemplate = SEOTemplateService.GetSEOTemplateById(smartSEOModel.SEOTemplateModel.Id);
if(seoTemplate == null)
{
throw new ArgumentException(String.Format("No SEOTemplate found with Id {0}", smartSEOModel.SEOTemplateModel.Id));
}
if (!ModelState.IsValid)
{
RedirectToAction("Configure");
}
SettingService.SaveSetting(smartSEOModel.SmartSEOSettingsModel.ToEntity());
seoTemplate = smartSEOModel.SEOTemplateModel.ToEntity(seoTemplate);
SEOTemplateService.UpdateSEOTemplate(seoTemplate);
UpdateLocales(seoTemplate, smartSEOModel.SEOTemplateModel);
//activity log
CustomerActivityService.InsertActivity("EditSEOTemplate", LocalizationService.GetResource("ActivityLog.EditSEOTemplate"));
SuccessNotification(LocalizationService.GetResource("SevenSpikes.NopSmartSEO.Admin.SEOTemplate.Notifications.SEOTemplateEdited"));
return View("SevenSpikes.Nop.Plugins.SmartSEO.Views.Configure", smartSEOModel);
}
Becuase you don't have a form within your partial view, it will not persist the data. Try using #Html.EditorFor instead of #Html.Partial.
So your main view would look like
#using (Html.BeginForm())
{
some razor code here
<div id="pnlSmartSEO">
#Html.EditorFor(model => model.SEOTemplateModel)
</div>
}
You would then need to move your partial view into a template. Rename your partial view to EditorTemplates\SEOTemplateModel.cshtml and place it in the same location where your main view is.
You will also need to make your template strongly typed: #model [namespace].SEOTemplateModel

Asp.net MVC Razor more than one form on a page

Yo
I have a registration page on my site - at the top of the page is a login form for existing users. In the main area there is the registration form.
The login are is a partial view with #model ViewModels.LoginViewModel
The registration are is also a partial with #model ViewModels.RegViewModel
The main page which houses these partials is a view with #model ViewModels.RegPageViewModel
This viewmodel looks like:
public class RegViewModel
{
public RegisterVm RegisterVm { get; set; }
public LoginVm LoginVm { get; set; }
}
When I submit the registration part of the page (it's action is register/capture - the receiving action expects a RegisterVm) to it's controller it complains about being passed the wrong viewmodel
What's the deal with subviews and their viewmodel? Is there a standard approach to dealing with this?
Should I have one submit URL for this page which figures out if it's a login request or a register request and then handles the post accordingly? That seems messy to me though...
http://monobin.com/__d33cf45a4 - RegisterVm.cs (LoginVm.cs is pretty much the same as this)
http://monobin.com/__m69132f76 - RegPageVm.cs
Register.cshtml:
#model xxxx.ViewModels.RegPageVm
#{
View.Title = "Register";
Layout = "~/Views/Shared/_BareBones.cshtml";
}
<link rel="stylesheet" href="#Url.Content("~/Public/Css/signup.css")" type="text/css" />
<div id="sign-up-container">
<div id="sign-up-box">
<div id="sign-up-box-left">
<img src="#Url.Content("~/Public/Images/Signup_176x81.png")" />
</div>
<div id="sign-up-box-right">
#Html.Partial("_Register")
</div>
</div>
</div>
<div class="clear">
</div>
_Register.cshtml:
#model xxxx.ViewModels.RegisterVm
#using (Html.BeginForm("Capture", "Register", FormMethod.Post))
{
<table class="sign-up-box-inner">
<tr>
<td class="label-area">
#Html.LabelFor(x => x.Email)
</td>
<td class="field-area">
#Html.TextBoxFor(x => x.Email, new { #class = "login-input", title = "Enter Name" })
</td>
</tr>
<tr>
<td class="label-area">
#Html.LabelFor(x => x.Password)
</td>
<td class="field-area">
#Html.PasswordFor(x => x.Password, new { #class = "login-input", title = "Enter Name" })
</td>
</tr>
<tr>
<td class="label-area">
#Html.LabelFor(x => x.UserName)
</td>
<td class="field-area">
#Html.TextBoxFor(x => x.UserName, new { #class = "login-input", title = "Enter Name" })
</td>
</tr>
<tr>
<td colspan="2">
<input type="image" src="../../Public/Images/Submit_150x47.png" class="submit-button" />
</td>
</tr>
</table>
#Html.AntiForgeryToken()
}
And finally RegisterController.cs:
public class RegisterController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost, ValidateAntiForgeryToken]
public ActionResult Capture(RegisterVm registerVm)
{
if (!ModelState.IsValid)
{
return View("index", new RegPageVm()
{
LoginVm = new LoginVm(),
RegisterVm = registerVm
});
}
return RedirectToAction("index", "Event");
}
}
w://
You need to ensure that the form elements (like the textbox etc) should have the same id as the RegisterVM and LoginVM properties. Your theory is right but I think you might be making a mistake in the naming convention of MVC.
If you can share your view code + the VM classes, then we'll be able to help better.
EDIT:
Looking at your code I think you should be passing the view model to your partial view. Like for example the following line believe should be like this >
#Html.Partial("_Register", Model.RegisterVm)
According to your answer to nEEbz:
You are using:
Html.TextBoxFor(x=>x.LoginVM.Email) // i guess
this would turn into <input name="LoginVM.Email" ...>
Notice the LoginVM. part
Your login action probably looks like:
public ActionResult Login(LoginVM model) { }
so it expect field names like Email and Password, not LoginVM.Email and LoginVM.Password.
So you could could use Html.Textbox instead (so that the field name doesn't get autocreated).

Resources