So what I want to do is take a string from a textarea and pass it to an action method overload (string paramJSON).
Action method:
public ActionResult SendMail(string templateName, string receiver, string paramJSON)
{
var paramDictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(paramJSON);
new SendMailClient().Send(templateName, receiver, paramDictionary);
if(Request.IsAjaxRequest())
{
return RedirectToAction("SendPartial", "TestMail");
}
return View();
}
Textarea:
#Html.TextAreaFor(a => a.TestParametrar, new { id = "paramTxt" })
Your html helper
#Html.TextAreaFor(a => a.TestParametrar, new { id = "paramTxt" })
generates a textarea with name="TestParametrar". When you submit a form, it sends back the values of each controls name and value attributes in this case TestParametrar: 'The text you entered' You method needs to include a parameter with the same name, for example
public ActionResult SendMail(string TestParametrar, ....)
and the value of the parameter will be the text entered in the form control.
However, since you view is based on a model, then it is better to just post back to the model and all properties will be bound
public ActionResult SendMail(YourModel model)
which has the added benefit of validating your properties. For example, if property TestParametrar has the [Required] attribute, then if the user does not enter a value, ModelSTate will be invalid and the view can be returned for correction.
#using (Html.BeginForm("SendMail2"))
{
#Html.TextAreaFor(a => a.TestParametrar, new { id = "paramTxt" })
<input type="submit" value="Send Message" />
}
And:
public ActionResult SendMail2(string TestParametrar)
{
return SendMail("myTemplate", "hello#world.com", TestParametrar);
}
Related
Under ASP we could bound a control with a model which has member
public string Contact { get; set; }
or directly <input type="email" asp-for="item.Contact"> or through corresponding HTML helper
As well we could use Data Annotation instead of implicitly declare type in Razor page
[EmailAddress]
public string Contact { get; set; }
But what to do if I would like to enter the list of email addresses separated by comma?
It is correct that unbounded HTML5 code <input type="email" multiple> works under latest browsers:
Multiple attribute for type="email" does not work. But when I am trying to bound it to the model it looks like EmailAddressAttribute is applied to the model and only one email address could be validated
Like #pcalkins said, the browsers will not separate it for you, you have to implement some split emails functionality like so
// 1. put this helper in your utilty class
private IEnumerable<string> GetEmails(string input)
{
if (String.IsNullOrWhiteSpace(input)) yield break;
MatchCollection matches = Regex.Matches(input, #"[^\s<]+#[^\s,>]+");
foreach (Match match in matches) yield return match.Value;
}
// 2. now call it to get list of emails
// for e.g. string strEmails = "Last, First <name#domain.com>, name#domain.com, First Last <name#domain.com>..";
string allContactsWithCommas = model.contactsWithCommas;
IEnumerable<string> emails = GetEmails(allContactsWithCommas );
// 3. try to give it something custom to validate
//[Required, MinLength(1, ErrorMessage = "Some validation error")]
[YourClassHoldingObject]
public List<int> Contact { get; set; }
// 4. or implement something custom in for your validation object, so the broswer knows how to handle/waht to call for validation
public class YourClassHoldingObject : IValidatableObject
{
[Required]
List<int> Contact
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// your contact logic validation here
if (Contact.Count < 1)
{
// some validation using System.ComponentModel.DataAnnotations
// - please customize for your needs
yield return new ValidationResult(
$"At least one email should be specified.", new[]
{ System.ComponentModel.DataAnnotations.EmailAddressAttribute().IsValid(
// for e.g. "email#istart.work") });
Contact) });
}
}
}
This weekend a lot of struggle with a View Component.
I try to add a dropdownlist that does an auto postback onchange. This dropdownlist is on a view component.
I have 2 problems:
I don't get the asp-page-handler after the post, does it work like I implemented it on the form-tag?
Post calls method public void OnPost on razor page containing view
component. I would think it would be better to have a method on the
View Component like OnChangeProject?
The code of my View (View Component):
<form asp-page-handler="ChangeProject" method="post">
#Html.AntiForgeryToken()
#Html.DropDownList("id", new SelectList(Model, "Id", "Id"), new { onchange = "this.form.submit()" })
</form>
Thanks in advance!!
I exprienced the same problem and the way i fixed it is already answered in your question.
The form call is made at the page where you got your View Component embedded. I don't think it would be even possible to call a handler in your View Component with asp-page-handler as this is Razor Pages tag helper.
The way i got it work is simply putting the page-handler method on the PageModel that is embedding the View Component. In your case you can simply implement this handler on your Razor Page:
public IActionResult OnPostChangeProject()
{
// ... do Something
}
I don't know though how it would work to trigger a controller method in your View Component. Possibly create a new Controller class and route to it with asp-controller and asp-action in your form tag.
You should remember that the Page handlers could be viewed as convenience methods.
All the ASP.Net Core framework does is looks at the Query string parameters and Form data and translates it into Page handler calls.
And even though the Handlers are not available in View Components or Partial Views you still can get access to all the required ingredients by injecting IHttpContextAccessor into the View.
It will provide you with HttpContext.Request which contains both the Query and the Form properties.
You can then create your own Handler mapper. Here is one, for example:
public class HandlerMapping
{
public string Name { get; set; }
public System.Delegate RunDelegate { get; set; }
public HandlerMapping(string name, Delegate runDelegate)
{
RunDelegate = runDelegate;
Name = name;
}
}
public class PartialHandlerMapper
{
IHttpContextAccessor _contextAccessor;
public PartialHandlerMapper(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public void RouteHandler(List<HandlerMapping> handlerMappings, string PartialDescriminatorString = null)
{
var handlerName = _contextAccessor.HttpContext.Request.Query["handler"];
var handlerMapping = handlerMappings.FirstOrDefault(x => x.Name == handlerName);
if (handlerMapping != null)
{
IFormCollection form;
try
{
form = _contextAccessor.HttpContext.Request.Form;
}
catch
{
return;
}
if (!string.IsNullOrWhiteSpace(PartialDescriminatorString) && form[nameof(PartialDescriminatorString)] != PartialDescriminatorString)
return;
List<Object> handlerArgs = new List<object>();
var prmtrs = handlerMapping.RunDelegate.Method.GetParameters();
foreach (var p in prmtrs)
{
object nv = null;
var formValue = form[p.Name];
if (!StringValues.IsNullOrEmpty(formValue))
{
try
{
nv = TypeDescriptor.GetConverter(p.ParameterType).ConvertFromString(formValue);
}
catch (FormatException)
{
//throw new FormatException($"Could not cast form value '{formValue}' to parameter {p.Name} (type {p.ParameterType}) of handler {handlerName}. Make sure you use correct type parameter. ");
nv = Activator.CreateInstance(p.ParameterType);
}
catch (ArgumentException)
{
nv = Activator.CreateInstance(p.ParameterType);
}
}
else
nv = Activator.CreateInstance(p.ParameterType);
handlerArgs.Add(nv);
}
handlerMapping.RunDelegate.DynamicInvoke(handlerArgs.ToArray());
}
}
}
And inject it into the service container:
services.AddScoped<PartialHandlerMapper>();
And here is a shopping cart partial view code section example:
#inject ShoppingManager shoppingManager
#inject PartialHandlerMapper partialHandlerMappping
#{
string ToggleCartItemTrialUseHandler = nameof(ToggleCartItemTrialUseHandler);
string DeleteCartItemHandler = nameof(DeleteCartItemHandler);
List<HandlerMapping> handlerMappings = new List<HandlerMapping> {
new HandlerMapping (ToggleCartItemTrialUseHandler, (Guid? PicID, bool? CurrentValue) => {
if (PicID == null || CurrentValue == null)
return;
shoppingManager.UpdateTrial((Guid)PicID, !(bool)CurrentValue);
}),
new HandlerMapping (DeleteCartItemHandler, (Guid? PicID) => {
if (PicID == null)
return;
shoppingManager.RemoveProductFromCart((Guid)PicID);
})
};
partialHandlerMappping.RouteHandler(handlerMappings);
var cart = shoppingManager.GetSessionCart();
}
Form element example from the same view:
<td align="center" valign="middle">
<form asp-page-handler="#DeleteCartItemHandler">
<input name=PicID type="hidden" value="#i.PicID" />
<button>
Delete
</button>
</form>
</td>
Where #i is an Item in the shopping cart
It's possible to create a combo (Controller/ViewComponent) by decorating the controller with a ViewComponent(Name="myviewcomponent").
Then create the invokeasync as usual, but because the controller doesn't inherit from a ViewComponent, the return result would be one of the ViewComponent result (ViewViewComponentResult, et).
The form in the viewcomponent can then have a button with asp-controller/action tag helpers targetting the controller/action.
I've a main view that contains an array received from the corresponding Action and it also contains a partial view reference below
Create.cshtml :
#model HrAndPayrollSystem.Models.EmployeeMasterA
#using (Html.BeginForm())
{
ViewData["fs_lbls"] = ViewBag.FS_lbls as string[];
#Html.Partial("~/Views/EmployeeMasterA/FinalSettlementTAB.cshtml", Model)
}
and the referenced partial view above is defined below
FinalSettlementTAB.cshtml :
#model HrAndPayrollSystem.Models.EmployeeMasterA
#Html.DropDownList("DeptId", null, "Department")
/* Print "ViewData["fs_lbls"]" array defined in the Main View `Create.cshtml` here */
I've an array defined in the Create.cshtml, now, I want to pass it into the partial view HR_EmployeeFinalSettlementTAB.cshtml and print it, What is the proper way to to this?
What I've tried :
I changed the #Html.Partial() line into below :
#Html.Partial("~/Views/EmployeeMasterA/FinalSettlementTAB.cshtml", null, new ViewDataDictionary { { "fs_lbls", ViewData["fs_lbls"] } })
and modified the FinalSettlementTAB.cshtml file as below :
#model HrAndPayrollSystem.Models.EmployeeMasterA
#Html.DropDownList("DeptId", null, "Department")
#foreach (var i in ViewData["fs_lbls"] as string[])
{
#i
}
But it throws an exception InvalidOperationException at line #Html.DropDownList("DeptId", null, "Department") by saying :
There is no ViewData item of type 'IEnumerable' that has the key 'DeptId'.
It throws the above exception whenever I try to pass the array data to the partial view using ViewDataDictionary, otherwise, it is working fine, when I'm not.
How do I get rid of the above exception and properly pass array data from main view to the partial view?
I suggest that you add a new property to EmployeeMasterA to store the labels, so that you do not need to use ViewData at all.
public class EmployeeMasterA
{
public string[] fs_lbls { get; set; }
public string SelectedLabel { get; set; }
public List<SelectListItem> Labels
{
get
{
if (this.fs_lbls == null)
{
return Enumerable.Empty<SelectListItem>().ToList();
}
return (from label in fs_lbls
select new SelectListItem
{
Text = label,
Value = label
}).ToList();
}
}
}
Create.cshtml
#model WebApplication1.Controllers.EmployeeMasterA
#using (Html.BeginForm())
{
#Html.Partial("FinalSettlementTAB", Model)
<input type="submit" value="Save"/>
}
FinalSettlementTAB.cshtml
#model WebApplication1.Controllers.EmployeeMasterA
#Html.DropDownList("SelectedLabel", Model.Labels)
Controller
public ActionResult Create()
{
var viewModel = new EmployeeMasterA();
viewModel.fs_lbls = new[] {"Label1", "label 2"};
return View(viewModel);
}
[HttpPost]
public ActionResult Create(EmployeeMasterA viewModel)
{
return View();
}
You can set the content of fs_lbls in the controller action method, before returning the Create view. When you post the form, the SelectedLabel property will contain the selected item from the dropdown list. Obviously you will need to change the property names to suite your needs, but hopefully this will give you an idea.
I have the following view model
public class ProjectVM
{
....
[Display(Name = "Category")]
[Required(ErrorMessage = "Please select a category")]
public int CategoryID { get; set; }
public IEnumerable<SelectListItem> CategoryList { get; set; }
....
}
and the following controller method to create a new Project and assign a Category
public ActionResult Create()
{
ProjectVM model = new ProjectVM
{
CategoryList = new SelectList(db.Categories, "ID", "Name")
}
return View(model);
}
public ActionResult Create(ProjectVM model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Save and redirect
}
and in the view
#model ProjectVM
....
#using (Html.BeginForm())
{
....
#Html.LabelFor(m => m.CategoryID)
#Html.DropDownListFor(m => m.CategoryID, Model.CategoryList, "-Please select-")
#Html.ValidationMessageFor(m => m.CategoryID)
....
<input type="submit" value="Create" />
}
The view displays correctly but when submitting the form, I get the following error message
InvalidOperationException: The ViewData item that has the key 'CategoryID' is of type 'System.Int32' but must be of type 'IEnumerable<SelectListItem>'.
The same error occurs using the #Html.DropDownList() method, and if I pass the SelectList using a ViewBag or ViewData.
The error means that the value of CategoryList is null (and as a result the DropDownListFor() method expects that the first parameter is of type IEnumerable<SelectListItem>).
You are not generating an input for each property of each SelectListItem in CategoryList (and nor should you) so no values for the SelectList are posted to the controller method, and therefore the value of model.CategoryList in the POST method is null. If you return the view, you must first reassign the value of CategoryList, just as you did in the GET method.
public ActionResult Create(ProjectVM model)
{
if (!ModelState.IsValid)
{
model.CategoryList = new SelectList(db.Categories, "ID", "Name"); // add this
return View(model);
}
// Save and redirect
}
To explain the inner workings (the source code can be seen here)
Each overload of DropDownList() and DropDownListFor() eventually calls the following method
private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata,
string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple,
IDictionary<string, object> htmlAttributes)
which checks if the selectList (the second parameter of #Html.DropDownListFor()) is null
// If we got a null selectList, try to use ViewData to get the list of items.
if (selectList == null)
{
selectList = htmlHelper.GetSelectData(name);
usedViewData = true;
}
which in turn calls
private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
which evaluates the the first parameter of #Html.DropDownListFor() (in this case CategoryID)
....
o = htmlHelper.ViewData.Eval(name);
....
IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>;
if (selectList == null)
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
MvcResources.HtmlHelper_WrongSelectDataType,
name, o.GetType().FullName, "IEnumerable<SelectListItem>"));
}
Because property CategoryID is typeof int, it cannot be cast to IEnumerable<SelectListItem> and the exception is thrown (which is defined in the MvcResources.resx file as)
<data name="HtmlHelper_WrongSelectDataType" xml:space="preserve">
<value>The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'.</value>
</data>
according to stephens (user3559349) answer, this can be useful:
#Html.DropDownListFor(m => m.CategoryID, Model.CategoryList ?? new List<SelectListItem>(), "-Please select-")
or in ProjectVM:
public class ProjectVM
{
public ProjectVM()
{
CategoryList = new List<SelectListItem>();
}
...
}
Most Likely Caused some sort of error redirecting to your page and you not initializing your model's drop down lists again.
Make sure that you initialize your drop downs in either the model's constructor or every time before you send said model to the page.
Otherwise you will need to maintain the state of the drop down lists either through the view bag or through the hidden value helpers.
OK, the poster's canned answer neatly explained why the error occurred, but not how to get it to work. I'm not sure that's really an answer, but it did point me in the right direction.
I ran into the same issue and found a slick way to resolve it. I'll try to capture that here. Disclaimer - I work on web pages once a year or so and really don't know what I'm doing most of the time. This answer should in no way be considered an "expert" answer, but it does the job with little work...
Given that I have some data object (most likely a Data Transfer Object) that I want to use a drop-down list to supply valid values for a field, like so:
public class MyDataObject
{
public int id;
public string StrValue;
}
Then the ViewModel looks like this:
public class MyDataObjectVM
{
public int id;
public string StrValue;
public List<SectListItem> strValues;
}
The real problem here, as #Stephen so eloquently described above, is the select list isn't populated on the POST method in the controller. So your controller methods would look like this:
// GET
public ActionResult Create()
{
var dataObjectVM = GetNewMyDataObjectVM();
return View(dataObjectVM); // I use T4MVC, don't you?
}
private MyDataObjectVM GetNewMyDataObjectVM(MyDataObjectVM model = null)
{
return new MyDataObjectVM
{
int id = model?.Id ?? 0,
string StrValue = model?.StrValue ?? "",
var strValues = new List<SelectListItem>
{
new SelectListItem {Text = "Select", Value = ""},
new SelectListITem {Text = "Item1", Value = "Item1"},
new SelectListItem {Text = "Item2", Value = "Item2"}
};
};
}
// POST
public ActionResult Create(FormCollection formValues)
{
var dataObject = new MyDataObject();
try
{
UpdateModel(dataObject, formValues);
AddObjectToObjectStore(dataObject);
return RedirectToAction(Actions.Index);
}
catch (Exception ex)
{
// fill in the drop-down list for the view model
var dataObjectVM = GetNewMyDataObjectVM();
ModelState.AddModelError("", ex.Message);
return View(dataObjectVM);
)
}
There you have it. This is NOT working code, I copy/pasted and edited to make it simple, but you get the idea. If the data members in both the original data model and the derived view model have the same name, UpdateModel() does an awesome job of filling in just the right data for you from the FormCollection values.
I'm posting this here so I can find the answer when I inevitably run into this issue again -- hopefully it will help someone else out as well.
I had the same problem, I was getting an invalid ModelState when I tried to post the form. For me, this was caused by setting CategoryId to int, when I changed it to string the ModelState was valid and the Create method worked as expected.
In my case the first ID in my list was zero, once I changed the ID to start from 1, it worked.
I am dynamically generating a dropdownbox.
I am trying to send the selected value in dropdown box as one of the fields of a model to controller.
#using (Html.BeginForm("AddItem", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<label>
Category:</label>
#Html.DropDownList("Category", (IEnumerable<SelectListItem>)ViewData["CategoryList"])<br />
<label>
Sku:</label>
#Html.TextBox("newItem.Sku")<br />
<label>
Title:</label>
#Html.TextBox("newItem.Title")<br />
I am able to send all the values as a part of model, except the value of Category(dropdownbox value), hence making the function in controller to fail..
ANSWER: Renaming the Dropdownlist "Category" to = "newItem.Category", did the work, basically it should match the model name.
Create a ViewModel for your Item with Properties to hold all Categories and SelectedCategoryId value
public class ItemViewModel
{
public int ItemId { set;get;}
public string SKU { set;get;}
public string SelectedCategoryId { get; set; }
public IEnumerable<SelectListItem> Categories{ get; set; }
}
In your home controller, Create Action method for Add where you create an object of ItemViewModel, Set the Categories and return to the View which is strongly typed.
public ActionResult AddItem()
{
ItemViewModel objNewItem=new ItemViewModel();
objNewItem.Items = new[]
{
new SelectListItem { Value = "1", Text = "Perfume" },
new SelectListItem { Value = "3", Text = "Shoe" },
new SelectListItem { Value = "3", Text = "Shirt" }
};
return View(objNewItem);
}
The Strongly typed View
#model ItemViewModel
#using (Html.BeginForm("AddItem", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
Category:
#Html.DropDownListFor(x => x.SelectedCategoryId ,new SelectList(Model.Categories,"Value",Text"), "Select..")
<input type="submit" value="Add" />
}
And have the HTTPPOST AddItem method in your Home Controller
[HttpPost]
public ActionResult AddItem(ItemViewModel objItem)
{
//Now you can access objItem.SelectedCategoryId and do what you like to do...
}
Your DropDown is bound to a field called Category. So you must have such field on your view model.
public class MyViewModel
{
public string Category { get; set; }
...
}
and your action:
[HttpPost]
public ActionResult AddItem(MyViewModel model)
{
// model.Category will contain the selected value
...
}
Also note that this property must be a simple scalar value (string, int, ...) and not a complex object.
And once you have an adapted view model you could use the strongly typed versions of the helpers:
#model MyViewModel
#using (Html.BeginForm("AddItem", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.LabelFor(x => x.Category)
#Html.DropDownListFor(x => x.Category, Model.Categories)
<br />
#Html.EditorFor(x => x.Sku)
<br/>
#Html.EditorFor(x => x.Title)
...
}
Renaming the Dropdownlist "Category" to = "newItem.Category", did the work, basically if you expect a model to be received in controller, which in this case was "newItem", down control name should match the model name and its property.