I'm trying to submit two forms to one action. I can't figure out is it a good idea and how to do better. Each form has it own partial view and both forms using same model. I mean model is divided by forms.
Here is view model and model:
public class TestViewModel
{
public Foo Foo { get; set; }
// here is some other data for main form
}
public class Foo
{
public string Name { get; set; }
public string Street { get; set; }
}
Action method:
[HttpPost]
public ActionResult Add(Foo foo)
{
return View();
}
Main view:
#model SushiJazz.Models.ViewModels.TestViewModel
<body>
<div id="frmName">
#{Html.RenderPartial("NameFrm"); }
</div>
<div id="frmStreet">
#{Html.RenderPartial("StreetFrm"); }
</div>
<div>
<input type="submit" value="btn" id="btn-submit"/>
</div>
</body>
First form:
#model SushiJazz.Models.ViewModels.TestViewModel
#using (Html.BeginForm("Add", "Test", FormMethod.Post, new { #id="name-frm"}))
{
#Html.TextBoxFor(model => model.Foo.Name);
<input type="submit" value="name" />
}
Second form:
#model SushiJazz.Models.ViewModels.TestViewModel
#using (Html.BeginForm("Add", "Test", FormMethod.Post, new { #id="street-frm"}))
{
#Html.TextBoxFor(model => model.Foo.Street)
<input type="submit" value="street" />
}
I want to submit both forms by btn-submit click to Add action.
I mean not call Add action twice but get Foo.Name and Foo.Street filled from different forms, call Add action and pass fully filled Foo model to it.
Is it possible? Maybe there is some other ways?
I suppose you're just demonstrating a concept as it doesn't make any sense to split those small pieces into separate partial views in this case.
But just move the #using (Html.BeginForm... wrapping from each partial view to include all your mark up in the main view with containing the two #{Html.RenderPartial sections.
Related
I've got a viewmodel that contains other viewmodels.
public class AggregateVM
{
public BrandVM BrandVM { get; set; }
public TinTypeVM TinTypeVM { get; set; }
}
When I http post to the controller action, the TinTypeVM is populated with my edited values, but the BrandVM viewmodel where I used a partial is always null.
Here's are the view.
#model SaveEF.ViewModels.AggregateVM
#using (Html.BeginForm("EditAggregate", "Aggregate"))
{
#Html.Partial("_EditBrand", Model.BrandVM)
#Html.Label("Tin Name")
#Html.EditorFor(model => model.TinTypeVM.Name)
<input type="submit" value="Save" />
}
Here's the partial view.
#model SaveEF.ViewModels.BrandVM
#Html.Label("Brand Name")
#Html.EditorFor(model => model.Name)
Here's the controller action.
public ActionResult EditAggregate(AggregateVM vm)
{
SaveBrand(vm.BrandVM);
SaveTinType(vm.TinTypeVM);
return RedirectToAction("Index");
}
How can I use partials in the view and still pass a single view model into the EditAggregate action? I've tried different parameters in Html.BeginForm("EditAggregate", "Aggregate", FormMethod.Post, new { vm = Model })) but that didn't help.
Short Answer
You need to pass AggregateVM to your partial too.
Long Answer
The way you are doing it right now is:
#model SaveEF.ViewModels.BrandVM
#Html.EditorFor(model => model.Name)
So if you were to inspect the name generated for the editor would be Name. Thus when you post, the default model binder of MVC will look for a matching property in your view model. But you view model is like this:
public class AggregateVM
{
public BrandVM BrandVM { get; set; }
public TinTypeVM TinTypeVM { get; set; }
}
And there is no Name property.
Fix
You need to pass AggregateVM to your partial too.
#model SaveEF.ViewModels.AggregateVM
#Html.EditorFor(model => model.BrandVM.Name)
and now the name for the editor will be BrandVM.Name. So when you post, the default model binder will look for the property BrandVM.Name and find it. Thus it will populate it.
The other alternative is to specify the name attribute yourself by using #Html.Editor or pass the attribute.
When i submit a form with incorrect data within a view component, my view component does not display any errors on the page.
So is it possible for a view component to return validation errors ?
View Component
<div class="card-block">
<form class="form-inline-custom" asp-controller="BragOption" asp-action="Create" method="post" role="form">
<div class="form-group">
<label asp-for="CreateBragOptionViewModel.PeriodFrom">From <span asp-validation-for="CreateBragOptionViewModel.PeriodFrom" class="alert-danger"></span></label>
<input asp-for="CreateBragOptionViewModel.PeriodFrom" class="form-control">
</div>
<div class="form-group">
<label asp-for="CreateBragOptionViewModel.PeriodTo">To <span asp-validation-for="CreateBragOptionViewModel.PeriodTo" class="alert-danger"></span></label>
<input asp-for="CreateBragOptionViewModel.PeriodTo" class="form-control">
</div>
<div class="form-group">
<button type="submit" class="btn btn-block btn-primary">START VOTING PERIOD</button>
</div>
</form>
</div>
Action Controller
[HttpPost]
public IActionResult Create(BragOptionViewModel model)
{
if (! ModelState.IsValid)
{
return View(nameof(BragManagementController.Index), model);
}
if (! _bragOptionService.IsVotingPeriodFromValid(model.CreateBragOptionViewModel))
{
ModelState.AddModelError("PeriodFrom", "The date you have entered should not be in the future");
return View(nameof(BragManagementController.Index), model);
}
if (!_bragOptionService.IsVotingPeriodToValid(model.CreateBragOptionViewModel))
{
ModelState.AddModelError("PeriodTo", "The date you have entered should not be in the past");
return View(nameof(BragManagementController.Index), model);
}
_bragOptionRepository.CreateVotingPeriod(_bragOptionService.LoadBragOption(model.CreateBragOptionViewModel));
return RedirectToAction(nameof(BragManagementController.Index), "BragManagement");
}
view models
public class CreateBragOptionViewModel
{
[DataType(DataType.Date)]
public DateTime PeriodFrom { get; set; }
[DataType(DataType.Date)]
public DateTime PeriodTo { get; set; }
}
public class BragOptionViewModel
{
public CreateBragOptionViewModel CreateBragOptionViewModel { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{MMM 0:d, yyyy}", ApplyFormatInEditMode = true)]
public DateTime VotingPeriodTo { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{MMM 0:d, yyyy}", ApplyFormatInEditMode = true)]
public DateTime VotingPeriodFrom { get; set; }
}
Haitham's comment points to the right path. Based on your code, I think that you are looking for both unobtrusive client side validation as well as a display of the validation summary in the event that the ModelState.IsValid property is false or there are programmer defined logic constraints that can't be detected until the model data reaches the controller.
In the asp.net core docs, it looks like most of this information is covered in the following link:
https://docs.asp.net/en/latest/tutorials/first-mvc-app/validation.html?highlight=validation
Client Side Unobtrusive Validation Needs both the javascript libraries and the correct HTML markup and attributes. The markup is auto-matically added in VS when you allow VS to create a strongly typed view that is bound to your model or viewmodel class.
JAVASCRIPT libraries In Order For Unobtrusive Validation
jquery.js (or min)
jquery.validate.js (or min)
jquery.validate.unobtrusive.js (or min)
HTML Markup
Spans after the input elements can display client side validation errors, in your code it would be something like adding spans with
<span asp-validation-for="<your model property>"></span>
Ex).
<label asp-for="CreateBragOptionViewModel.PeriodFrom">From <span asp-validation-for="CreateBragOptionViewModel.PeriodFrom" class="alert-danger"></span></label>
<input asp-for="CreateBragOptionViewModel.PeriodFrom" class="form-control">
<span asp-validation-for=""CreateBragOptionViewModel.PeriodFrom"></span>
Additionally, if you don't have a client side input error but you add an error to the ModelState manually after the data reaches the controller, you can display or render the model error data in a div with the validation-summary attribute inside your form when the model is returned to the view.
<form class="form-inline-custom" asp-controller="BragOption" asp-action="Create" method="post" role="form">
<div asp-validation-summary="ModelOnly"></div>
I am having a problem returning values to the controller when using a ViewModel.
For clarity I have simplified the code below where the original has many more fields.
When the page is loaded, the value in the hidden field is as expected. However when the form is submitted the value in the field is not being sent and instead I get an ArgumentNullException.
Please can you advise on what I am doing wrong.
View
#model Project.Models.SCView
#using (Html.BeginForm("ScorecardEdit"))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.FV.ID)
<input type="submit" value="Save" />
}
Model
public class FixView
{
public int ID { get; set; }
[DisplayFormat(DataFormatString = "{0:ddd dd/MM/yyyy}")]
public DateTime MatchDate { get; set; }
}
public class SCView
{
public FixView FV { get; set; }
public SCView()
{
this.FV = new FixView();
}
}
Controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ScorecardEdit(SCView ReturnSC)
{
}
The code that you have should be working as MVC should be able to map your FV.ID property as expected as the HiddenFor() helper will generate the proper names to handle mapping it :
Using the same code that you provided, the debugger demonstrated the following after submitting the form :
The issue here sounds like you have a few other properties, possibly collection-based ones that use the DropDownListFor() helpers that have collections which are not posted to the server, so when you attempt to use the model you have to render populate one of those helpers, you are getting your ArgumentNullException.
I have a view with three submit buttons. First button must validate some fields, second button must validate other fields, third button doesn't validate anything.
How can I do that on both client (unobtrusive) and server sides?
Most likely with alot of hand rolled usage, especially if you want it to be unobtrusive. You're going to need to create something like <input data-validationgroup="group1"... and then on the click action that your javascript code will then validate what you want. I would imagine jQuery Validate has some type of ruleset support but you'll have to figure that out.
You're going to have to do a similar sequence on the server side, and create ruleset type validation classes/blocks/methods that you interpret the submit action to the relevant ruleset. I'd look at a library like FluentValidation for this part.
To achieve what you want it, is is very extremely unlikely you will be able to achieve this using the DataAnnotations attributes on your model class.
Personally I have always liked and used the FluentValidation.NET library in all my projects. Not only that it is very powerful in terms of expressing validation rules but this library has an excellent integration with ASP.NET MVC. So I will try to provide a sample solution for this problem using it (only server side validation for the moment, later we can talk about unobtrusive client side validation if you want).
So start a new ASP.NET MVC 3 project using the default template and install the FluentValidation.MVC3 NuGet package (the current stable version is 2.0.0.0).
Then let's define a view model:
public class MyViewModel
{
public string Field1 { get; set; }
public string Field2 { get; set; }
}
Now we can assume that if button1 is clicked Field1 is required and if button2 is clicked Field2 is required and if button3 is clicked none of them are required. A fictional scenario but pretty close to your requirements.
Now let's define two different fluent validators for this model corresponding each to button1 and button2:
public class MyModelValidator1 : AbstractValidator<MyViewModel>
{
public MyModelValidator1()
{
RuleFor(x => x.Field1)
.NotEmpty();
}
}
public class MyModelValidator2 : AbstractValidator<MyViewModel>
{
public MyModelValidator2()
{
RuleFor(x => x.Field2)
.NotEmpty();
}
}
Now because it is only at runtime that we know which button is clicked we need to apply the correct validator based on value in the request. So let's write a custom validator provider factory:
public class MyFactory : IValidatorFactory
{
private readonly Func<HttpContextBase> _contextProvider;
public MyFactory(Func<HttpContextBase> contextProvider)
{
_contextProvider = contextProvider;
}
public IValidator GetValidator(Type type)
{
if (type == typeof(MyViewModel))
{
var context = _contextProvider();
if (!string.IsNullOrEmpty(context.Request["button1"]))
{
return new MyModelValidator1();
}
if (!string.IsNullOrEmpty(context.Request["button2"]))
{
return new MyModelValidator2();
}
}
return null;
}
public IValidator<T> GetValidator<T>()
{
return (IValidator<T>)GetValidator(typeof(T));
}
}
and register it in Application_Start:
ModelValidatorProviders.Providers.Add(
new FluentValidationModelValidatorProvider(
new MyFactory(() => new HttpContextWrapper(HttpContext.Current))
)
);
and that's pretty much all. Now what's left is trivial.
A controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel();
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
return Content("Thanks for submitting", "text/plain");
}
}
and a view:
#model MyViewModel
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(x => x.Field1)
#Html.EditorFor(x => x.Field1)
#Html.ValidationMessageFor(x => x.Field1)
</div>
<div>
#Html.LabelFor(x => x.Field2)
#Html.EditorFor(x => x.Field2)
#Html.ValidationMessageFor(x => x.Field2)
</div>
<input type="submit" value="Submit with button 1" name="button1" />
<input type="submit" value="Submit with button 2" name="button2" />
<input type="submit" value="Submit with button 3" name="button3" />
}
I have a View that allows a user to enter/edit data for a new Widget. I'd like to form up that data into a json object and send it to my controller via AJAX so I can do the validation on the server without a postback.
I've got it all working, except I can't figure out how to pass the data so my controller method can accept a complex Widget type instead of individual parameters for each property.
So, if this is my object:
public class Widget
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
I'd like my controller method to look something like this:
public JsonResult Save(Widget widget)
{
...
}
Currently, my jQuery looks like this:
var formData = $("#Form1").serializeArray();
$.post("/Widget/Save",
formData,
function(result){}, "json");
My form (Form1) has an input field for each property on the Widget (Id, Name, Price). This works great, but it ultimately passes each property of the Widget as a separate parameter to my controller method.
Is there a way I could "intercept" the data, maybe using an ActionFilterAttribute, and deserialize it to a Widget object before my controller method gets called?
Thanks Jeff, that got me on the right path. The DefaultModelBinder is smart enough to do all the magic for me...my problem was in my Widget type. In my haste, my type was defined as:
public class Widget
{
public int Id;
public string Name;
public decimal Price;
}
Notice that the type has public fields instead of public properties. Once I changed those to properties, it worked. Here's the final source code that works correctly:
Widget.aspx:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Widget.aspx.cs" Inherits="MvcAjaxApp2.Views.Home.Widget" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<script src="../../Scripts/jquery-1.2.6.js" type="text/javascript"></script>
<script type="text/javascript">
function SaveWidget()
{
var formData = $("#Form1").serializeArray();
$.post("/Home/SaveWidget",
formData,
function(data){
alert(data.Result);
}, "json");
}
</script>
<form id="Form1">
<input type="hidden" name="widget.Id" value="1" />
<input type="text" name="widget.Name" value="my widget" />
<input type="text" name="widget.Price" value="5.43" />
<input type="button" value="Save" onclick="SaveWidget()" />
</form>
</asp:Content>
HomeController.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;
namespace MvcAjaxApp2.Controllers
{
[HandleError]
public class HomeController : Controller
{
public ActionResult Index()
{
ViewData["Title"] = "Home Page";
ViewData["Message"] = "Welcome to ASP.NET MVC!";
return View();
}
public ActionResult About()
{
ViewData["Title"] = "About Page";
return View();
}
public ActionResult Widget()
{
ViewData["Title"] = "Widget";
return View();
}
public JsonResult SaveWidget(Widget widget)
{
// Save the Widget
return Json(new { Result = String.Format("Saved widget: '{0}' for ${1}", widget.Name, widget.Price) });
}
}
public class Widget
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
Note that (in MrDustpan's solution) the parameter name widget in the MVC Action method must match with the prefix used in the name attribute in the ASPX file.
If this is not the case then the Action method will always receive a null object.
<input type="text" name="widget.Text" value="Hello" /> - OK
<input type="text" name="mywidget.Text" value="Hello" /> - FAILS
Phil Haack has a good blog post about model binding that might be helpful. Not 100% what you're talking about here, but I think it might give you a better overall understand about the DefaultModelBinder.
What you want to do is structure your javascript form object in the same way your backend object is structured:
{ Id : "id", Name : "name", Price : 1.0 }
Then use the toJSON plugin to convert it into the above string. You send this string to your backend and use something like the JayRock libraries to convert it to a new Widget object.