ASP.net MVC4 validating elements of a drop down in the model? - validation

How can I validate the values assigned to elements of a dropdown list? Normally I would assign ranges in the model and that field would be validated. However, if I have something like this I am not sure how to handle it.
Model
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Value { get; set; }
public DateTime Given { get; set; }
public TimeSpan TimeGiven { get; set; }
public string Phase { get; set; }
public bool Active { get; set; }
public int PersonId { get; set; }
}
The name in the model is a dropdown list of different products. I am not sure how to handle the validation for the Value since the different products will have different ranges. For example, Product Named X will have a valid range of 25-30 where product Y will have a valid range of .01 - .5. The Person can have many products assigned so I have a one to many relationship set up with Person and Product.
Is there a way to validate the value based on what product they select X, Y? I will have approximately 40 different products so Ideally I could do this without having to having a separate model for each product.

You can validate using custom business rules with a ValidationAttribute
It is very straightforward you just need to do the following:
Create a class that inherits from ValidationAttribute and override the IsValid method.
Decorate your property with the attribute you just created.
For example:
[AttributeUsage(AttributeTargets.Property, AllowMultiple =false, Inherited = false)]
public class MyBusinessRuleValidation: ValidationAttribute
{
protected override ValidationResult IsValid(object v, ValidationContext validationContext)
{
var Name = (string)v //since we decorated the property Name with this attribute;
//retrieve Value's value using validationContext
var value = (decimal) validationContext.ObjectType.GetProperty("Value").GetValue(validationContext.ObjectInstance, null);
//check whether you need to exit with error
if( name == ProductX) {
if(value > 10 && value < 25)
return new ValidationResult(ErrorMessage);
}
return ValidationResult.Success;
}
}
Use the validator:
public class Product
{
public int Id { get; set; }
[MyBusinessRuleValidation(ErrorMessage="Some ugly error")]
public string Name { get; set; }
....
}

Related

EF CodeFirst computed field in CF entity class

I have added computed fields(Active and CreditsLeft) directly into my CodeFirst entity class. Is it good idea to add computed field logic inside CF Entity class?
public class User : Entity
{
public User()
{
Id = Helper.GetRandomInt(9);
DateStamp = DateTime.UtcNow;
TimeZone = TimeZoneInfo.Utc.Id;
}
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string Email { get; set; }
[Required]
[MaxLength(50)]
public string Password { get; set; }
[MaxLength(50)]
public string FirstName { get; set; }
[MaxLength(50)]
public string LastName { get; set; }
[Required]
public DateTime DateStamp { get; set; }
public virtual ICollection<Order> Orders { get; set; }
public virtual ICollection<Statistic> Statistics { get; set; }
public virtual ICollection<Notification> Notifications { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public bool Active
{
get
{
return Orders.Any(c => c.Active && (c.TransactionType == TransactionType.Order || c.TransactionType == TransactionType.Subscription));
}
}
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public int CreditsLeft
{
get
{
return Orders.Sum(p => p.Credits != null ? p.Credits.Value : 0);
}
}
}
Is it good idea to add computed field logic inside CF Entity class?
Sure, you can do this, but there are a few things you must take care of.
First, the attribute for a property that is computed by business logic is not [DatabaseGenerated(DatabaseGeneratedOption.Computed)], because this indicates that the value is computed in the database (as in a computed column). You should mark the property by the [NotMapped] attribute. This tells Entity Framework to ignore the property in database mapping.
Second, since both properties use Orders, you must make sure that the orders are loaded or can be lazy loaded when either property is accessed. So you may want to load Users with an Include statement (Include(user => user.Orders)). Or else you must ensure that the context is still alive when Active or CreditsLeft is accessed.
Third, you can't address the properties directly in an EF LINQ query, as in
db.Users.Select(u => u.Active);
because EF will throw an exception that it doesn't know Active. You can address the properties only on materialized user objects in memory.

MVC3 same ViewModel used for two cases with Required attribute in only one case

I have a ViewModel. something like this
public class ViewModel
{
public int Id { get; set; }
public int? Value { get; set; }
}
I have a table of existing ViewModels, and below that I have a form where you can add a new ViewModel
For existing ViewModels that are fetched from DB i want no validation on the Value property, but for the case when adding a new ViewModel I want required validation.... The real model is more complex then this one so I want to use the same model in both cases.. Is it possible?
edit: this works
public class AddNewViewModel : ViewModel
{
public new int Value { get; set; }
}
Is it better to use new or virtual/override and why?
Required attributes are compiled into the class. You could do something like this:
public class BaseViewModel
{
public int Id { get; set; }
public virtual int? Value { get; set; }
}
public class CreateViewModel : BaseViewModel
{
[Required]
public override int? Value { get; set; }
}
This way, you only add the validation attribute to the properties where you need them.

EF 4.1 - Update Properties On Child Collection

I have searched hi and low and I am stuck here.
I am using EF 4.1 in an MVC3 app, with the Service/Repository/UnitOfWork pattern and AutoMapper to map my models and entities.
So I have a really basic situation; I have a collection of ChildProducts that have a collection of PriceTiers.
My view models look like this:
AddEditChildProductModel
public class AddEditChildProductModel
{
#region "Fields/Properties"
public ActionType ActionType { get; set; }
public string FormAction { get; set; }
public int ID { get; set; }
public int ProductID { get; set; }
public string Sku { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public decimal MSRP { get; set; }
public decimal RetailPrice { get; set; }
public int Servings { get; set; }
public decimal Weight { get; set; }
public bool Display { get; set; }
public int DisplayIndex { get; set; }
public IEnumerable<AddEditPriceTierModel> PriceTiers { get; set; }
#endregion
#region "Constructor(s)"
#endregion
#region "Methods"
#endregion
}
AddEditPriceTierModel
public class AddEditPriceTierModel
{
#region "Fields/Properties"
public int ID { get; set; }
public int ChildProductID { get; set; }
public decimal Price { get; set; }
public int QuantityStart { get; set; }
public int QuantityEnd { get; set; }
public bool IsActive { get; set; }
#endregion
#region "Constructor(s)"
#endregion
#region "Methods"
#endregion
}
In the controller action, I am simply trying to map the changed PriceTier properties:
public ActionResult EditChildProduct(AddEditChildProductModel model)
{
if (!ModelState.IsValid)
return PartialView("AddEditChildProduct", model);
ChildProduct childProduct = productService.GetChildProductByID(model.ID);
AutoMapper.Mapper.Map<AddEditChildProductModel, ChildProduct>(model, childProduct);
UnitOfWork.Commit();
return ListChildProducts(model.ProductID);
}
And I am getting this error:
The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
When stepping into the action, the models/entities are mapped correctly, I don't get it!!
Eranga is right. I'm guessing your productService does not call AsNoTracking on the ef context before returning the ChildProduct. If not, this means what it returns is still attached to the context. When automapper does its thing, it replaces the whole collection, which can orphan the attached child entities that were not part of the form submission. Since the orphans don't have a non-null foreign key, they must be deleted from the context before calling SaveChanges. If they are not, you get this infamous exception.
On the other hand, if your productService calls AsNoTracking on the context before returning the entity, it will not track changes, and will not try to delete any orphaned items that do not exist in the collection created by automapper.

MVC3 Dynamic DataAnnotation attribute StringLength

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!

Validation Attributes MVC 2 - checking one of two values

Could someone help me with this issue. I'm trying to figure out how to check two values on a form, one of the two items has to be filled in. How do I do a check to ensure one or both of the items have been entered?
I'm using viewmodels in ASP.NET MVC 2.
Here's a little snip of code:
The view:
Email: <%=Html.TextBoxFor(x => x.Email)%>
Telephone: <%=Html.TextBoxFor(x => x.TelephoneNumber)%>
The viewmodel:
[Email(ErrorMessage = "Please Enter a Valid Email Address")]
public string Email { get; set; }
[DisplayName("Telephone Number")]
public string TelephoneNumber { get; set; }
I want either of these details to be provided.
Thanks for any pointers.
You can probably do this in much the same way as the PropertiesMustMatch attribute that comes as part of the File->New->ASP.NET MVC 2 Web Application.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class EitherOrAttribute : ValidationAttribute
{
private const string _defaultErrorMessage = "Either '{0}' or '{1}' must have a value.";
private readonly object _typeId = new object();
public EitherOrAttribute(string primaryProperty, string secondaryProperty)
: base(_defaultErrorMessage)
{
PrimaryProperty = primaryProperty;
SecondaryProperty = secondaryProperty;
}
public string PrimaryProperty { get; private set; }
public string SecondaryProperty { get; private set; }
public override object TypeId
{
get
{
return _typeId;
}
}
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
PrimaryProperty, SecondaryProperty);
}
public override bool IsValid(object value)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
object primaryValue = properties.Find(PrimaryProperty, true /* ignoreCase */).GetValue(value);
object secondaryValue = properties.Find(SecondaryProperty, true /* ignoreCase */).GetValue(value);
return primaryValue != null || secondaryValue != null;
}
}
The key part of this function is the IsValid function that determines if one of the two parameters has a value.
Unlike normal Property-based attributes, this is applied to the class level and can be used like so:
[EitherOr("Email", "TelephoneNumber")]
public class ExampleViewModel
{
[Email(ErrorMessage = "Please Enter a Valid Email Address")]
public string Email { get; set; }
[DisplayName("Telephone Number")]
public string TelephoneNumber { get; set; }
}
You should be able to add as many as these as you need per form, but if you want to force them to enter a value into one of more than two boxes (Email, Telephone or Fax for example), then you would probably be best changing the input to be more an array of values and parse it that way.

Resources