My Blazor application has two forms in different components. Both forms use he same view model. Though the model is the same, different fields are displayed in the components. E.g. the first component's form does not have the UnitPrice field, but the second does. I use a simple validation:
[Required(ErrorMessage = "Unit Price is required.")]
[Range(0.01, double.MaxValue, ErrorMessage = "Unit Price must be greater than 0")]
public double UnitPrice { get; set; }
Unfortunately, when the first form is displayed and submitted, the missing field is validated, and the validation fails. Is there any way to do it without splitting the model or using custom validation?
Example as requested:
public interface IForm
{
int FormStatus { get; set; }
// Your other fields that are always shared on this form...
}
public class Form1 : IForm
{
public int FormStatus { get; set; }
[Required(ErrorMessage = "Unit Price is required.")]
[Range(0.01, double.MaxValue, ErrorMessage = "Unit Price must be greater than 0")]
public decimal UnitPrice { get; set; }
}
public class Form2 : IForm
{
public int FormStatus { get; set; }
[Required]
public string Name { get; set; }
// I made this up but it demonstrates the idea of encapsulating what differs.
}
Your shared Blazor Component would be something like.
// SharedFormFields.razor
<input type="text" #bind-Value="_form.FormStatus">
#code {
[Parameter] private IForm Form { get; set; }
}
And then your consuming components/pages
#page "/Form1"
<EditContext Model=_form1>
<SharedFormFields Form=_form1>
<input type="number" #bind-Value="_form1.UnitPrice">
</EditContext
#code {
private Form1 _form1 = new()
}
I used conditional validation by deriving my view model from IValidatableObject and implementing it:
public class MyViewModel : IValidatableObject
{
...
public double UnitPrice { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var result = new List<ValidationResult>();
if (StatusId >= (int)Models.Status.ReadyForReview) // my condition
{
if (UnitPrice == 0)
{
result.Add(new ValidationResult("Unit Price is required."));
}
else if (UnitPrice < 0)
{
result.Add(new ValidationResult("Unit Price must be greater than 0."));
}
}
return result;
}
}
Related
I found this great post by Chris Sainty: Creating Bespoke Input Components for Blazor from Scratch. It is exactly what I need, but not with string, but with uploaded files IBrowserFile. So I have adapted and extended the example for me. The customized component displays the new files and saves it in my model, but in the CSS the status unfortunately stays on class="modified invalid".
I must be missing a small detail here. What is it? Thanks in advance for any hints.
Here is my code reduced to the essentials.
Selection.razor
#page "/selection"
#inherits ParentComponent<SelectionTestModel>
<PageComponent #ref="Page" Model="Model" StatusCode="StatusCode" PageType="PageType.Touch">
<PageBody>
<EditForm Model="Model" OnValidSubmit="Save">
<DataAnnotationsValidator />
<DocumentComponent #ref="DocumentUpload" #bind-Documents="Model.Files" />
</EditForm>
</PageBody>
</PageComponent>
#code {
private DocumentComponent DocumentUpload;
}
SelectionTestModel.cs
public class SelectionTestModel
{
public int? KeyID { get; set; }
/* ... */
[System.ComponentModel.DisplayName("Document")]
[System.ComponentModel.DataAnnotations.Display(Name = "Document")]
[System.ComponentModel.DataAnnotations.Range(2, 2, ErrorMessage = "You have to bring exactly two files!")]
public List<DocumentModel> Files { get; set; } = new List<DocumentModel>();
}
DocumentModel
public class DocumentModel
{
public int? Id { get; set; }
public string Reference { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public string ContentType { get; set; }
public string Content { get; set; } /*file as base64 string*/
}
DocumentComponent.razor
#using System.Linq.Expressions
<div class="dropzone rounded #_dropClass #_validClass">
<InputFile id="inputDrop" multiple
ondragover="event.preventDefault()"
ondragstart="event.dataTransfer.setData('', event.target.id)"
accept="#AllowedFileTypes"
OnChange="OnInputFileChange"
#ondragenter="HandleDragEnter"
#ondragleave="HandleDragLeave" />
#*...*#
</div>
#code {
[CascadingParameter] public EditContext EditContext { get; set; }
[Parameter] public List<DocumentModel> Documents { get; set; } = new List<DocumentModel>();
[Parameter] public EventCallback<List<DocumentModel>> DocumentsChanged { get; set; }
[Parameter] public Expression<Func<List<DocumentModel>>> DocumentsExpression { get; set; }
/*...*/
public List<string> AllowedFileTypes { get; set; } = new List<string> { ".pdf", /*...*/ };
private FieldIdentifier _fieldIdentifier;
private string _validClass => EditContext?.FieldCssClass(_fieldIdentifier) ?? null;
protected override void OnInitialized()
{
base.OnInitialized();
_fieldIdentifier = FieldIdentifier.Create(DocumentsExpression);
}
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
// validation: do we accept the file (content type, amount of files, size)
if (e.FileCount == 1) // keep it simple for this example
{
// read from IBrowserFile and return DocumentModel in memory only
Documents.Add(await SaveFile(e.File));
await DocumentsChanged.InvokeAsync(Documents);
EditContext?.NotifyFieldChanged(_fieldIdentifier);
}
}
/*...*/
}
How does it behave in the browser (Chrome)
After loading the page everything looks as expected.
After that I upload a single file. So I have one file and I expect two. The validation turns red and I get "modified invalid". So far everything is great.
Finally I drag another file into the component and get two files. I can also see this in the model. But unfortunately the class attribute "modified valid" is not set.
Thanks again for any advice
I dug too deep in the wrong direction and didn't see the obvious.
The problem is that there is an attribute set in the model that does not throw an error, but also cannot validate.
The Range attribute is not for lists and therefore the model could never validate. With an own attribute I could work around this.
SelectionTestModel.cs
[Library.Validation.Attribute.ListRange(2, 2)]
public List<DocumentModel> Files { get; set; } = new List<DocumentModel>();
ListRangeAttribute.cs
namespace Library.Validation.Attribute
{
public class ListRangeAttribute : ValidationAttribute
{
public int Minimum { get; set; }
public int Maximum { get; set; }
public ListRangeAttribute(int minimum = 0, int maximum = int.MaxValue)
{
Minimum = minimum > 0 ? minimum : 0;
Maximum = maximum;
}
public string GetErrorMessage(string displayName) { /* ... */ }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var list = value as IList;
if (list == null)
{
throw new InvalidOperationException($"Attribute {nameof(ListRangeAttribute)} must be on a property of type {nameof(IList)}.");
}
if ((list?.Count ?? 0) < Minimum || (list?.Count ?? int.MaxValue) > Maximum)
{
return new ValidationResult(GetErrorMessage(validationContext.DisplayName), new[] { validationContext.MemberName });
}
return ValidationResult.Success;
}
}
}
I hope this post can help others.
Remaining: Now I am left with a new mystery.
Why does the validation text disappear after a save button click, which could not be saved due to an invalid state of the model!?
Good Day guys!
I've just started using MVC3, I've 2 models in my application i.e. "Page" and "PageHistory" both has same properties. except that "PageHistory" has one extra property called "PageId" which references the "Page" model.
My question is am doing it in correct way? or should I use inheritance for this.
If inheritance is a option, How can I handle this, any examples will help me a lot.
My Model looks like as follows:
public class Page
{
private readonly IndianTime _g = new IndianTime();
public Page()
{
CreatedOn = _g.DateTime;
Properties = "Published";
Tags = "Page";
RelativeUrl = string.Empty;
}
public string Path
{
get { return (ParentPage != null) ? ParentPage.Heading + " >> " + Heading : Heading; }
}
[Key]
public int Id { get; set; }
[StringLength(200), Required, DataType(DataType.Text)]
public string Title { get; set; }
[StringLength(200), Required, DataType(DataType.Text)]
public string Heading { get; set; }
[MaxLength, Required, DataType(DataType.Html)]
public string Content { get; set; }
[Display(Name = "Reference Code"), ScaffoldColumn(false)]
public string ReferenceCode { get; set; }
[Required]
[Remote("CheckDuplicate", "Page", ErrorMessage = "Url has already taken", AdditionalFields = "initialUrl")]
public string Url { get; set; }
[Display(Name = "Created On"), ScaffoldColumn(false)]
public DateTime CreatedOn { get; set; }
//Parent Page Object (Self Reference: ParentId = > Id)
[Display(Name = "Parent Page")]
public int? ParentId { get; set; }
[DisplayFormat(NullDisplayText = "Root")]
public virtual Page ParentPage { get; set; }
public virtual ICollection<Page> ChildPages { get; set; }
}
I don't think inheritance will be the right way, the current structure looks ok to me.
See if you choose to make the Page as parent, and Pagehistory as being inherited from Page which actually it is not its just the pagehistory quite simply put.
Your idea of inheritance should always come from real world implementations, for ex. there could be diff. kinds of pages all inheriting from a Super Page type, while Page history is just a property to the page rather a complex property with properties inside it.
[HttpPost]
public ActionResult Edit(Car car)
{
if (ModelState.IsValid)
{
db.Entry(car).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(car);
}
This is a controller method scaffolded by MCV 4
My "car" entity has a unique field: LicensePlate.
I have custom validation on my Entity:
Validation:
public partial class Car
{
partial void ValidateObject(ref List<ValidationResult> validationResults)
{
using (var db = new GarageIncEntities())
{
if (db.Cars.Any(c => c.LicensePlate.Equals(this.LicensePlate)))
{
validationResults.Add(
new ValidationResult("This licenseplate already exists.", new string[]{"LicensePlate"}));
}
}
}
}
should it be usefull, my car entity:
public partial class Car:IValidatableObject
{
public int Id { get; set; }
public string Color { get; set; }
public int Weight { get; set; }
public decimal Price { get; set; }
public string LicensePlate { get; set; }
public System.DateTime DateOfSale { get; set; }
public int Type_Id { get; set; }
public int Fuel_Id { get; set; }
public virtual CarType Type { get; set; }
public virtual Fuel Fuel { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var result = new List<ValidationResult>();
ValidateObject(ref result);
return result;
}
partial void ValidateObject(ref List<ValidationResult> validationResults);
}
QUESTION:
Everytime I edit a car, it raises an error:
Validation failed for one or more entities. See
'EntityValidationErrors' property for more details.
The error is the one raised by my validation, saying it can't edit because there is already a car with that license plate.
If anyone could point me in the right direction to fix this, that would be great!
I searched but couldn't find anything, so even related posts are welcome!
note: this field has a unique constraint, so it is imperative that this validation is still triggered for a create-action
Allright I found a fix but I'm not sure it's the best ever.
I modified the validation so that it only triggers when the Id is non existend (so.. 0).
This way, I can make a distiction between new entities and updated ones.
if (db.Cars.Any(c => c.LicensePlate.Equals(this.LicensePlate) && c.Id != this.Id))
This does fix my problem, but somehow I think there should be a cleaner fix.
I have this model:
public class ReservationViewModel
{
public Flight InboundFlight { get; set; }
public Flight OutboundFlight { get; set; }
}
//Flight
public class Flight
{
public List<ISeat> Seats { get; set; }
}
//Seats
public interface ISeat
{
ECabin Cabin { get; }
int NumberOfSeatsAvailable { get; }
int SeatsChosen { get; set; }
string PropertyName { get; }
}
My HTML consist of the folliwing:
<select id="OutboundFlight__0__Seats_SeatsChosen" name="OutboundFlight.[0].Seats.SeatsChosen" class="valid"><option...
<select id="OutboundFlight__0__Seats_SeatsChosen" name="OutboundFlight.[1].Seats.SeatsChosen" class="valid"><option...
<select id="OutboundFlight__0__Seats_SeatsChosen" name="OutboundFlight.[2].Seats.SeatsChosen" class="valid"><option...
My Action:
[HttpPost]
public ActionResult Index(ReservationViewModel model, FormCollection form)
{
return View();
}
Upon submit I try to bind back to the model but the Seats of each flight returns null...
Help will be appreciated
The HTML being generated is incorrect to get it to bind to a list - the field name has to match what what accessing the property from c# would look like:
This should work:
name="OutboundFlight.Seats[0].SeatsChosen"
I have been struggling to create a Dropdown list which will display Country names from database.
The situation is:
I have a Controller "AdvertisementController", a model"AdvertisementModel" and a View "Create.cshtml".
On the view I need to create a dropdown list which will display country names from database.
I know the good thing will be to create a Viewmodel. But how shall I do that?
A bunch of code will be much appreciated. :)
I have the following code but it shows 'null reference' error.
Viewmodel:
public class CommunicationViewModel
{
public string CategoryID { get; set; }
public IEnumerable<SelectListItem> CategoryList { get; set; }
}
Model:
public class CreateAdModel
{
[Required]
[Display(Name = "Title")]
public string Title { get; set; }
[Required]
[Display(Name = "Description")]
[DataType(DataType.MultilineText)]
public string Message { get; set; }
[Required]
[Display(Name = "Ad type")]
public string AdType { get; set; }
[Required]
[Display(Name = "Ad category")]
public string AdCategory { get; set; }
public CommunicationViewModel categories { get; set; }
}
Controller:
public ActionResult Index()
{
var query = db.AddCategory.Select(c => new SelectListItem
{
Value = c.ID.ToString(),
Text = c.Name
}
);
var model = new CommunicationViewModel { CategoryList = query.AsEnumerable() };
return View(model);
}
Razor:
#Html.DropDownListFor(m=>m.categories.CategoryID,Model.categories.CategoryList,"--Select one--")
This may help you. Drop down for roles when adding users. very simple tutorial
http://rtur.net/blog/post/2009/06/03/Quick-and-dirty-role-management-in-ASPNET-MVC.aspx