view component data input validation - c#

I've been trying to play around with view components in ASP.NET Core to create a more "widget" based system. What I currently have is a contact form that opens a modal dialog and displays a view component based on the selection they make. For simplicity sake, I invoke the viewcomponent as follows from my layout page (yes I'm not using the vc: convention atm):
#await Component.InvokeAsync("Inquiry")
It will then invoke the inquiry form through here:
public class InquiryViewComponent : ViewComponent
{
private readonly ILogger<InquiryViewComponent> _logger;
private readonly IServiceRepository _serviceRepository;
public InquiryViewComponent(ILogger<InquiryViewComponent> logger, IServiceRepository serviceRepository)
{
_logger = logger;
_serviceRepository = serviceRepository;
}
public async Task<IViewComponentResult> InvokeAsync()
{
// do stuff here like create a backing model and then return the view
return await Task.FromResult(View("~/Views/Inquiry/Inquiry.cshtml", _viewModel));
}
}
Everything loads correctly and the modal dialog opens (have a script to invoke the modal on open) with the following:
#model InquiryViewModel
//modal initialization here
//modal header here
#using (Html.BeginForm("Inquiry", "Inquiry"))
{
<div class="modal-body">
<div class="container">
<div class="row mb-2">
<div class="col-md-6">
#Html.LabelFor(m => m.Inquire.FirstName, new { #class = "required" })
#Html.TextBoxFor(m => m.Inquire.FirstName, new { #class="form-control", autocomplete="given-name", required="required" })
</div>
<div class="col-md-6">
#Html.LabelFor(m => m.Inquire.LastName, new { #class = "required" })
#Html.TextBoxFor(m => m.Inquire.LastName, new { #class = "form-control", autocomplete = "family-name", required = "required" })
</div>
</div>
//OTHER FIELDS HERE
</div>
//FOOTER
}
Once the user submits, if the model is invalidated, it posts back to the view Inquiry which I don't want. I do understand that it is trying to return the page on the form (I can see where this is happening). I also understand I could use client-side validation but I'm aiming to build more complex widgets and don't want a pile of JavaScript code. What I want to know: if I have a form based view component and it needs model validation from the server, can I have it return the result WITHOUT rendering the page? Is this implementation just completely incorrect for view components and what other ways are more suitable?

Related

MVC - Returning partial view is creating a new page

I have a form with some input fields. When a user clicks on the submit I want to post the fields back, use them to query a service and load some results into a view on the same page while preserving the original input. For this, I've scaffolded a view using my model and added a partial view below to populate the results with.
....bottom of form <div class="form-group">
#Html.LabelFor(model => model.Amount, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Amount, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.PostalAmount, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Search" class="btn btn-default" />
</div>
</div>
</div>
<div class="list-group">
#Html.Partial("_ResultsPartial")
</div>
In my controller, I have 2 actions, one to create the initial view and one for the post back.
[HttpGet]
public ActionResult Search()
{
ViewBag.Message = "Enter search details";
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public PartialViewResult Search(SearchCriteria searchCriteria)
{
List<SearchResult> results = new List<SearchResult>();
if (ModelState.IsValid)
{
//await go off and do the actaul search
results = _dataProvider.GetData(searchCriteria);
}
return PartialView("~/Views/Shared/_ResultsPartial.cshtml", results);
}
So in order to render the partial view, I use return PartialView passing in the new model which contains the results of my search. One of the problems I'm having with this is that creates a new page, i.e. the partial view doesn't load on the existing page. What other way is there to render a partial view while in a controller action so that the partial view loads on the page it was created?
Try replacing your rendering with something like:
#{Html.RenderAction("Results", new {results = model.Results})};
Try this one
return PartialView("_ResultsPartial", results);

MVC 5 Viewmodel binding works but post back is partial filled

I have a parameterless Index for the HttpGet which works. But when I post it the HttpPost version of Index is invoked and the viewmodel object is passed in, but there is only the value of the dropdown in it. The rest is null (products, title)
[HttpPost]
public ActionResult Index(ProductsViewModel pvm)
{
// breakpoint on line 36, shows that pvm.Title is null and Products too.
return View(pvm);
}
My compilable and running example can be downloaded from my OneDrive http://1drv.ms/1zSsMkr
My view:
#model KleinKloteProductOverzicht.Models.ProductsViewModel
#using (Html.BeginForm("Index", "Products"))
{
<h2>#Html.DisplayFor(m => m.Title)</h2>
<input type="submit" value="post dit" /><br/>
<div class="row">
<div class="col-lg-2 col-md-2">
#Html.DropDownListFor(x => x.CurrentSort, EnumHelper.GetSelectList(typeof(SortOptions)), new { #class = "multiselect"})
</div>
</div>
if (Model.Products.Count() > 0)
{
<div class="row">
#foreach (var item in Model.Products)
{
#Html.DisplayFor(i => item.Name);
}
</div>
}
}
If I have this view model:
public class ViewModel
{
public string Name {get;set;}
public string SelectedLocation {get;set;}
public IEnumerable<SelectListItem> Locations {get;set;}
}
And your actions look like this:
public ActionResult MyForm()
{
var vm = new ViewModel
{
Locations = context.Locations.ToList() // Some database call
}
return View(vm);
}
[HttpPost]
public ActionResult MyForm(ViewModel vm)
{
vm.Locations // this is null
}
It is null because the model binder can't find a form control that is setting its data.
The <form> must set some data in the view for the model binder to pick it up.
<form>
Name: <input type="text" id="name" />
</form>
This will set the Name property on the view model, because the model bind can see the id of the form control and uses that to know what to bind to.
So in terms of your view, you need to make sure you wrap any content that you want to post back to the server with #using(Html.BeginForm())
Anyway this is my guess.
Well, you seem to be confused as to how [HttpPost] and form tags interact with eachother.
You see, when .NET MVC binds your parameters in your controller actions, it tries to derive that data from the request. For [HttpGet] it does this by looking at the query string.
For [HttpPost] calls, it also looks at the Request.Form. This variable is populated with the values of all input fields that were inside the form you submitted.
Now, this is your view:
#using (Html.BeginForm("Index", "Products"))
{
<h2>#Html.DisplayFor(m => m.Title)</h2>
<input type="submit" value="post dit" /><br/>
<div class="row">
<div class="col-lg-2 col-md-2">
#Html.DropDownListFor(x => x.CurrentSort, EnumHelper.GetSelectList(typeof(SortOptions)), new { #class = "multiselect" })
</div>
</div>
if (Model.Products.Count() > 0)
{
<div class="row">
#foreach (var item in Model.Products)
{
#Html.DisplayFor(i => item.Name);
}
</div>
}
}
You only have one select tag (generated by Dropdownlistfor) but no other inputs. That's why .NET MVC cannot infer any other data for your view model.
If you change your view to this:
#model KleinKloteProductOverzicht.Models.ProductsViewModel
#using (Html.BeginForm("Index", "Products"))
{
<h2>#Html.DisplayFor(m => m.Title)</h2>
<input type="submit" value="post dit" /><br/>
<div class="row">
<div class="col-lg-2 col-md-2">
#Html.DropDownListFor(x => x.CurrentSort, EnumHelper.GetSelectList(typeof(SortOptions)), new { #class = "multiselect" })
</div>
</div>
if (Model.Products.Count() > 0)
{
<div class="row">
#for (var i = 0; i < Model.Products.Count; i++)
{
#Html.DisplayFor(model => model.Products[i].Name)
#Html.HiddenFor(model => model.Products[i].ID)
}
</div>
}
}
You'll see I've added a hidden input (<input type="hidden">) for the product id. Note that the product name still will be null.
I would suggest you follow a tutorial on .NET MVC and read up on some of the concepts behind it, because the very fact that you ask this question reveals that you have much to learn.
Best of luck!
P.S. One last tip: #Html.Blablabla writes directly to your view. You usually don't need that ";" at the end, because it will be inside your generated html.
Your property is not associated with a "postable" control, therefore it will not be submitted along with the form data. If your really want to get the value in your Title property, just set it as a hidden input.
#Html.HiddenFor(m => m.Title)
A label will not be posted when submitting a form but an input will. This is exactly what HiddenFor does; it creates a hidden input element which will be picked up by the form submit.

MVC submit isn't returning all data

I'm writing an MVC app which ends up accessing a SQL database. On my edit page, I previously had every item available to be edited that is in the model. Recently I was asked to no longer allow the user to edit the primary keys. I did a quick change to change the primary key fields (in this example, there are 2 of them) from an EditorFor to a DisplayFor. The new view is this:
#model App.Data.Item
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Item</h4>
<hr />
#Html.ValidationSummary(true)
<div class="form-group">
<strong>ID:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID)</p>
</div>
</div>
<div class="form-group">
<strong>ID2:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID2)</p>
</div>
</div>
<div class="form-group">
<strong>Description:</strong>
<div class="col-md-10">
<p>#Html.EditorFor(model => model.Description)
#Html.ValidationMessageFor(model => model.Description)</p>
</div>
</div>
<br />
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<button type="submit" class="btn btn-primary">Submit <i class="fa fa-caret-right"></i></button>
</div>
</div>
</div>
}
It used to work with the full editing. Now the data is displayed properly, as expected. However, when submit is pressed, Null values are passed back to the controller for the values that are displayed.
These are the edit functions in my controller. ItemService.Edit() just saves the data to the server. It works correctly.
[Authorize]
public ActionResult Edit(string id)
{
if (id == null)
{
//return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
string[] vals = id.Split('|');
ItemAttribute itemAttribute = itemAttributeService.Find(int.Parse(vals[0]), vals[1]);
if (itemAttribute == null)
{
return HttpNotFound();
}
return View(itemAttribute);
}
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize]
public ActionResult Edit([Bind(Include = "ID,ID2,Description")]
Item item)
{
if (item.Description == null)
{
ModelState.AddModelError("Description", "Description cannot be null.");
}
if (ModelState.IsValid)
{
itemService.Edit(item);
return RedirectToAction("../Home/Index/");
}
return View(item);
}
Lastly, my data model:
public class Item
{
public int ID { get; set; }
public string ID2 { get; set; }
public string Description { get; set; }
}
Why is the data no longer being passed back to the second function, and how do I get it to pass correctly so that I can save it to the database?
You need to have an input element generated for the items that you want returned. Currently, you are only displaying two of your model elements and have no associated input with them. As a result, they will not be POSTed back to the server.
To get them to post to the server and not "editable" from a textbox, add a Html.HiddenFor() helper for each of the items that you need returned.
<div class="form-group">
<strong>ID:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID)</p>
<p>#Html.HiddenFor(model => model.ID)</p>
</div>
</div>
<div class="form-group">
<strong>ID2:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID2)</p>
<p>#Html.HiddenFor(model => model.ID)</p>
</div>
</div>
However, keep in mind that anyone can edit the HTML using Firebug or Chrome console tools and submit any value that they want for any field, even if you did not want to change it. Be sure that when you are persisting the changes to the database, you are NOT including these fields as part of the update.
Try this, just before this line of code:
#Html.DisplayFor(model => model.ID)
put in this for debugging:
#Html.LabelFor(model => model.ID)
Tell us what you see...
If you see the label then check your controller, in particular the parameter it takes on the post. It should take and Item of type ITEM per your model.
Before the controller receives the data MVC has to try to populate the data... It converts name/value pairs to model types with values secretly. If you don't see any data after you are in the controller it's usually because the names were not found!

When using Html.Action in layout getting error: Child actions are not allowed to perform redirect actions

I'll try to keep this as brief as possible.. I have a ViewResult that I am rendering on my _layout.cshtml page. This ViewResult is in a controller called CommonController where I am keeping some actions and data that I need on every page. If it matters, my ViewResult is a simple form.
ViewResult and the CommonController
[Authorize]
public class CommonController : AuthenticatedBaseController
{
[ChildActionOnly]
public ViewResult OrgSwitch()
{
//do stuff
return View();
}
[HttpPost]
public RedirectToRouteResult OrgSwitch(string UserOrgs)
{
return RedirectToAction("Index", "Recruiter", new { orgId = UserOrgs, area = "InkScroll" });
}
}
In my _Layout.cshtml page I render it like so:
#Html.Action("OrgSwitch", new { controller = "Common", area = "MyArea" })
This is all working fine, but now I am adding some functionality elsewhere in the app. I had asp.net mvc5 scaffold out a controller and views for a class I need to CRUD against. Just the quick and dirty forms so I can test a couple of things. I am going into the create page to add a new item. warning, it is mvc scaffolding, very ugly:
#model inkScroll.Models.Vacancy
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Vacancy</h4>
<hr />
#Html.ValidationSummary(true)
<div class="form-group">
#Html.LabelFor(model => model.JobRef, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.JobRef)
#Html.ValidationMessageFor(model => model.JobRef)
</div>
</div>
// lots more ugly code
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
When I click 'submit' in the controller it does the usually validation checks and stuff. If my form is valid, there is no problem. If there are modelstate issues, I get the child action error.
Any ideas?
EDIT to clarify, the razor page you see is a scaffolded page that posts to a seperate JobsController. The data being passed into the post of the 'create' page is working fine, I am only getting the error if the model validation fails.

Adding jQuery to forms with the same model in MVC3

I am trying to have two submission forms with similar functionality on the same view, where each form is strongly typed to the same model. I then am trying to add a datepicker to the same input on both forms, but I am unsure of how to do this. Here is my code:
#using (#Ajax.BeginForm(...
new { id = "editScheduleForm" }))
{
<div class="editor-label">
#Html.LabelFor(model => model.StartTime)
</div>
<div class="editor-field">
<input
#Html.EditorFor(model => model.StartTime)
#Html.ValidationMessageFor(model => model.StartTime)
</div>
}
...
#using (#Ajax.BeginForm(...
new { id = "addScheduleForm" }))
{
<div class="editor-label">
#Html.LabelFor(model => model.StartTime)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.StartTime)
#Html.ValidationMessageFor(model => model.StartTime)
</div>
}
Both of these forms are in their own strongly-typed partial views. I naturally tried simply adding the datepicker in jQuery to both partial views, like so:
$(function () {
$("#StartTime").datepicker();
});
But it unsurprisingly only worked for one of the inputs. I have been trying to use the HTML id that I added to both Ajax form declarations (e.g. editScheduleForm and addScheduleForm), but am unsure of how to do this. Any suggestions?
Solution:
As scottm suggested, I looked through documentation for using a custom editor template. I read this, detailing functionality I didn't know existed:
http://msdn.microsoft.com/en-us/library/ee407399.aspx
Here, you can specify a template, and then add a parameter for the htmlFieldName, which is specifically designed to circumvent the problem I was happening.
You can add a class to the inputs in your editor template. Then you can then use the class as the jQuery selector.
Add a file called DateTime.cshtml under ~/Views/Shared/EditorTemplates, and add the following:
#model DateTime
#Html.TextBox("", (Model.HasValue ? Model.Value.ToShortDateString() : string.Empty), new { #class = "date" })
Then add this jQuery to the page.
$(function () {
$(".date").datepicker();
});
The ID must be unique in a document.
Never rely on multiple ids on a page working correctly. Matter of fact, despite what most examples in blogs do, you should hardly ever use ids as jquery selectors.
Like #scottm said. You should use classes to tag items which you want to be widget-ized.
Here's a simplistic bit of code so you can create jq ui widgets declaratively:
$(function() {
$('[data-usewidget]').each(function() {
var $this = $(this), widget = $this.data('usewidget');
$.fn[widget] && $this[widget]();
});
});
Then in your html
<input name=startTime data-usewidget=datepicker />

Categories