I have an action method which outputs a model which has multiple sub models. In one of the sub model I have some additional properties which are not required in my view.
Sub model- ProjectModel-
[Required(ErrorMessage = "*")]
public int Id { get; set; }
[Required(ErrorMessage = "*")]
public int SectorDivisionId { get; set; }
[Required(ErrorMessage = "*")]
[StringLength(250, ErrorMessage = "Project name should not be more than 250 characters.")]
public string Program { get; set; }
[Required(ErrorMessage = "*")]
[StringLength(25, ErrorMessage = "Project number should not be more than 25 characters.")]
public string ProjectNumber { get; set; }
public string WorkPackage { get; set; }
public string WorkPackageType { get; set; }
[Required(ErrorMessage = "*")]
public DateTime StartDate { get; set; }
[Required(ErrorMessage = "*")]
public DateTime EndDate { get; set; }
public int ProjectDirectorId { get; set; }
So while initializing the sub model to my main model I am only using those properties which I need as shown below.
model.ProjectInfo = new ProjectModel()
{
Id = projectId,
ProjectNumber = prj.p.ProjectNumber,
Director = prj.Director,
Program = prj.p.Program,
StartDate = prj.p.StartDate,
EndDate = prj.p.EndDate,
ProjectReviewPeriodList = projectReviewPeriodList.AsEnumerable().
Select(o => new ProjectReviewPeriodModel
{
Id = o.Id,
ProjectReviewTypeId = o.ProjectReviewTypeId,
ProjectId = o.ProjectId,
ReviewPeriod = o.ReviewPeriod,
ReviewPeriodDate = o.ReviewPeriodDate
}).ToList()
};
Now, while posting the form I have an action filter at global level which validates the Model. The validation (ModelState.IsValid) fails for some of the fields from the sub model which I haven't initialized as per my needs.
I thought of two options-
Using ModelState.Remove(<PropertyName>) to skip validation. This is not possible as I am using a global level action filter.
Create a new view model
Is there any other way of doing this, preferably in the action method level?
Please let me know if any doubts or I can explain it more clearly.
Thanks.
The clean way would be to use different ViewModels for different usecases.
That being said, you can implement the validation logic with IValidatableObject instead of using Data Annotations attributes.
Introduce a flag into the ViewModel that indicates the usecase, e.g. IsEditUsecase. Set this flag somewhere where you know the usecase, e.g. in the controller.
Then only perform the validations that are needed for this usecase.
public class ProjectModel : IValidatableObject {
public bool IsEditUsecase { get; set; }
[Required(ErrorMessage = "*")] // required for every usecase
public int Id { get; set; }
// no [Required] and [StringLength] attributes
// because only required in some of the usecases
public string Program { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
// validation that occurs only in Edit usecase
if (IsEditUsecase) {
// Validate "Program" property
if (string.IsNullOrWhiteSpace(Program)) {
yield return new ValidationResult(
"Program is required",
new[] { "Program" }
);
}
else if (Program.Length > 250) {
yield return new ValidationResult(
"Project name should not be more than 250 characters.",
new[] { "Program" }
);
}
// validate other properties for Edit usecase ...
}
// validate other usecases ...
}
}
As a dirty hack, I have added hidden fields in my razor page for all those properties which caused ModelState validation error. Basically I added some default values for the hidden fields and it works fine now.
Not recommended though but it was a quick fix.
Related
I have some code that is functioning oddly and was wondering if anyone else hase come across this issue.
I have a view model that collects data from a database via a stored procedure and a vb object (no I do not know vb this is legacy)
When I execute the program the data is collected as expected via the controller. When I debug it I can see all of my parameters populating with information. However when it comes to the view it says that the parameters are null. I have included my code
Models:
public class PersonIncomeViewModel
{
public string IncomeTypeDesc { get; set; }
public string IncomeDesc { get; set; }
public string Income { get; set; }
}
public class PersonIncomeListViewModel
{
public int? PersonId { get; set; }
public List<PersonIncomeListItem> Incomes { get; set; }
public PersonIncomeListViewModel()
{
Incomes = new List<PersonIncomeListItem>();
}
}
public class PersonLookupViewModel : Queue.QueueViewModel
{
public int Action { get; set; }
public bool ShowAdvancedFilters { get; set; }
//Person Search Variables
[Display(Name = #"Search")]
public string SpecialSearch { get; set; }
[Display(Name = #"Person Id")]
public int? PersonId { get; set; }
[Display(Name = #"Full Name")]
public string FullName { get; set; }
[Display(Name = #"SSN")]
public string SSN { get; set; }
public string AddressStatus { get; set; }
public string EmploymentStatus { get; set; }
public PersonIncomeViewModel Income { get; set; }
public List<PersonIncomeListItem> Incomes { get; set; }
public PersonLookupViewModel()
{
Income = new PersonIncomeViewModel();
Incomes = new List<PersonIncomeListItem>();
}
}
Controller:
public ActionResult _Income(int id)
{
var vm = new PersonLookupViewModel();
var personManager = new dtPerson_v10_r1.Manager( ref mobjSecurity);
//var person = personManager.GetPersonObject((int)id, vIncludeIncomes: true);
var person = personManager.GetPersonObject(id, vIncludeIncomes: true);
var look = JsonConvert.SerializeObject(person.Incomes);
foreach (dtPerson_v10_r1.Income income in person.Incomes)
{
if (income.IncomeType_ID == 0)
{
var item = new PersonIncomeListItem
{
IncomeTypeDesc = "Unknown",
IncomeDesc = income.IncomeDesc,
Income = mobjFormat.FormatObjectToCurrencyString(income.Income)
};
vm.Incomes.Add(item);
}
if (income.IncomeType_ID == 1)
{
var item = new PersonIncomeListItem
{
IncomeTypeDesc = "Alimony",
IncomeDesc = income.IncomeDesc,
Income = mobjFormat.FormatObjectToCurrencyString(income.Income)
};
vm.Incomes.Add(item);
}
if (income.IncomeType_ID == 2)
{
var item = new PersonIncomeListItem
{
IncomeTypeDesc = "Child Support",
IncomeDesc = income.IncomeDesc,
Income = mobjFormat.FormatObjectToCurrencyString(income.Income)
};
vm.Incomes.Add(item);
}
}
return PartialView(vm);
}
View:
#using dtDataTools_v10_r1
#using ds_iDMS.Models.Person
#model ds_iDMS.Models.Person.PersonLookupViewModel
#{
var format = new dtDataTools_v10_r1.CustomFormat();
var newInitials = (Model.Income.IncomeTypeDesc.First().ToString() + Model.Income.IncomeDesc.First().ToString() + Model.Income.Income.First().ToString()).ToUpper();
}
using (Html.DSResponsiveRow(numberOfInputs: ExtensionMethods.NumberOfInputs.TwoInputs))
{
using (Html.DSCard(ExtensionMethods.Icon.CustomText, iconInitials: newInitials, color: ExtensionMethods.Colors.PrimaryBlue))
{
<div>#Model.Income.IncomeTypeDesc</div>
<div>#Model.Income.IncomeDesc</div>
<div>#Model.Income.Income</div>
}
}
There are some extensions that we have built but they are irrelevant to the issue
The line that errors out is this one:
var newInitials = (Model.Income.IncomeTypeDesc.First().ToString() + Model.Income.IncomeDesc.First().ToString() + Model.Income.Income.First().ToString()).ToUpper();
Which drives all of the extension methods on the view and as I run the debugger over it all of the parameters read null, however like I said when I run the debugger and check them in the controller they are populated properly.
Sorry about the long post but I wanted to ensure all the detail was there
This is how to pass the Object model to your Partial View
return PartialView("YourViewName", vm);
or using the Views path
return PartialView("~/YourView.cshtml", vm);
EDIT
Try starting your Action Method like this
var vm= new Person();
vm.PersonLookupViewModel = new PersonLookupViewModel();
Problem solved I had issues with some of my vb objects and had the vb person take a look at them and she fixed them.
Thank you for all the help
EDIT
What had to happen is the vb object had to be re-written and my logic was just fine as it was in the beginning. I marked the one response to my question as the answer because had it been in true MVC without vb objects attached to it, that would have worked perfectly
I have seen this done in code once or twice but can't find an example now. Until recently I was working in webforms and have switched to MVC. I have looked around and not found a whole lot on this. The code below is from my controller to pull in the values and display in my view model. I need to take the m.body.MembersId from MSMQ. I know what the problem is I just dont know how to use Linq like this.
The Situation , I need to do all this in one line. Cast the memberIds information that was originally a list named MembersId back to a list to display it but also do a string.join so that the entire list is displayed as one string. While I am aware of how to do both of those by themselves but to do in one line is proving beyond me. I will openly admit I am fairly new to linq and am learning as I go.
This is the class
public class MsgChildClass{
public string SourceSystem { get; set; }
public List<string> MemberIds { get; set; }
public string ParentId { get; set; }
public string Error { get; set; }
}
this is the function
public ActionResult GetAllMessages() {
string queueName = (ViewBag.QueueName != null) ? ViewBag.QueueName : Request.QueryString["queueName"];
Session["CurrentQueueName"] = queueName;
TempData["queueName"] = queueName;
ViewBag.QueueName = queueName;
var queue = _Queues[queueName];
queue.MessageReadPropertyFilter.ArrivedTime = true;
queue.MessageReadPropertyFilter.Body = true;
queue.Formatter = new JsonMessageFormatter();
var messages = queue.GetMessages();
var model = messages.Select(m =>
new ViewModel() {
Id = m.Id,
Date = m.ArrivedTime,
SourceSystem = ((MsgChildClass)m.Body).SourceSystem,
DeletedMemberIds = ((MsgChildClass)m.Body).DeletedMemberIds,//OrderByDescend(x => x); string.Join(",",((MsgChildClass)m.Body).DeletedMemberIds),
ParentMemberId = ((MsgChildClass)m.Body).ParentId,
Error = ((MsgChildClass)m.Body).Error,
QueName = queueName
}).ToList();
return Json(model.ToList(), JsonRequestBehavior.AllowGet);
}
View Model
public class ViewModel
{
[Required]
public string Id { get; set; }
//[Required]
[Display(Name = "Arrival Time")]
public DateTime Date { get; set; }
[Required]
[Display(Name = "Source System")]
public string SourceSystem { get; set; }
[Required]
[Display(Name = "Member Ids")]
public List <string> MemberIds { get; set; }
[Required]
[Display(Name = "Parent Member Id")]
public string ParentMemberId { get; set; }
[Display(Name = "Error")]
public string Error { get; set; }
[Display(Name = "QueName")]
public string QueName { get; set; }
}
This is what I have. All of it works correctly except the memberids. Debugging shows me that the memberids have the value but I am not drilling down far enough. So hopefully some one can help me out quite a bit as been quite few hours at this point. Google is great but he didnt help me out with this one.
It's cleaner and more efficient if you only cast once into a variable. And another advantage is that you can add a break point and look at msgChild.
var model = messages.Select(m =>
{
var msgChild = (MsgChildClass)m.Body;
return new ViewModel
{
Id = m.Id,
Date = m.ArrivedTime,
SourceSystem = msgChild.SourceSystem,
MemberIds = msgChild.MemberIds,
ParentMemberId = msgChild.ParentId,
Error = msgChild.Error,
QueName = queueName
};
}).ToList();
The code was correct which was the maddening part. debugger was telling me the values were there. But the part I didnt look at was how the datatable.js was loading the data for display. After seeing that was done incorrect, I corrected it and now the list is now displayed as string , string, string which is what I was needing so thanks every one
I am trying to set-up a remote validation similar to the one in this example:
Example
My application has a twist however, my form elements are dynamically generated, therefore this tag:
[Remote("doesUserNameExist", "Account", HttpMethod = "POST", ErrorMessage = "User name already exists. Please enter a different user name.")]
is not set in stone, I need to vary the ErrorMessage for example and preferably vary the action. Is it possible, or would you suggest taking the long-way, meaning to implement the whole ajax validation on my own.
Any suggestions are appreciated.
If you need to have a dynamic error message then you could return this as string from your validation action:
public ActionResult DoesUserNameExist(string username)
{
if (Exists(uasername))
{
string errorMessage = "Some dynamic error message";
return Json(errorMessage, JsonRequestBehavior.AllowGet);
}
return Json(true, JsonRequestBehavior.AllowGet);
}
And if you need even more flexibility such as invoking dynamic dynamic actions, then you're better of rolling your custom validation solution instead of relying on the built-in Remote attribute.
You can inherit from RemoteAttribute and make it fetch the required values from a service or factory according to your own logic. Here is an example:
[AttributeUsage(AttributeTargets.Property)]
public class MyRemoteAttribute : RemoteAttribute
{
public MyRemoteAttribute(Type type, string propertyName)
: base(MyRemoteAttributeDataProvider.GetAttributeData(type,propertyName).Action, MyRemoteAttributeDataProvider.GetAttributeData(type,propertyName).Controller)
{
var data = MyRemoteAttributeDataProvider.GetAttributeData(type,propertyName);
base.ErrorMessage = data.ErrorMessage;
base.HttpMethod = data.HttpMethod;
}
}
public static class MyRemoteAttributeDataProvider
{
public static RemoteAttributeData GetAttributeData(Type type
, string propertyName)
{
//this is where you are going to implement your logic im just implementing as an example
//you can pass in a different type to get your values. For example you can pass in a service to get required values.
//property specific logic here, again im going to implement to make this
//specification by example
var attrData = new RemoteAttributeData();
if(propertyName == "MyOtherProperty")
{
attrData.Action = "MyOtherPropertyRelatedAction";
attrData.Controller = "MyOtherPropertyRelatedController";
attrData.ErrorMessage = "MyOtherPropertyRelated Error Message";
attrData.HttpMethod = "POST";
}
else
{
attrData.Action = "UserNameExists";
attrData.Controller = "AccountController";
attrData.ErrorMessage = "Some Error Message";
attrData.HttpMethod = "POST";
}
return attrData;
}
}
public class RemoteAttributeData
{
public string Controller { get; set; }
public string Action { get; set; }
public string HttpMethod { get; set; }
public string ErrorMessage { get; set; }
}
And this is how you are supposed to use is:
public class RegisterViewModel
{
[Required]
[Display(Name = "User name")]
[MyRemote(typeof(RegisterViewModel),"UserName")]
public string UserName { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[System.ComponentModel.DataAnnotations.Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
[Required]
[MyRemote(typeof(RegisterViewModel),"MyOtherProperty")]
public string MyOtherProperty { get; set; }
}
As I also mentioned above at the commentary. You should specialize that provider according to your needs.
I hope this helps.
UPDATE:
I update the implementation based on your comment, so that it takes a property name and does some property name specific wiring.
My Model:
public class FPTAssetSummary : IValidatableObject
{
[Required]
[Display(Name = "New Version")]
public int ForatFrom { get; set; }
[Required]
[Display(Name = "Old Version")]
public int ForatTo { get; set; }
public List<FPTFORATExcel> FPTForatVersionList { get; set; }
public IEnumerable<ValidationResult> Validate(
ValidationContext validationContext)
{
if (ForatFrom <= ForatTo)
{
yield return new ValidationResult(
"Old version must be higher than the new version");
}
}
}
My Controller:
[HttpPost]
public ActionResult ForatExcelCompare(FPTAssetSummary foratcompare)
{
var ExcelIDFrom = foratcompare.ForatFrom;
var ExcelIDTo = foratcompare.ForatTo;
return RedirectToAction("Index", new
{
ForatFrom = ExcelIDFrom,
ForatTo = ExcelIDTo
});
}
Currently I am posting two integers from a view (2 dropdownboxes), to the controller below and passing the two values into the index with the two parameters (ForatFrom and ForatTo).
However my IValidationObject method isn't returning the ValidationResult message. I think I need to check the model state in the ForatExcelCompare method, however I need to be able to return to the previous controller with the error message if the model state is false. Any Ideas?
I have an MVC Model class that looks like this:
[ValidateNewRegistration]
public class NewRegistrationModel {
[System.ComponentModel.DisplayName("Profile Display Name")]
[Required(ErrorMessage = "Display Name must be specified.")]
[StringLength(50)]
public string DisplayName { get; set; }
[System.ComponentModel.DisplayName("Email Address")]
[Required(ErrorMessage = "Email address must be specified.")]
[RegularExpression("^[A-Za-z0-9._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}$",
ErrorMessage = "The email address you specified is invalid.")]
[StringLength(100)]
public string EmailAddress1 { get; set; }
}
In my view, I have this:
#Html.ValidationSummary()
#Html.EditorForModel()
I understand how to use the metadata to mark up the attributes, but I want to do is implement my own Editor Template that has two fields in it ... a first name and last name, that will be used in at least 1/2 dozen places in my website.
How do I add error messages so that they appear in the Validation Summary, but in a way that is contained within the EditorTemplate?
Long term what I am looking for is the ability to put a "[UIHint("--Editor Template--")]" into the classes, and have the editor template self-contained enough to issuing it's onw error message for the form field. So that I do not have to add metadata to the class that says [Required...] or [RegularExpression...]. My end goal is a class that looks like this:
[ValidateNewRegistration]
public class NewRegistrationModel {
[System.ComponentModel.DisplayName("Profile Display Name")]
[UIHint("DisplayName")]
public string DisplayName { get; set; }
[System.ComponentModel.DisplayName("Email Address")]
[UIHint("EmailAddress")]
public string EmailAddress1 { get; set; }
[System.ComponentModel.DisplayName("Email Address (confirm)")]
[UIHint("EmailAddress")]
public string EmailAddress2 { get; set; }
}
A similar question has already been asked here: Is there a way to reuse data annotations?
Check the solutions there and comment if you need help with anything specific
class UIHintAttribute : ValidationAttribute
{
public string Field { get; set; }
public UIHintAttribute(string field)
{
Field = field;
}
public override bool IsValid(object value)
{
if (Field == "DisplayName")
{
if (new RequiredAttribute { ErrorMessage = "DisplayName cannot be blank." }.IsValid(value) && new StringLengthAttribute(50).IsValid(value))
return true;
return false;
}
if (Field == "EmailAddress")
{
if (new RequiredAttribute { ErrorMessage = "Email address cannot be blank." }.IsValid(value)
&& new RegularExpressionAttribute("^[A-Za-z0-9._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}$") { ErrorMessage = "The email address you specified is invalid." }.IsValid(value)
&& new StringLengthAttribute(100).IsValid(value))
return true;
return false;
}
// Needs to return true by default unless Required exists
return true;
}
}
Now you should be able to use it the way you wanted to.
Edit:
Long term what I am looking for is the ability to put a
"[UIHint("--Editor Template--")]" into the classes, and have the
editor template self-contained enough to issuing it's onw error
message for the form field. So that I do not have to add metadata to
the class that says [Required...] or [RegularExpression...].
The class above will make the following possible:
public class NewRegistrationModel {
[System.ComponentModel.DisplayName("Profile Display Name")]
[UIHint("DisplayName")]
public string DisplayName { get; set; }
[System.ComponentModel.DisplayName("Email Address")]
[UIHint("EmailAddress")]
public string EmailAddress1 { get; set; }
[System.ComponentModel.DisplayName("Email Address (confirm)")]
[UIHint("EmailAddress")]
public string EmailAddress2 { get; set; }
}