Consider the following code:
public class AccountNumber
{
[AccountNumber] //This validator confirms the format of the account number
public string Value {get; set;}
public int Format { get; set;}
public string ToString()
{
return Value + " is format " + Format;
}
}
public class MyViewModel
{
public MyViewModel()
{
SourceAccount = new AccountNumber();
DestinationAccount= new AccountNumber();
}
[Required]
AccountNumber SourceAccount {get; set;}
AccountNumber DestinationAccount {get; set;}
}
And then, in my View:
#Html.EditorFor(model => model.SourceAccount.Value)
#Html.EditorFor(model => model.DestinationAccount.Value)
Basically, I want to say that the user must enter a Source Account, and that they optionally enter a Destination Account. However, if they do enter a Destination Account it must conform to a certain format.
The problem with the code above is that the required validator on the SourceAccount will always return valid, as SourceAccount is never null. What would be a good approach for implementing what I am trying to achieve?
Please note that in real-life the setter for Value is more complex than shown, as it reformats the account number in a canonical format.
Edit Please note that we have to use inbuilt MVC validation, as that is what the rest of the project is currently using.
See Extending the Model Binder for Enhanced Validation.
This is fully compatible with built-in MVC validation.
You can - of course - customize this solution by using your own interface for validation.
A simple approach could be to add simple string properties for the SourceAccount and DestinationAccount numbers as follows:
public class MyViewModel
{
public MyViewModel()
{
}
[Required]
[AccountNumber]
public string SourceAccountNumber { get; set; }
[AccountNumber]
public string DestinationAccountNumber { get; set; }
public AccountNumber SourceAccount
{
get
{
return new AccountNumber
{
Value = SourceAccountNumber,
Format = 0 // Set Format appropriately
};
}
}
public AccountNumber DestinationAccount
{
get
{
return new AccountNumber
{
Value = DestinationAccountNumber,
Format = 0 // Set Format appropriately
};
}
}
}
Maybe you'd like to try FluentValidation, it's a model validation alternative to data annotation attributes, which allows you to add more complex model validation logic.
The code is still pretty concise and straightforward:
[Validator(typeof(PersonValidator))]
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
}
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(x => x.Id).NotNull();
RuleFor(x => x.Name).Length(0, 10);
RuleFor(x => x.Email).EmailAddress();
RuleFor(x => x.Age).InclusiveBetween(18, 60);
}
}
Related
I'm having some problem with one of my data in strawberry shake. It's a simple query but when I try to create an object of it, the generated code dtoData ask for a type in the constructor.
All my other query don't required anything in the constructor.
I use the Dto for model in the ViewModel for mapping.
Example of a working one.
public clas ProjectModelVM : ViewModelBase
{
//The ProjectDtoInput class is created by the Generation code.
// If I create a new Instance of ProjectDtoInput the constructor is empty and it's working
// var project = new ProjectDtoInput()
public ProjectDtoInput _projectModel;
public ProjectModelVM(ProjectDtoInput projectModel)
{
_projectModel = projectModel;
}
}
the query is
query GetActiveProject{
activeProject{
cL_PROJET_NO,
cL_PROJET_NOM
}
}
And the Dto in the server side is
public class QmProdFormDto
{
public int? FormID { get; set; }
public string? FormName { get; set; }
public string? ProjectNo { get; set; }
public string? ProjectName { get; set; }
public string? DivName { get; set; }
public string? SubDivName { get; set; }
}
Example of a NON working one
public class PaintBrandModelVM : ViewModelBase
{
// The QmProdPaintDtoData class is created by the Generation code.
// If I create a new Instance of QmProdPaintDtoData the constructor ask me for some __type input
public QmProdPaintDtoData _paintBrandModel;
public PaintBrandModelVM(QmProdPaintDtoData paintBrandModel)
{
_paintBrandModel = paintBrandModel;
}
}
the query is
query GetPaintLst{
paintLst{
paintBrandID,
paintBrandName
}
}
And the Dto on the server side is
public class QmProdPaintDto
{
public int? PaintBrandID { get; set; }
public string? PaintBrandName { get; set; }
}
I don't understand why one of the DTO ask me for mendatory input.
Can someone give me a hand on this.
Tell me if you need more of the code.
Thanks.
Jc
I am validating the content for file import and I have an IsValid property for each line.
public class Header
{
public int LineNumber { get; set; }
public string Property1 { get; set; }
public string Property2 { get; set; }
public bool IsValid { get; set; }
}
public class Detail
{
public int LineNumber { get; set; }
public string Property1 { get; set; }
public string Property2 { get; set; }
public string Property3 { get; set; }
public bool IsValid { get; set; }
}
public class Trailer
{
public int LineNumber { get; set; }
public string Property1 { get; set; }
public bool IsValid { get; set; }
}
public class ImportFile
{
public Header Header { get; set; }
public List<Detail> Details { get; set; }
public Trailer Trailer { get; set; }
}
and my validators look somewhat like:
public class DetailValidator : AbstractValidator<Detail>
{
public DetailValidator()
{
RuleFor(d => d.Property1)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.WithState(d => d.LineNumber)
.Length(3)
.WithState(d => d.LineNumber);
RuleFor(d => d.Property2)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.WithState(d => d.LineNumber)
.MaximumLength(50)
.WithState(d => d.LineNumber);
...
}
}
public class ImportFileValidator : AbstractValidator<ImportFile>
{
public ImportFileValidator()
{
RuleFor(f => f.Header)
.SetValidator(new HeaderValidator());
RuleForEach(f => f.Details)
.SetValidator(new DetailsValidator());
...
}
}
After I call the validation, I wanted to set the IsValid property of each line of the file (be it header, detail or trailer) base from the result of the validation.
What is possible for now is, since I am using WithState to store the LineNumber, I can match the ValidationResult against the ImportFile instance to set each line's validity like below:
ImportFile file = // parsed file content
var result = new ImportFileValidator().Validate(file);
foreach (var detail in file.Details)
{
var error = result.Errors.FirstOrDefault(e =>
Convert.ToInt32(e.CustomState) == detail.LineNumber);
detail.IsValid = error == null;
}
And I have to check for the header and trailer as well.
Is there a way I can do this inside the validators? I am trying to explore the FluentValidation's documentation, but I can't seem to find what I needed there.
As I was exploring the available methods in FluentValidation, I saw OnFailure and OnAnyFailure methods. This methods might be a good help to what I needed to do, but the problem is they're obsolete as of 10.3.0 and will be removed on version 11. They're suggesting to use a custom validator instead.
The Header, Detail and Trailer Abstract Validators remain as is.
I created custom validator extensions for those 3.
Each extension methods creates an instance of the corresponding validator and executes it. I can make them generic for header, detail and trailer since they will do the same thing, set IsValid property to the validation result.
public static IRuleBuilderOptionsConditions<ImportFile, T> IsHeaderValid<T>(this IRuleBuilder<ImportFile, T> ruleBuilder)
where T : Header
{
return builder.Custom((header, context) =>
{
// Create the Header Abstract Validator Instance
var validator = new HeaderValidator();
var result = validator.Validate(Header);
header.IsValid = result.IsValid;
// Pass the errors to the context
result.Errors.ForEach(context.AddFailure);
}
}
I had to change the ImportFileValidator to call the custom validators, instead of using setvalidator.
The ImportFileValidator looks like this:
public class ImportFileValidator : AbstractValidator<ImportFile>
{
public ImportFileValidator()
{
RuleFor(f => f.Header)
.IsHeaderValid();
RuleForEach(f => f.Details)
.IsDetailValid();
...
}
}
This is pretty much how I was able to set the IsValid property without having to do the matching I initially did in the question.
I'm creating an xml feed of products which needs to match the clients scheme exactly.
I'm using web api. I would like the property extractDate to be an attribute. The following code is outputting extractDate as an element not an attribute
public Feed GetProducts()
{
var feed = new Feed()
{
extractDate = "extractDate",
incremental = true,
name = "name",
Brands = GetBrands(),
Categories = GetCategories(),
Products = GetProducts()
};
return feed;
}
Here is my model Feed. Note the following doesn't seem to turn the element into an attribute
[XmlAttribute(AttributeName = "extractDate")]
public class Feed
{
[XmlAttribute(AttributeName = "extractDate")] //attribute is ignored
public string extractDate { get; set; }
public bool incremental { get; set; }
public string name { get; set; }
public List<Brand> Brands { get; set; }
public List<Category> Categories { get; set; }
public List<Product> Products { get; set; }
}
How do i output
<feed extractDate="2012/01/01"
// other logic
/>
Web API by default uses DataContractSerializer in XmlMediaTypeFormatter and probably that's the reason you are not seeing your attribute decorations taking effect. Do you have the XmlSerializer enabled on the XmlMediaTypeFormatter to see your expected output?
config.Formatters.XmlFormatter.UseXmlSerializer = true;
Also, you could set XmlSerializer only for specific types too using the following api:
config.Formatters.XmlFormatter.SetSerializer<>
Edit
Managed to simulate your issue with a blank project and Kiran's answer seems to do the trick.Just add this line in your controller(for testing purposes, it should probably be in your global.asax)
GlobalConfiguration.Configuration.Formatters.XmlFormatter.UseXmlSerializer = true;
Do you have the [XmlRoot] on top of your class or is it missing?
Not sure the attribute will work without an xml class decorator.
A simple sanity check you could do is serialize the class without web api involved to make sure it's nothing silly but actually web api related.
How about this:
[XmlRoot("feed")]
public class Feed
{
[XmlAttribute(AttributeName = "extractDate")]
public string extractDate { get; set; }
public bool incremental { get; set; }
public string name { get; set; }
public List<Brand> Brands { get; set; }
public List<Category> Categories { get; set; }
public List<Product> Products { get; set; }
}
I'm having trouble passing view information from my Get/Create action to my view. Here are my three model classes;
public class Competition
{
public int Id { get; set; }
public int CompetitionId { get; set; }
public string Name { get; set; }
public string Prize { get; set; }
}
public class CompetitionEntry
{
public int Id { get; set; }
public int CompetitionEntryId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int CompetitionId { get; set; }
}
public class CompetitionEntryViewModel
{
public int Id { get; set; }
public Competition Competitions { get; set; }
public int CompetitionId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
Here is my Get/Create action in CompetitionEntry Controller;
public ActionResult Create(int id)
{
CompetitionEntryViewModel competitionentryviewmodel = db.CompetitionEntriesView.Find(id);
return View(competitionentryviewmodel);
}
I know this doesn't work. The id parameter goes into the URL fine. How to I get access to my Competition class in th Get action? I need to be able to show the competion name on my Create Competition entry view.
Thanks in advance!
public ActionResult Create(int id)
{
var data = db.CompetitionEntriesView.Find(id);
CompetitionEntryViewModel competitionentryviewmodel = new CompetitionEntryViewModel();
competitionentryviewmodel.CompetitionName = data.Name;
return View(competitionentryviewmodel);
}
What you are trying to do is build an object graph and display it through a view model. In order to do this, you need to map your domain model(s) to your view model.
You can do the mapping yourself by writing a lot of code (re-inventing the wheel), or, you could consider using third party tools to do this for you. I recommend you use an AutoMapper as it is very simple to use imo.
The other problem is that your view model contains a domain model. This is likely to cause you a lot of headache in near future. If I were you, I would replace Competition with CompetitionViewModel.
I would also consider creating a view model for a list of competitions, i.e. CompetitionsViewModel. Look into partial views to see how you can display a list of competitions.
Good luck
UPDATE #3: Entire question
I have a class HB:
public class HB
{
public int Id { get; set; }
[StringLength(3000)]
public string Text { get; set; }
public Title Title { get; set; }
}
And Title:
public class Title
{
public int Id { get; set; }
public string Name { get; set; }
public int MaxChar { get; set; }
}
Before you can write a HB (which is kind of an article), you have to choose your title, so your StringLength for HB.Text can be determined. Meaning, this article can only have a certain amount of chars, deppending on what 'Title' the writer has. Example: Title1 can only write a 'HB' with 1000 chars, and Title2 can write a 'HB' with 3000 chars. So. Thats means the the StringLength has to come from Title.MaxChar. Whats the smartest way to do that?
The Title entity is prefixed data that will be stored in the db.
To be crystal clear, what I want to achieve is something in the line with: [StringLength(Title.MaxChar)]
Ive done structure/design for this mechanism in Webforms a million times, my brain just cant addapt to mvc, so some help would be appreciated. Code would be even more appreciated.
Pretty sure that is not possible as written. This strikes me as trying to force business logic into the model that belongs in the controller.
In this situation, I would make the attribute on the Text property [StringLength(3000)]. In the controller, during validation, I would write something along these lines:
public ActionResult (HB model)
{
if (model.Text.Length > model.Title.MaxChar){
ModelState.AddModelError("Text", string.Format("Text for this Title cannot exceed {0} characters.", model.Title.MaxChar));
}
if (ModelState.IsValid)
{
//do stuff
return RedirectToAction("Index"); //or something
}
else
{
return View(model);
}
}
I believe this will accomplish what you are trying to do. Now, for the Title object, I'd flatten that out a bit in your model:
public class HB
{
#region Base Properties
public int Id { get; set; }
[StringLength(3000)]
public string Text { get; set; }
#endregion
#region Title Properties
public int TitleId { get; set; }
public string TitleName { get; set; }
public int TitleMaxChar { get; set; }
#endregion
}
This is assuming you need to display that information in your view. If you just need to reference it for your business logic validation, just have the TitleId property and use that to instantiate the Title object in your controller when you need it. Don't forget to make hidden inputs for each of these properties if they are not editable!