MVC Routing with Period in parameter name - c#

I am trying to implement a remote validation using entity framework in an MVC application. I need help trying to define the action signature and the appropriate route config. I have the following class in my model:
public class FiscalReports
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long id { get; set; }
public Int64 Counter { get; set; }
public short FiscalYear { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:MMM dd, yyyy}")]
[DisplayFormat(DataFormatString = "${0:N0}")]
[Remote("ValidateSalary", "FiscalReports", AdditionalFields ="Counter, FiscalYear")]
public int? Salaries { get; set; }
}
I have a viewmodel which is used for a view that contains several of the above objects.
public class FiscalReportVM
{
public FiscalReports CurrentFR { get; set; }
public FiscalReports ReportedToDate { get; set; }
public FiscalReports BudgetToDate { get; set; }
}
The Validation action is in the FiscalReports controller is as follows:
public JsonResult ValidateSalary(int Salaries, short FiscalYear, int Counter)
{
return ValidateFiscalField(Salaries, FiscalYear, Counter, "Salaries");
}
In the view I am using the HTML helper
#Html.EditorFor(model=>model.CurrentFR.Salaries)
This generates the field and validation correctly. Generated HTML is below
input data-val="true" data-val-number="The field Salaries must be a number." data-val-remote="'Salaries'; is invalid." data-val-remote-additionalfields="*.Salaries,*.Counter,*.FiscalYear" data-val-remote-url="/FiscalReports/ValidateSalary" name="CurrentFR.Salaries" type="number" value="" />
The validation request is firing properly and in fiddler I see the following request:
http://localhost:50409/FiscalReports/ValidateSalary?CurrentFR.Salaries=27000&CurrentFR.Counter=4773&CurrentFR.FiscalYear=2
My problem is that I have trouble defining a route and action with the variables in dotted notation. The action definition does not accept dotted parameters (Can't do ValidateSalary(int CurrentFR.Salaries,....). I need help trying to define the action signature and the appropriate route config.

Can't you just use a bit of JQuery to change the name attribute? Something to the effect of:
$("CurrentFR.Salaries").attr('name', 'Salaries')
Remember having a not dissimilar issue and I just temporarily changed the name in the view, and then changed in back in the action.

Related

Why select asp-for has no value in post handler

I have this form on my page. It's for adding some project to DB. Project requires director for itself, so i pass SelectList of workers to this form.
<form asp-controller="Project" asp-action="Edit" method="post">
...some other fields...
#if (ViewBag.workers != null)
{
<div>
<label asp-for="director">Director</label>
<select asp-for="director" asp-items=#ViewBag.workers>
</select>
<span asp-validation-for="director"></span>
</div>
}
...
</form>
Select tag working, there are my workers. But when i'm trying to submit, i have error "The director field is required."
I've checked form with js and it has data from select option, but my post handler don't.
There is code of hadler
[HttpPost]
public IActionResult Edit(Project model)
{
// here model hasn't director field
if (ModelState.IsValid)
{
dataManager.project.SaveProject(model);
return RedirectToAction(nameof(HomeController.Index), nameof(HomeController).Replace("Controller", ""));
}
ViewBag.workers = new SelectList(dataManager.worker.GetAllWorkers(), nameof(Worker.id), nameof(Worker.name));
return View(model);
}
And Project class code
public class Project
{
[Required] public Guid id { set; get; }
[Required] public string name { set; get; }
public string customer { set; get; }
public string executor { set; get; }
public Worker director { set; get; }
public DateTime start { set; get; }
public DateTime end { set; get; }
public uint priority { set; get; }
}
Post the solution mentioned in the comment as the answer post as the reference for the future reader.
Issue & Concern
From this line:
ViewBag.workers = new SelectList(dataManager.worker.GetAllWorkers(), nameof(Worker.id), nameof(Worker.name));
You are setting the value of the drop-down list option as id. Unsure what is the id type, (possibly it is an int, Guid type) but I am sure that it is not a Worker (object) type.
While in the Project model, you specify director as Worker type.
public class Project
{
...
public Worker director { set; get; }
}
Since an int/Guid type is unmatched with the Worker type, the API action is unable to bind the value that you passed from the View to the model. Thus, you will get Project as null in the API action.
Solution
Would suggest that modify the property type in Project model as int/Guid (depending on your Worker ID type in the database) rather than using the Worker (object) type. And remove the Worker property as it is no longer needed and avoid the error in ModelState due to the value is not provided.
Note: If this Project model is the entity model that was generated (scaffolded) by Entity Framework, would suggest creating another class that acts as the DTO (Data Transfer Object). Then map the received DTO to entity model.
public class Project
{
...
public Worker directorId { set; get; }
}
In the View, bind the drop-down list with asp-for="workerId" to pass the selected value as workerId.
<select asp-for="directorId" asp-items=#ViewBag.workers></select>
Back-end side
3.1. Key point: Query the Worker object with the received project.workerId. Then bind the Worker object to the entity model before inserting.
3.2. (If implementing the DTO as mentioned in 1) You need to implement the logic (or look for mapping library such as AutoMapper) to map from DTO to the entity model.

Inconsistent requirement of Model prefix for asp.net core tag helpers

I noticed a strange behavior in which there is inconsistent requirement of Model prefix for asp.net core tag helpers below. asp-for cannot accept Model prefix but asp-items must have Model prefix. My head explodes.
#model ProblemVM
<select
asp-for="Problem.TagId"
asp-items="Model.Tags.ToSelectListItem(Model.Problem.TagId)"
/>
public class ProblemVM
{
public IEnumerable<Tag> Tags { get; set; }
public Problem Problem{ get; set; }
}
Related classes.
public abstract class ISelectListItemable
{
[Key]
public int Id { get; set; }
[Required]
public virtual string Name { get; set; }
}
public class Tag: ISelectListItemable
{
[Display(Name = "Tag Name")]
public override string Name { get; set; }
}
public class Problem : ISelectListItemable
{
[Display(Name = "Problem Name")]
public override string Name { get; set; }
public int TagId { get; set; }
[ForeignKey(nameof(TagId))]
public virtual Tag Tag { get; set; }
}
public static IEnumerable<SelectListItem> ToSelectListItem<T>(this IEnumerable<T> items, int selectedValue)
where T : ISelectListItemable
{
return from item in items
select new SelectListItem
{
Text = item.Name,
Value = item.Id.ToString(),
Selected = item.Id.Equals(selectedValue)
};
}
Question
What is the rule of using Model prefix for tag helpers?
It's about the actual type of the property the attribute of the tag helper corresponds to. The asp-for attribute maps to a For property on the built-in tag helpers which is typed as ModelExpression. So, it literally is looking for an expression relative to the model of the view.
The asp-items attribute, on the other hand is typed as IEnumerable<SelectListItem>, and thus literally needs a concrete value, not just an expression. The fact that it just so happens to be coming from a prop on your model is inconsequential. The value could come from ViewData, or be satisfied directly inline.
There are certain situations, though, when you still need to include Model for an attribute like asp-for. This is generally when your model itself is a list, dictionary, etc. and you need to index. You can't just add something like [i].Foo as an expression, so in that case you would do #Model[i].Foo.
The documentation does say that:
The asp-for attribute value is a special case and doesn't require a Model prefix, the other Tag Helper attributes do (such as asp-items)
The reason I see is that asp-for will always be from your model. But asp-items can be any collection. It doesn't have to be from your model. So if you do want it to be from your model, you need to tell it that.

How to validate textboxes in ASP.NET MVC

I am new to ASP.NET MVC and am trying to validate a text-box. Basically, if user inputs less than 2 or a non number how can I get the error to display. Here's the tutorial I am trying to follow.
I have my code below.
Create View:
<%= Html.ValidationSummary()%>
<%= using (HtmlBeginForm()){%>
<div class="half-col">
<label for="Amount">Amount:</label>
<%= Html.TextBox("Amount")%>
<%= Html.ValidationMessage("Amount", "*")%>
</div>
Create Controller:
[AcceptVerbs (HttpVerbs.Post)]
public ActionResult Create([Bind(Exclude ="ID")] Charity productToCreate)
{
//Validation
if (productToCreate.Amount < 2)
ModelState.AddModelError("Amount, Greater than 2 please");
return View(db.Donations.OrderByDescending(x => x.ID).Take(5).ToList()); //Display 5 recent records from table
}
Model:
public class Charity
{
public int ID { get; set; }
public string DisplayName { get; set; }
public DateTime Date { get; set; }
public Double Amount { get; set; }
public Double TaxBonus { get; set; }
public String Comment { get; set; }
}
Error:
CS1501 No overload for method 'AddModelError' takes 1 CharitySite
You are adding the error to your modelstate incorrectly. You can read more about the ModelStateDictionary on the MSDN
AddModelError takes 2 parameters, so you would want:
ModelState.AddModelError("Amount", "Greater Than 2 Please.");
Having said that, you can use attributes to validate your model properties so you don't have to write all of that code by hand. Below is an example using the Range attribute. The RegularExpression attribute could also work. Here is an MSDN article containing information about the different types of attributes.
public class Charity
{
public int ID { get; set; }
public string DisplayName { get; set; }
public DateTime Date { get; set; }
[Range(2, Int32.MaxValue, ErrorMessage = "The value must be greater than 2")]
public Double Amount { get; set; }
public Double TaxBonus { get; set; }
public String Comment { get; set; }
}
Also as a side note, the tutorial you are following is for MVC 1&2. Unless you HAVE to use / learn that. I would recommend following the tutorial for MVC 5 here.
Change this line:
ModelState.AddModelError("Amount, Greater than 2 please");
to:
ModelState.AddModelError("Amount ", "Amount, Greater than 2 please");
The first parameter is the member of the model being validated; it can be an empty string just to indicate an error not associated to a field. By specifying the Amount field, internally it uses that to highlight the erroring field (the control should have input-validation-error CSS class added to it) if you are using all of the client-side validation pieces.
ModelState.AddModelError takes 2 arguments, not 1. Link to MSDN ModelStateDictionary.AddModelError Method.
ModelState.AddModelError("Amount", "Greater than 2 please");
if (productToCreate.Amount < 2)
ModelState.AddModelError("Amount", "Greater than 2 please");

Set required fields based on HTTP Verb

Is it possible to set an optional [Required] attribute, applicable on PATCH or PUT. I have the following code but no matter what the controller call it will always be required.
public class Car
{
[DataMember(Order = 0)]
public string CarId { get; set; }
[DataMember(Order = 1)]
[Required]
public string IsIncluded { get; set; }
}
Controller;
[HttpPatch]
public HttpResponseMessage PatchCar(Car car)
{
// check if submitted body is valid
if (!ModelState.IsValid)
{
// Something is bad!
}
}
What I want is something like the following;
public class Car
{
[DataMember(Order = 0)]
public string CarId { get; set; }
[DataMember(Order = 1)]
[Required(Patch = True, Put = False]
public string IsIncluded { get; set; }
}
Then my ModelState will take the very into account.
I thought about creating separate derived classes for each action (verb), but the code quickly becomes incredibly verbose.
This is one of the drawbacks of using data annotations for validation unfortunately they cannot be conditionally added.
There are a number of options to you...
Create separate models (or view models) for each verb.
Look into something like this.. http://andrewtwest.com/2011/01/10/conditional-validation-with-data-annotations-in-asp-net-mvc/ which extends required to be IfRequired and adds conditional validation to data annotations. (You would need to roll your own I should think and it may get clumsy!)
Try something like FluentValidation.
http://fluentvalidation.codeplex.com/ (this could be a good option depending on your application requirements).
Hope this helps!

ASP.NET MVC Exclude Data Annotation Attribute in Html.ValidationMessageFor

I use the EF-CF, and have the following entities:
Countries, Companies
Companies has a property called CountryId with StringLength attribute and the min and max restrictions (min 3 chars, max 3 chars, country id is ISO-Alpha-3). When the user needs to create a Company, I show a html element with all available countries. This is perfect!
However, when the I execute the jquery validator to the form, this checks for 3 selected options and not the length selected option value.
I need the StringLengthAttribute in my Country Model, I cannot remove it.
I hope to "remove" or "hide" the StringLengthAttribute in the call:
#Html.ValidationMessageFor(model => model.CountryId)
Thanks!
I think I understand your question. A possible solution would be to use a ViewModel to pass to the view as oppose to using the Company entity directly. This would allow you to add or remove data annotations without changing the entity model. Then map the data from the new CompanyViewModel over to the Company entity model to be saved to the database.
For example, the Company entity might look something like this:
public class Company
{
public int Id { get; set; }
[StringLength(25)]
public string Name { get; set; }
public int EmployeeAmount { get; set; }
[StringLength(3, MinimumLength = 3)]
public string CountryId {get; set; }
}
Now in the MVC project a ViewModel can be constructed similar to the Company entity:
public class CompanyViewModel
{
public int Id { get; set; }
[StringLength(25, ErrorMessage="Company name needs to be 25 characters or less!")]
public string Name { get; set; }
public int EmployeeAmount { get; set; }
public string CountryId { get; set; }
}
Using a ViewModel means more view presentation orientated annotations can be added without overloading entities with unnecessary mark-up.
I hope this helps!
Ready!
I remove the rule for the html control.
$("##(Html.HtmlIdNameFor(model => model.CountryId))").rules("remove", "rangelength");
The "rangelength" is the jquery validation rule for the StringLengthAttribute.
Where "Html.HtmlIdNameFor" is a helper to get the "Id" generated by ASP.NET MVC.
Review How to get the HTML id generated by asp.net MVC EditorFor

Categories