Custom validation for hidden/non-bound field in Blazor - c#

I'm working with Blazor for the first time (also the first time I've worked with .NET in a few years so I'm rusty), and an additional component library that my team and I have decided to use with Blazor is MudBlazor. Right now I'm working on a page that has an Autocomplete component.
What I'm trying to do is to use an Autocomplete feature to pull up a list of Books that a user can add to a list. At least one Book must be in the list. My problem is that because of how MudAutocomplete works, I'm unable to bind it to a list, so I have the Book appended to a list on a click event. However, hitting the submit button isn't hitting either of the validations I've implemented and I can't seem to figure out why.
I've got the following code:
<MudForm #ref="form" #bind-isValid="#success" bind-errors="#errors">
<MudAutocomplete T="Book" Label="Select Book(s)" ValueChanged="#(b => AppendBookToList(b))" SearchFunc="#SearchBooks" MinCharacters="4" ToStringFunc="#(b => b == null ? null : $"{b.Name + " by " + b.Author}")" Validation="#(new Func<string, IEnumerable<string>>(ValidateRequiredBooks)">
</MudAutocomplete>
<!-- List books that were selected from the autocomplete -->
#foreach (var b in Books)
{
<MudChip Color="Color.Primary" OnClose="RemoveRequester" Text="b.Id">#b.Name by #b.Author</MudChip>
}
<!-- I guess use this area below to secretly bind the Books field? Not sure how to display the validation error otherwise -->
<MudField #bind-Value="#Books"></MudField>
...
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto" OnClick="#(() => form.Validate())">Submit</MudButton>
</MudForm>
...
#code {
...
public class Book
{
public int Id { get; set; }
public string Name { get; set; }
public string Author { get; set; }
}
[BooksValidation(ErrorMessage = "At least one Book required.")]
public List<Book> Books { get; set; }
...
// This is not firing
private IEnumerable<string> ValidateRequiredBooks(string value)
{
if (Books.Count == 0)
{
yield return "At least one Book must be selected.";
yield break;
}
}
}
I also created the following custom validation attribute for my Books variable:
public class BooksValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
// Breakpoint here isn't getting hit
var listValue = value as List<Book>;
if (listValue != null && listValue.Count != 0)
{
return null;
}
else
{
return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName });
}
}
}
Any ideas on why these aren't working at all? It's driving me up a wall

I use Validation attribute like:
Validation="#(new BooksValidationAttribute())"

Related

Correct way of implementing a Model for a Dropdownlist .Net Core

I'm currently trying to implement a Dropdown List with what I would assume is hardcoded values. Basically what I want is to have a Dropdown List with a choice of three possible values.
I have a total of 4 controllers and views that will be using this dropdown. I've seen a lot of examples for MVC2 and MVC3 where people have hard coded their dropdowns in their views, and I personally don't prefer to go with a quick and "dirty" fix such as that.
I have a model containing the following.
public class Status
{
public int id { get; set; }
public string status { get; set; }
public Status(int statusId, string statusName)
{
id = statusId;
status = statusName;
}
}
The status should be able to have any of the 3 possible values:
Active
Inactive
Processing
I thought of creating the status' using this method I currently have in my status class:
public static List<Status> getAllStatus()
{
List<Status> states = new List<Status>();
states.Add(new Status(1, "Active"));
states.Add(new Status(2, "Inactive"));
states.Add(new Status(3, "Processing"));
return states;
}
I haven't been able to figure out how to use this model inside my Controllers alongside with how to pass it along to my views and was hoping someone in here would known how to do that?
EDIT1:
I guess I forgot to mention that I will be storing the selected value as a string in my database and that I am using a view which doesn't have the model of my status class, but rather the model of object which I will be storing in my database (which might be the case of a vehicle object).
EDIT2:
I have a model called Customer, which has some of the following values:
public int CustomerID { get; set }
public string Email { get; set }
public string Phone { get; set }
public Status Status { get; set; }
In my DB for my Customer model I have a string in which I wan to store the selected Status.
So basically I wan't to change the following to a dropdown with 3 options, Inactive, Active and Processing.
However I don't want to code this in my view as I will be needing it in 8 different views and copy pasting that is not very sleek code.
#Html.DropDownListFor(m => m.status_id, new SelectList(getAllStatus(), "id", "status"))
It doesn't make much sense to save it as a string within your database as it sounds more like something static. So u should consider an Enum. To me more precise look to my previous answer and add those Model properties to a ViewModel.
public class CustomerViewModel () {
public int SelectedStatusId { get; set; }
[DisplayName("Status")]
public IEnumerable<SelectListItem> StatusItems
{
get
{
yield return new SelectListItem { Value = "", Text = "- Select a status -" };
StatusTypeEnum[] values = (StatusTypeEnum[])Enum.GetValues(typeof(StatusTypeEnum));
foreach (StatusTypeEnum item in values)
{
if (item != StatusTypeEnum.Unknown)
{
yield return new SelectListItem { Value = ((int)item).ToString(), Text = item.GetDescription() };
}
}
}
}
}
Pass this into your View through your controller:
public class CustomerCOntroller(){
public ActionResult Index(){
CustomerViewModel viewModel = new CustomerViewModel();
return View(viewModel);
}
}
And you are done. If u are more working with a list which u need to build up add it to your viewModel object.
Greetings,
S..
To start you have a lot of vague questions. Be more specific if you can. If you don't know how MVC works that well I would recomment to follow some tutorials on it.
Model.cs (A ViewModel is preferred). You Should create a ViewModel which you passes to the View. Below is an example how to get a list of items.
public int SelectedStatusId { get; set; }
[DisplayName("Status")]
public IEnumerable<SelectListItem> StatusItems
{
get
{
yield return new SelectListItem { Value = "", Text = "- Select a status -" };
StatusTypeEnum[] values = (StatusTypeEnum[])Enum.GetValues(typeof(StatusTypeEnum));
foreach (StatusTypeEnum item in values)
{
if (item != StatusTypeEnum.Unknown)
{
yield return new SelectListItem { Value = ((int)item).ToString(), Text = item.GetDescription() };
}
}
}
}
StatusTypeEnum.cs
public enum StatusTypeEnum()
{
[Description("Active")] // For correct naming
Active,
Inactive,
Processing
}
View.cshtml
#Html.DropDownListFor(x => Model.SelectedStatusId, Model.StatusItems)
EnumAttribute.cs (To read the Annotation Descriptions. And don't try to understand this. It's just magic. It gets the DataAnnotation of the enum types by reflection.)
public static class EnumAttribute
{
public static string GetDescription<TEnum>(this TEnum value)
{
var fi = value.GetType().GetField(value.ToString());
if (fi != null)
{
var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attributes.Length > 0)
{
return attributes[0].Description;
}
}
return value.ToString();
}
}
You could declare States as public propery in your class and use this to access it in your views:
<ComboBox Grid.Column="1" Grid.Row="0" Margin="5"
VerticalAlignment="Center" ItemsSource="{Binding States}"
IsTabStop="False"}"/>

Collections Editor is not persisting entries in custom web control markup in design view in VS 2013?

I am trying to develop a simple custom web control for ASP.Net WebForms that has a collections property called Subscriptions.
I can compile the control project successfully and add it from toolbox to an aspx page without any issues.
The problem is when I add entries for Subscriptions property using the collections editor in design view in Visual Studio 2013.
I can input multiple Subscriptions but when I click on OK button of the collections editor and then I go back to Subscriptions property in design view it's empty even though I had input some entries a moment ago.
Markup of custom control in aspx
<cc1:WebControl1 ID="WebControl1" runat="server"></cc1:WebControl1>
Question : What is not correct with my code that is causing the collections to not show up in control's markup in design view?
Custom web control code
namespace WebControl1
{
[ToolboxData("<{0}:WebControl1 runat=\"server\"> </{0}:WebControl1>")]
[ParseChildren(true)]
[PersistChildren(false)]
public class WebControl1 : WebControl
{
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public string Text
{
get
{
String s = (String)ViewState["Text"];
return ((s == null) ? "[" + this.ID + "]" : s);
}
set
{
ViewState["Text"] = value;
}
}
[
Category("Behavior"),
Description("The subscriptions collection"),
DesignerSerializationVisibility(DesignerSerializationVisibility.Visible),
Editor(typeof(SubscriptionCollectionEditor), typeof(UITypeEditor)),
PersistenceMode(PersistenceMode.InnerDefaultProperty)
]
public List<Subscription> Subscriptions { get; set; }
protected override void RenderContents(HtmlTextWriter output)
{
output.Write(Text);
}
}
}
[TypeConverter(typeof(ExpandableObjectConverter))]
public class Subscription
{
private string name;
private decimal amount;
public Subscription()
: this(String.Empty, 0.00m)
{
}
public Subscription(string nm, decimal amt)
{
name = nm;
amount = amt;
}
[
Category("Behavior"),
DefaultValue(""),
Description("Name of subscription"),
NotifyParentProperty(true),
]
public String Name
{
get
{
return name;
}
set
{
name = value;
}
}
[
Category("Behavior"),
DefaultValue("0.00"),
Description("Amount for subscription"),
NotifyParentProperty(true)
]
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
}
public class SubscriptionCollectionEditor : System.ComponentModel.Design.CollectionEditor
{
public SubscriptionCollectionEditor(Type type)
: base(type)
{
}
protected override bool CanSelectMultipleInstances()
{
return false;
}
protected override Type CreateCollectionItemType()
{
return typeof(Subscription);
}
}
I was able to solve the problem by making following 2 changes.
Since for collections like List the .Net framework will automatically display an appropriate editor so we don't need to specify the editor since the collection is of List type. So we don't need this attribute Editor(typeof(SubscriptionCollectionEditor), typeof(UITypeEditor)).
The setter for Subscriptions needs to be removed and only a get should be there as in code below. If a setter is used then it should be used as in second code snippet. But automatic get and set should not be used with collection property in a custom web control.
The final code for the collections property should look like below and then collections will not disappear when one returns to it later on in design-time VS 2013.
Code that works without a setter
private List<Subscription> list = null;
[Category("Behavior"),
Description("The subscriptions collection"),
DesignerSerializationVisibility(DesignerSerializationVisibility.Visible),
PersistenceMode(PersistenceMode.InnerDefaultProperty)
]
public List<Subscription> SubscriptionList
{
get
{
if (lists == null)
{
lists = new List<Subscription>();
}
return lists;
}
}
Code that works with a setter
[Category("Behavior"),
Description("The subscriptions collection"),
DesignerSerializationVisibility(DesignerSerializationVisibility.Visible),
PersistenceMode(PersistenceMode.InnerDefaultProperty)
]
public List<Subscription> SubscriptionList
{
get
{
object s = ViewState["SubscriptionList"];
if ( s == null)
{
ViewState["SubscriptionList"] = new List<Subscription>();
}
return (List<Subscription>) ViewState["SubscriptionList"];
}
set
{
ViewState["SubscriptionList"] = value;
}
}

How to validate number of items in a list in mvc model

I am writing an online assessment form. On this form the user has to select a minimum of 3 and a maximum of 7 people who will provide an assessment on them. I have a form where the user adds the assessors and I display the list underneath this form. Once the user has finished adding assessors then click on the self assessment button to fill his/hers own self assessment.
What I want to do is to validate that indeed the number of assessors is in the right range before the user leaves the page.
The model is like this
public class AssessorsViewModel
{
List<Assessor> Assessors { get; set; }
}
public class Assessor
{
string Email { get; set; }
string Name { get; set; }
}
I have validation attributes for the Assessor class so everytime the user adds an assessor I can validate this, but I can't figure out how to validate the Count on the Assessors List.
I am using ASP.net MVC.
Thanks in advance
A custom ValidationAttribute would do it for you:
public class LimitCountAttribute : ValidationAttribute
{
private readonly int _min;
private readonly int _max;
public LimitCountAttribute(int min, int max) {
_min = min;
_max = max;
}
public override bool IsValid(object value) {
var list = value as IList;
if (list == null)
return false;
if (list.Count < _min || list.Count > _max)
return false;
return true;
}
}
Usage:
public class AssessorsViewModel
{
[LimitCount(3, 7, ErrorMessage = "whatever"]
List<Assessor> Assessors { get; set; }
}
You can simply validate this in the controller:
public ActionResult TheAction(AssessorsViewModel model)
{
if (model.Assessors == null
|| model.Assessors.Count < 3
|| model.Assessors.Count > 7)
{
ModelState.AddModelError("Assessors", "Please enter note less than 3 and not more than 7 assessors.");
return View(model);
}
...
}
Another option would be to write a custom validation attribute. Here is an example of how to do this (the validator there is different, but the approach is clear).
You could always add a custom validation attribute that would get fired when the model is validated.
Check the answer here on another question:
ASP.NET MVC: Custom Validation by DataAnnotation

How to filter a recursive object?

In my current project, a method I don't control sends me an object of this type:
public class SampleClass
{
public SampleClass();
public int ID { get; set; }
public List<SampleClass> Items { get; set; }
public string Name { get; set; }
public SampleType Type { get; set; }
}
public enum SampleType
{
type1,
type2,
type3
}
I display those data in a TreeView, but I would like to display only the path ending with SampleClass objects having their Type property set to type3, no matter the depth of this leaf.
I have absolutely no clue on how to do that, can someone help me ?
Thanks in advance !
Edit
To explain the problem I meet with the solutions proposed by Shahrooz Jefri and dasblinkenlight, here is a picture. The left column is the original data, without filtering, and the right one is the data filtered. Both methods provide the same result.
In red is the problem.
Use this Filter method:
public void Filter(List<SampleClass> items)
{
if (items != null)
{
List<SampleClass> itemsToRemove = new List<SampleClass>();
foreach (SampleClass item in items)
{
Filter(item.Items);
if (item.Items == null || item.Items.Count == 0)
if (item.Type != SampleType.type3)
itemsToRemove.Add(item);
}
foreach (SampleClass item in itemsToRemove)
{
items.Remove(item);
}
}
}
In addition to initially determining which items to show, if the datasize is substantial and you expect users to frequently collapse and expand sections then filtering after every click my result in slow ui response.
Consider the Decorator pattern or some other way of tagging each node with relevant info so that the filtering is not required after every click.
Try this approach:
static bool ShouldKeep(SampleClass item) {
return (item.Type == SampleType.type3 && item.Items.Count == 0)
|| item.Items.Any(ShouldKeep);
}
static SampleClass Filter(SampleClass item) {
if (!ShouldKeep(item)) return null;
return new SampleClass {
Id = item.Id
, Name = item.Name
, Type = item.Type
, Items = item.Items.Where(ShouldKeep).Select(x=>Filter(x)).ToList()
};
}
The above code assumes that Items of leaves are empty lists, rather than nulls.

Validation of objects in ViewModel not adding CSS validation classes

I have the following view models:
public class Search {
public int Id { get; set; }
[Required(ErrorMessage = "Please choose a name.")]
public string Name { get; set; }
[ValidGroup(ErrorMessage = "Please create a new group or choose an existing one.")]
public Group Group { get; set; }
}
public class Group {
public int Id { get; set; }
public string Name { get; set; }
}
I have defined a custom validation attribute as follows:
public class ValidGroupAttribute : ValidationAttribute {
public override bool IsValid(object value) {
if (value == null)
return false;
Group group = (Group)value;
return !(string.IsNullOrEmpty(group.Name) && group.Id == 0);
}
}
I have the following view (omitted some for brevity):
#Html.ValidationSummary()
<p>
<!-- These are custom HTML helper extensions. -->
#Html.RadioButtonForBool(m => m.NewGroup, true, "New", new { #class = "formRadioSearch", id = "NewGroup" })
#Html.RadioButtonForBool(m => m.NewGroup, false, "Existing", new { #class = "formRadioSearch", id = "ExistingGroup" })
</p>
<p>
<label>Group</label>
#if (Model.Group != null && Model.Group.Id == 0) {
#Html.TextBoxFor(m => m.Group.Name)
}
else {
#Html.DropDownListFor(m => m.Group.Id, Model.Groups)
}
</p>
The issue I'm having is the validation class input-validation-error does not get applied to the Group input. I assume this is because the framework is trying to find a field with id="Group" and the markup that is being generated has either id="Group_Id" or id=Group_Name. Is there a way I can get the class applied?
http://f.cl.ly/items/0Y3R0W3Z193s3d1h3518/Capture.PNG
Update
I've tried implementing IValidatableObject on the Group view model instead of using a validation attribute but I still can't get the CSS class to apply:
public class Group : IValidatableObject
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (string.IsNullOrEmpty(Name) && Id == 0) {
yield return new ValidationResult("Please create a new group or select an existing one.", new[] { "Group.Name" });
}
}
}
Update 2
Self validation doesn't work. I think this is because the second parameter in the ValidationResult constructor isn't used in the MVC framework.
From: http://www.devtrends.co.uk/blog/the-complete-guide-to-validation-in-asp.net-mvc-3-part-2
In some situations, you might be tempted to use the second constructor overload of ValidationResult that takes in an IEnumerable of member names. For example, you may decide that you want to display the error message on both fields being compared, so you change the code to this:
return new ValidationResult(
FormatErrorMessage(validationContext.DisplayName), new[] { validationContext.MemberName, OtherProperty });
If you run your code, you will find absolutely no difference. This is because although this overload is present and presumably used elsewhere in the .NET framework, the MVC framework completely ignores ValidationResult.MemberNames.
I've come up with a solution that works but is clearly a work around.
I've removed the validation attribute and created a custom model binder instead which manually adds an error to the ModelState dictionary for the property Group.Name.
public class SearchBinder : DefaultModelBinder {
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor) {
if (propertyDescriptor.Name == "Group" &&
bindingContext.ValueProvider.GetValue("Group.Name") != null &&
bindingContext.ValueProvider.GetValue("Group.Name").AttemptedValue == "") {
ModelState modelState = new ModelState { Value = bindingContext.ValueProvider.GetValue("Group.Name") };
modelState.Errors.Add("Please create a new group or choose an existing one.");
bindingContext.ModelState.Add("Group.Name", modelState);
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
// Register custom model binders in Application_Start()
ModelBinders.Binders.Add(typeof(SearchViewModel), new SearchBinder());
With ModelState["Group.Name"] now having an error entry, the CSS class is being rendered in the markup.
I would much prefer if there was a way to do this with idiomatic validation in MVC though.
Solved!
Found a proper way to do this. I was specifying the wrong property name in the self validating class, so the key that was being added to the ModelState dictionary was Group.Group.Name. All I had to do was change the returned ValidationResult.
yield return new ValidationResult("Please create a new group or select an existing one.", new[] { "Name" });

Categories

Resources