I have a form that posts a single value, but I cant seem to get it to the controller. I have verified the value exists in the form, but it arrives at the controller as null. Here is the form post:
<%Html.BeginForm("SaveRecord", "NewApplicant", FormMethod.Post, new { id = Model.PersonModel.ApplicantID } ); %>
<%: Html.Hidden("NewId", Model.PersonModel.ApplicantID) %>
<input type="submit" class="SKButton" value="Save" title="Save this new application as a unique record." />
<% Html.EndForm(); %>
and here is the contoller action:
public ActionResult SaveRecord(NewApplicantViewModel model)
{
int NewAppId = model.PersonModel.ApplicantID;
I have also tried:
public ActionResult SaveRecord(int NewId)
{
model.PersonModel.ApplicantID = NewId;
These must be a simple fix, and I want to pass the id in the model, dont want to use ajax. Thoughts?
Try using:
<%: Html.HiddenFor(model => model.PersonModel.ApplicantID) %>
Related
I have an MVC3 page with an object (Header) that has data and a list of objects (Details) that I want to update on a single page. On the details object I have custom validation (IValidatableObject) that also needs to run.
This appears to generally be working as expected, validations are running and returning ValidationResults and if I put an #Html.ValidationSummary(false); on the page it displays those validations. However I don't want a list of validations at the top, but rather next to the item being validated i.e. Html.ValidationMessageFor which is on the page, but not displaying the relevant message. Is there something I'm missing? This is working on other pages (that don't have this Master-Details situation), so i'm thinking it is something about how I'm going about setting up the list of items to be updated or the editor template for the item?
Edit.cshtml (the Header-Details edit view)
#foreach (var d in Model.Details.OrderBy(d => d.DetailId))
{
#Html.EditorFor(item => d, "Detail")
}
Detail.ascx (the Details Editor Template)
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Detail>" %>
<tr>
<td>
<%= Model.Name %>
<%= Html.HiddenFor(model => model.DetailId) %>
</td>
<td class="colDescription">
<%= Html.EditorFor(model => model.Description) %>
<%= Html.ValidationMessageFor(model => model.Description) %>
</td>
<td class="colAmount">
<%= Html.EditorFor(model => model.Amount) %>
<%= Html.ValidationMessageFor(model => model.Amount) %>
</td>
</tr>
Model is Entity Framework with Header that has Name and HeaderId and Detail has DetailId, HeaderId, Description and Amount
Controller Code:
public ActionResult Edit(Header header, FormCollection formCollection)
{
if (formCollection["saveButton"] != null)
{
header = this.ProcessFormCollectionHeader(header, formCollection);
if (ModelState.IsValid)
{
return new RedirectResult("~/saveNotification");
}
else
{
return View("Edit", header);
}
}
else
{
return View("Edit", header);
}
}
[I know controller code can be cleaned up a bit, just at this state as a result of trying to determine what is occuring here]
IValidatableObject implementation:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (this.Name.Length < 5) && (this.Amount > 10))
{
yield return new ValidationResult("Item must have sensible name to have Amount larger than 10.", new[] { "Amount" });
}
}
I would recommend you to use real editor templates. The problem with your code is that you are writing a foreach loop inside your view to render the template which generates wrong names for the corresponding input fields. I guess that's the reason why you are doing some workarounds in your controller action to populate the model (header = this.ProcessFormCollectionHeader(header, formCollection);) instead of simply using the model binder to do the job.
So let me show you the correct way to achieve that.
Model:
public class Header
{
public IEnumerable<Detail> Details { get; set; }
}
public class Detail : IValidatableObject
{
public int DetailId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int Amount { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if ((this.Name ?? string.Empty).Length < 5 && this.Amount > 10)
{
yield return new ValidationResult(
"Item must have sensible name to have Amount larger than 10.",
new[] { "Amount" }
);
}
}
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new Header
{
Details = Enumerable.Range(1, 5).Select(x => new Detail
{
DetailId = x,
Name = "n" + x,
Amount = 50
}).OrderBy(d => d.DetailId)
};
return View(model);
}
[HttpPost]
public ActionResult Index(Header model)
{
if (ModelState.IsValid)
{
return Redirect("~/saveNotification");
}
return View(model);
}
}
View (~/Views/Home/Index.cshtml):
#model Header
#using (Html.BeginForm())
{
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
#Html.EditorFor(x => x.Details)
</tbody>
</table>
<button type="submit">OK</button>
}
Editor template for the Detail type (~/Views/Shared/EditorTemplates/Detail.ascx or ~/Views/Shared/EditorTemplates/Detail.cshtml for Razor):
<%# Control
Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<MvcApplication1.Controllers.Detail>"
%>
<tr>
<td>
<%= Html.DisplayFor(model => model.Name) %>
<%= Html.HiddenFor(model => model.DetailId) %>
<%= Html.HiddenFor(model => model.Name) %>
</td>
<td class="colDescription">
<%= Html.EditorFor(model => model.Description) %>
<%= Html.ValidationMessageFor(model => model.Description) %>
</td>
<td class="colAmount">
<%= Html.EditorFor(model => model.Amount) %>
<%= Html.ValidationMessageFor(model => model.Amount) %>
</td>
</tr>
Here are a couple of things that I did to improve your code:
I performed the ordering of the Details collection by DetailId at the controller level. It's the controller's responsibility to prepare the view model for display. The view should not be doing this ordering. All that the view should do is display the data
Thanks to the previous improvement I git rid of the foreach loop in the view that you were using to render the editor template and replaced it with a single #Html.EditorFor(x => x.Details) call. The way this works is that ASP.NET MVC detects that Details is a collection property (of type IEnumerable<Detail>) and it will automatically look for a custom editor templated inside the ~/Views/SomeController/EditorTemplates or ~/Views/Shared/EditorTemplates folders called Detail.ascx or Detail.cshtml (same name as the type of the collection). It will then render this template for each element of the collection so that you don't need to worry about it
Thanks to the previous improvement, inside the [HttpPost] action you no longer need any ProcessFormCollectionHeader hacks. The header action argument will be correctly bound from the request data by the model binder
Inside the Detail.ascx template I have replaced <%= Model.Name %> with <%= Html.DisplayFor(model => model.Name) %> in order to properly HTML encode the output and fill the XSS hole that was open on your site.
Inside the Validate method I ensured that the Name property is not null before testing against its length. By the way in your example you only had an input field for the Description field inside the template and didn't have a corresponding input field for the Name property, so when the form is submitted this property will always be null. As a consequence I have added a corresponding hidden input field for it.
i have problem to pass data from view to controller , i have view that is strongly typed with my viewmodel "TimeLineModel", in the first i passed to this view my viewmodel from action on my controller
public ActionResult confirmation(long socialbuzzCompaignId)
{
return View(new TimeLineModel() { socialBuzzCompaignId = socialbuzzCompaignId, BuzzMessages = model });
}
with this i can get info from my action and display it on view , but i have other action POST which i won't get my view model to do some traitement
[HttpPost]
public ActionResult confirmation(TimeLineModel model)
{
}
i can get some propretie of the model but in others no , for example i can get the properti "socialBuzzCompaignId" of model , but other propertie like "IEnumerable BuzzMessages" i can't get it , i dont now why !!
this is the content of my view
#model Maya.Web.Models.TimeLineModel
#{
ViewBag.Title = "confirmation";
}
#using (Html.BeginForm())
{
<h2>confirmation</h2>
<fieldset>
#foreach (var msg in Model.BuzzMessages)
{
<div class="editor-label">
#msg.LongMessage
</div>
<br />
}
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
You need to include BuzzMessages properties within a form element. Since it's not editable, you'd probably want to use hiddens. There are two ways to do this. Easiest is instead of doing a foreach loop, do a for loop and insert them by index.
#for (int i =0; i<Model.BuzzMessages.Count(); i++v)
{
<div class="editor-label">
#Model.BuzzMessages[i].LongMessage
#Html.HiddenFor(m => m.BuzzMessages[i].LongMessage);
</div>
<br />
}
but to do this you'd need to use an IList instead of an IEnumerable in your view model to access by index.
Alternatively, you could create an Editor Template named after your BuzzMessages class (whatever its name is).
#model BuzzMessagesClass
#Html.HiddenFor(m => m.LongMessages)
<!-- Include other properties here if any -->
and then in your main page
#Html.EditorFor(m => m.BuzzMessages)
Check out http://coding-in.net/asp-net-mvc-3-how-to-use-editortemplates/ or search stack overflow if the details of editor templates confuse you.
Just like any HTML POST method, you have to get the data back to the Controller somehow. Just simply "showing" the data on the page doesn't rebind it.
You have to put the data in an input (or a control that will post back) to the appropriate model property name.
So, if you have a model property with name FirstName and you want this data to be rebound to the model on POST, you have to supply it back to the model by placing an "input hidden" (or similar control that postbacks) with the ID of FirstName will rebind that property to the model on POST.
Hope that explains it.
#foreach (var msg in Model.BuzzMessages)
{
<div class="editor-label">
#msg.LongMessage
<input type="hidden" name="BuzzMessages.LongMessage" value="#msg.LongMessage" />
</div>
}
It will post array of LongMessages. Get values like this:
[HttpPost]
public ActionResult confirmation(TimeLineModel model, FormCollection collection)
{
var longMessages = collection["BuzzMessages.LongMessage"];
}
So I have a partial view...
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<NewsletterUnsubscribe_MVC3v2.Models.IntegraRecord>" %>
<% if (!String.IsNullOrEmpty(Model.ErrorMessage))
{%>
<div class="input-validation-error">
<%:Model.ErrorMessage %>
</div>
<% }
else
{%>
<% using (Html.BeginForm())
{%>
<%:Html.ValidationSummary(true)%>
<fieldset>
<legend>IntegraRecord</legend>
<div class="editor-field">
<%:Html.LabelFor(m => m.EmailAddress)%>: <strong><%:Model.EmailAddress%></strong>
</div>
<%:Html.HiddenFor(m=>m.EmailAddress) %>
<div class="editor-field">
Unsubscribe from Area mailings: <%:Html.CheckBoxFor(m => m.AreaUnsubscribe)%>
</div>
<div class="editor-field">
Unsubscribe from Monthly newsletters: <%:Html.CheckBoxFor(m => m.MonthlyUnsubscribe)%>
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
<% }
}%>
When I hit submit and look what's in the posted data I see
EmailAddress:someone#somewhere.co.uk
AreaUnsubscribe:true
AreaUnsubscribe:false
MonthlyUnsubscribe:true
MonthlyUnsubscribe:false
As a result TryUpdateModel returns true but doesn't populate any fields
This gets posted to the controller...
[HttpPost]
public ActionResult GetRecord(IntegraRecord model)
{
if (TryUpdateModel(model))
{
try
{
BusinessLayer.UpdateEmailAddress(model);
}
catch (ArgumentException)
{
return View("Error", ViewBag.Message = "Could Not Update Email Address.");
}
}
return PartialView("GetRecord", model);
}
Any help massively appreciated...
Update: So following the clarification below (Thanks!)
I'm not using a custom model binder so I guess I'm missing some other convention too...
Here's my model...
public class IntegraRecord
{
private const string EmailRegex = #"[snip]";
[Required(ErrorMessage = "Email Address is required")]
[RegularExpression(EmailRegex, ErrorMessage = "This does not appear to be an email address")]
public string EmailAddress;
public bool AreaUnsubscribe;
public bool MonthlyUnsubscribe;
public string ErrorMessage;
public IntegraRecord()
{
}
public IntegraRecord(string email, bool area, bool monthly)
{
EmailAddress = email;
AreaUnsubscribe = area;
MonthlyUnsubscribe = monthly;
}
}
That's how MVC handles checkboxes: asp.net mvc: why is Html.CheckBox generating an additional hidden input (and many other places)
The problem is onthe server side (default model binder is aware of that and doesn't have a problem). Are you using custom model binder?
I have a partial view which I need to re-use:
div class="selectDate">
#using (Html.BeginForm("ViewTransactionLog", "Profile", FormMethod.Get))
{
<div class="selectDateLabel">Date:</div>
<div>
#Html.TextBox("start", range.Start, new { #class = "pickDate" }) to #Html.TextBox("end", range.End, new { #class = "pickDate" })
</div>
<div>
<input type="submit" value="Go" />
</div>
}
</div>
This is the code for picking 2 dates. As the data is lightweight, I wish to pass it through the Get method. I also wish to generalize it and put it into its own cshtml; however, Html.BeginForm expects the controller name and action name to be given if I wish to use the Get method. Is there anyway to avoid this so I could just move the code into a partial view of its own?
Assuming you want the form to post back to the current controller and action, you should be able to use an extension method:
public static MvcForm BeginForm<TModel>(
this HtmlHelper<TModel> html,
FormMethod formMethod)
{
string controller = (string)html.ViewContext.RouteData.Values["controller"];
string action = (string)html.ViewContext.RouteData.Values["action"];
return html.BeginForm(action, controller, formMethod);
}
How can I get access to a variable in a View which was created in a Controller?
Either put the variable into the Model that you are using for your View
Or use a ViewBag variable - e.g. from http://weblogs.asp.net/hajan/archive/2010/12/11/viewbag-dynamic-in-asp-net-mvc-3-rc-2.aspx
public ActionResult Index()
{
List<string> colors = new List<string>();
colors.Add("red");
colors.Add("green");
colors.Add("blue");
ViewBag.ListColors = colors; //colors is List
ViewBag.DateNow = DateTime.Now;
ViewBag.Name = "Hajan";
ViewBag.Age = 25;
return View();
}
and
<p>
My name is
<b><%: ViewBag.Name %></b>,
<b><%: ViewBag.Age %></b> years old.
<br />
I like the following colors:
</p>
<ul id="colors">
<% foreach (var color in ViewBag.ListColors) { %>
<li>
<font color="<%: color %>"><%: color %></font>
</li>
<% } %>
although hopefully you'll be using Razor :)
You need to send the variable to the view in the ViewModel (the parameter to the View() method) or the TempData dictionary.
You can add it to the ViewData[] dictionary or the (newer) ViewBag dynamic.
In your controller:
ViewData['YourVariable'] = yourVariable;
// or
ViewBag.YourVariable = yourVariable;
In your view:
<%: ViewData["yourVariable"] %>
// or
<%: ViewBag.YourVariable %>