I have a large model (large I mean model class contains a lot of fields/properties and each has at least one validation attribute (such as Required, MaxLength, MinLength etc)). Instead of creating one view with a lot of inputs for user to fill model with data I want to create several views where user will fill part of model fields and go to the next step (some kind of "wizard"). While redirecting between steps I store not fullfilled model object in Session. Something like below:
Model:
public class ModelClass
{
[MaxLength(100)] ...
public string Prop1{get;set;}
[MaxLength(100)] ...
public string Prop2{get;set;}
...
[Required][MaxLength(100)] ...
public string Prop20{get;set;}
}
Controller:
[HttpPost]
public ActionResult Step1(ModelClass postedModel)
{
// user posts only for example Prop1 and Prop2
// so while submit I have completly emty model object
// but with filled Prop1 and Prop2
// I pass those two values to Session["model"]
var originalModel = Session["model"] as ModelClass ?? new ModelClass();
originalModel.Prop1 = postedModel.Prop1;
originalModel.Prop2 = postedModel.Prop2;
Session["model"] = originalModel;
// and return next step view
return View("Step2");
}
[HttpPost]
public ActionResult Step2(ModelClass postedModel)
{
// Analogically the same
// I have posted only Prop3 and Prop4
var originalModel = Session["model"] as ModelClass;
if (originalModel!=null)
{
originalModel.Prop3 = postedModel.Prop3;
originalModel.Prop4 = postedModel.Prop4;
Session["model"] = originalModel;
// return next step view
return View("Step3");
}
return View("SomeErrorViewIfSessionBrokesSomeHow")
}
Step1 view has inputs only for Prop1 and Prop2, Step2 view contains inputs for Prop3 and Prop4 etc.
BUT HERE IS THE THING
When user is on, for example, step 1, and fills Prop1 with value more than 100 characters length client side validation works fine. But, of course , I have to validate this value and on the server side in controller. If I had full model I'd just do the following:
if(!ModelState.IsValid) return View("the same view with the same model object");
so user has to fill the form again and correct.
BUT on step 1 user has filled only 2 properties of 20, and I need to validate them. I can't use ModelState.IsValid because model state will be invalid. As You can see Prop20 is marked with [Required] attribute, when user submits Prop1 and Prop2, Prop20 is null and that's why ModelState is invalid. Of course I could allow user to go to step2, fill all of the steps and validate model state only on the last step but I don't want to allow user to go to step 2 if he filled step 1 incorrect. And I want this validation in controller. So the question is:
How can I validate only part of the model? How can I verify that only some of the model properties match their validation attributes?
One possible solution:
Use ModelState.IsValidField(string key);
if (ModelState.IsValidField("Name") && ModelState.IsValidField("Address"))
{ ... }
Then at the end when everything is done use:
if(ModelState.IsValid) { .. }
I think the most elegant way is to do it like that:
List<string> PropertyNames = new List<string>()
{
"Prop1",
"Prop2"
};
if (PropertyNames.Any(p => !ModelState.IsValidField(p)))
{
// Error
}
else
{
// Everything is okay
}
or:
List<string> PropertyNames = new List<string>()
{
"Prop1",
"Prop2"
};
if (PropertyNames.All(p => ModelState.IsValidField(p)))
{
// Everything is okay
}
else
{
// Error
}
In MVC Core, this will be the equivalent of:
if (ModelState.GetFieldValidationState("Name") == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Valid)
{
// do something
}
However, I would recommend simply creating a separate view model in this instance.
Your partial view model could be inherited by your larger view model so you won't have to repeat yourself in code (DRY principal).
It's better to avoid hard-coding the property names!
Just to add to the existing answers for this. Rather than hardcoding the property names I would use an attribute to be added along with the rest of your validation attributes along the lines of:
public class ValidationStageAttribute : Attribute
{
public int StageNumber { get; private set; }
public ValidationStageAttribute(int stageNumber)
{
StageNumber = stageNumber;
}
}
Now that we can get the property names without knowledge of the model itself the partial validation can be pulled into a method (if you use it a lot, your base controller would be a good spot).
protected bool ValidateStage(object viewModel, int stageToValidate)
{
var propertiesForStage = viewModel.GetType()
.GetProperties()
.Where(prop => prop.GetCustomAttributes(false).OfType<ValidationStageAttribute>().Any(attr => attr.StageNumber == stageToValidate))
.Select(prop => prop.Name);
return propertiesForStage.All(p => ModelState.IsValidField(p));
}
Now all you'd need to do in your post action would be to call ValidateStage(viewModel, 1)
Related
hopefully I'm missing something obvious, but I have a bit of an issue with some code I've written, and it's not feeling like I've written my code correctly.
So, let's say I have the following Model:
public class SampleViewModel {
[Required]
public string Property1 { get; set; }
[Required]
[EmailAddress]
public string Property2 { get; set; }
public IList<AbstractModel> Items { get; set; }
}
And then I have this abstract view model:
public abstract AbstractModel {
[Required(ErrorMessage = "This field is required")]
public virtual string Value { get; set; }
}
And these concrete view models:
public ConcreteModel1 : AbstractModel { }
public ConcreteModel2 : AbstractModel { }
Within my Controller, I have the following code (this is actually being done elsewhere, but for this sample, this is fine):
var model = new SampleViewModel();
var fields = new List<AbstractModel>() {
new ConcreteModel1() { Value = model.Property1 },
new ConcreteModel2() { Value = model.Property2 },
};
model.Fields = fields;
return View(model);
Within the SampleViewModel partial view (as I have one for each view model type), I have the following:
#model SampleViewModel
#for(var i = 0; i < Model.Items; i++) {
#Html.EditorFor(m => Model.Items[i])
}
Lets say that I also have a distinct partial view (with very different layout requirements) per each AbstractModel.
Example for the ConcreteModel1:
#model ConcreteModel1
#Html.TextboxFor(m => m.Value)
And for the ConcreteModel2:
#model ConcreteModel2
#Html.DisplayFor(m => m.Value)
This is all working, but as I've had to pass the ViewModel's properties (Property1) into the AbstractModel, I have lost the connection between the view and the underlying model. I have been able to bind the form fields back to the model, using a custom Model Binder, but the main thing I'm missing are the model validators which have been added to the SampleViewModel class.
Ideally I want this information to be available to the AbstractModel. Validation is happening, but I'm only getting basic validation on the client (via AbstractModel's Value required attribute), but I'd like to be able to pass along validation needs from my SampleViewModel into the AbstractModel.
Expectation
What I'd really like to happen is for the AbstractModel's Value property to somehow impersonate the property that is passed into it, so that it is just acting as a proxy to the original model, but has just reshaped the SampleViewModel (or specifically it's Property1 property).
So the important thing is, considering the following creation of my fields:
var fields = new List<AbstractModel>() {
new ConcreteModel1() { Value = model.Property1 },
new ConcreteModel2() { Value = model.Property2 },
};
How do the AbstractModels know that their Values are supposed to be: Required, and also Required and an EmailAddress, based on the properties that are used to create them?
Thank you for your input.
rI need some advice on where to run a calculation on data.
I have a viewmodel that contains all the fields that I need for my calculation and I created the following for one of my calculations:
public class CommissionVM
{
public int? LoanAmountLock { get; set; } // from loan table
public decimal BranchRev { get; set; } // from revenue table
public decimal BranchProcessFee { get; set; } // from revenue table
public decimal BranchGrossTotal
{
get
{
return Convert.ToDecimal(LoanAmountLock * (BranchRev/ 100) + BranchProcessFee);
}
}
}
I tried to use the Model.BranchGrossTotal in my view, but it is returning 0. I think I have an order-of-operations problem. The values LoanAmountLock, BranchRev, and BranchProcessFee are returned as the results of a query:
public ActionResult Reconcile(int? id, string RevenueLoanType)
{
var model = new CommissionVM()
{
Loan = db.Loan.FirstOrDefault(a => a.id == id ),
BranchRevenues = db.BranchRevenues.FirstOrDefault(a => a.RevenueLoanType == RevenueLoanType),
};
return View(model);
}
I originally was able to get these calculations to work by doing all the math in the controller after I populate the viewmodel with the query, but there will be about 10 calculations, and from what I understand, I shouldn't clutter up my controller with business logic.
What is the best solution for this? Do I need to create another class for the calculations? If so, how do I populate that class with my data and use it in my controller?
EDIT: I am not sure how to set up the business classes and use them in the controller. Can anyone point me in the direction of a tutorial?
You should not do the calculation in your controller nor in your view model. You should do it in the Business Layer. Think about View Models are really simple classes that contain data to be displayed to the user, that's it.
Regarding the calculation, you should convert one of the terms to decimal, not the result of the calculation. If you divide integers, you get an integer.
You could create a class and call it CommissionService for example. That class should call your Data Layer to get the data, do any extra calculation and return the data (maybe a DTO) to the controller. The controller should create View Models based on the DTO and send them to the view.
Read these articles:
1) https://msdn.microsoft.com/en-us/library/hh404093.aspx
2) http://www.asp.net/mvc/overview/older-versions-1/models-%28data%29/validating-with-a-service-layer-cs
3) http://blog.diatomenterprises.com/asp-net-mvc-business-logic-as-a-separate-layer/
4) http://sampathloku.blogspot.com.ar/2012/10/how-to-use-viewmodel-with-aspnet-mvc.html
I don't like calculations on my view models -- you can't reuse the calculation easily elsewhere and it is harder to test and debug. Create separate classes to do business logic.
Your business logic classes can either return your view models or return values you use to populate those models. The trade-off is ease of use with reusability.
I generally favor returning the value rather than a big object so my services are more reusable.
Controller
public class BranchController : Controller
{
private IBusinessService service;
public BranchController()
{
this.service = new BusinessService(db);
}
public ActionResult Reconcile(int? id, string RevenueLoanType)
{
var model = new CommissionVM
{
BranchGrossTotal = this.service.GetBranchGrossTotal(id, RevenueLoanType),
...
};
return View(model);
}
}
Service
You can make any number of these and your controllers would use them as needed. If you need a query you should pass the DbContext instance or you may have problems with related entities on separate contexts.
public interface IBusinessService
{
decimal GetBranchGrossTotal(int id, string revenueLoanType);
}
public class BusinessService : IBusinessService
{
private DbContext db;
public BusinessService(DbContext db)
{
this.db = db;
}
public decimal GetBranchGrossTotal(int id, string revenueLoanType)
{
var branch = db.Branch.First(b => b.Id == id);
// do stuff
return total;
}
}
You could fully-populate and return a view model in your GetBranchGrossTotal() if you choose.
First of all, the properties you are assigning to your CommissionVM on your controller do not match the ones declared on your model. You assign Loan and BranchRevenues, when you have only LoanAmountLock and BranchRevs available on your model.
Please notice that the Loan property is an object itself, and the LoanAmountLock must be retrieved from this object (Loan.LoanAmountLock). The same happens with the BranchRevenues object. You should assign the BranchRevs to the respective property of the BranchRevenues object as needed. If you do not do this, then the values will default to 0 and when trying to calculate the BranchGrossTotal it will obviously be 0.
Another reason, assuming that you are correctly populating your model properties, is that the FirstOrDefault method, renders null values because there is no such entity. This will result also in the BranchGrossTotal to be 0.
You are right that you do not need to clutter your controller neither with calculations nor with db access. I would create a business class ComissionBusiness and instantiate it at the top of your controller. This class would have a method which performs all calculations. You should move the Reconcile method to your new business class method and call it on the reconcile action. Something like (excuse the lack of syntax)
public MyController : Controller {
public ComissionBusiness comissionBusiness;
public MyController(){
comissionBusiness = new ComissionBusiness();
}
public ActionResult Reconcile(int? id, string RevenueLoanType)
{
var model = comissionBusiness.Reconcile(id, revenueLoanType);
return View(model);
}
}
When I am changing the "model => model.id" to "model => model.Supplierid" i am getting below error
"The parameter 'expression' must evaluate to an IEnumerable when
multiple selection is allowed."
please have look on below code
// this my model class
public class clslistbox{
public int id { get; set; }
public int Supplierid { get; set; }
public List<SuppDocuments> lstDocImgs { get; set; }
public class SuppDocuments
{
public string Title { get; set; }
public int documentid { get; set; }
}
public List<SuppDocuments> listDocImages()
{
List<SuppDocuments> _lst = new List<SuppDocuments>();
SuppDocuments _supp = new SuppDocuments();
_supp.Title = "title";
_supp.documentid = 1;
_lst.Add(_supp);
return _lst;
}
}
// this my controller
[HttpGet]
public ActionResult AddEditSupplier(int id)
{
clslistbox _lst = new clslistbox();
_lst.lstDocImgs= _lst.listDocImages();
return View(_lst);
}
// this is view where i am binding listboxfor
#model clslistbox
#using (Html.BeginForm("AddEditSupplier", "Admin", FormMethod.Post))
{
#Html.ListBoxFor(model => model.id, new SelectList(Model.lstDocImgs, "documentid", "title"))
}
Can anyone see the reason for it?
I think the changing of the property in the expression here is a red-herring - it won't work in either case.
Update
However, see at the end of my answer for some probably needlessly detailed exposition on why you didn't get an error first-time round.
End Update
You're using ListBoxFor - which is used to provide users with multiple selection capabilities - but you're trying to bind that to an int property - which cannot support multiple selection. (It needs to be an IEnumerable<T> at least to be able to bind a list box to it by default in MVC)
I think you mean to be using DropDownListFor - i.e. to display a list of items from which only one can be selected?
If you're actually looking for single-selection semantics in a listbox, that's trickier to do in MVC because it's Html helpers are geared entirely around listboxes being for multiple selection. Someone else on SO has asked a question about how to get a dropdown to look like a list box: How do I create a ListBox in ASP.NET MVC with single selection mode?.
Or you could generate the HTML for such a listbox yourself.
(Update) - Potentially needlessly detailed exposition(!)
The reason you don't get an exception first time round is probably because there was no value for id in ModelState when the HTML was generated. Here's the reflected MVC source (from SelectExtensions.SelectInternal) that's of interest (the GetSelectListWithDefaultValue call at the end is the source of your exception):
object obj =
allowMultiple ? htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string[])) :
htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string));
if (!flag && obj == null && !string.IsNullOrEmpty(name))
{
obj = htmlHelper.ViewData.Eval(name);
}
if (obj != null)
{
selectList =
SelectExtensions.GetSelectListWithDefaultValue(selectList, obj, allowMultiple);
}
Note first that the control variable allowMultiple is true in your case, because you've called ListBoxFor. selectList is the SelectList you create and pass as the second parameter. One of the things that MVC (unfortunately in some cases) does is to use ModelState to modify the select list you pass when re-displaying a view in order to ensure that values which were set in ModelState via a POST are re-selected when the view is reloaded (this is useful when page validation fails because you won't copy the values to your underlying model from ModelState, but the page should still show those values as being selected).
So as you can see on the first line, the model's current value for the expression/field you pass is fished out of model state; either as a string array or as a string. If that fails (returns null)then it makes another go to execute the expression (or similar) to grab the model value. If it gets a non-null value from there, it calls SelectExtensions.GetSelectListWithDefaultValue.
As I say - what you're trying to do will ultimately not work in either the case of Id or SupplierId (because they would need to be IEnumerable) but I believe this ModelState->Eval process is yielding a null value when you use Id, so the process of getting an 'adjusted' SelectList is skipped - so the exception doesn't get raised. The same is not true when you use SupplierId because I'll wager that there's either a value in ModelState at that point, or the ViewData.Eval successfully gets an integer value.
Not throwing an exception is not the same as working!.
End update
Try changing your property from int to int[]
public class SuppDocuments
{
public string Title { get; set; }
public int documentid { get; set; }
}
Assuming above is the class used for binding the model , try changing the documentid property as below
public class SuppDocuments
{
public string Title { get; set; }
public int[] documentid { get; set; }
}
I am using C# to create a view model that I later serialize into Json for use with KnockoutJs.
Now I need to add information on a property level if a certain user has access to view and/or edit the property.
Since Javascript has little to no reflection possibilities I want to do this with C# before serializing to Json.
My view model could look like this:
class ViewModel {
public string Name { get; set; }
}
I want to use a User Access Service that would take the user, which has some Roles, and the view model and go through the Roles and apply the access rights. Something like this:
class AccessService {
public void Apply(IUser user, ViewModel viewModel) {
if(user.Roles.Contains(Roles.Admin)) {
viewModel.AllowRead(vm => vm.Name);
}
}
}
The viewModel.AllowRead(..) method would be an extension method that would take an object (or maybe an interface or type if necessary) and this is where the magic would happen.
I would like the result of this operation to be that the viewModel would get a new property with the name CanRead which in turn would have a boolean property with the name Name.
The resulting viewModel would look like this:
class ViewModel {
public string Name { get; set; }
public object CanRead { // Could non anonymous as well.
return new {
Name = true;
};
}
}
Can this be done with dynamics or do I have to use Reflection.Emit? I'm not asking for "show me the codez". I just want to know if my idea is mental or if it's possible.
Any ideas?
I think it would be possible and you could use the Lambdanator to help achieve this.
Example usage:
Lambda.Reflect<SomeClass>(x => x.AMethod());
Lambda.Reflect<SomeClass>(x => x.AProperty);
Lambda.Reflect(() => local);
I have a view model like so that is created from my validator.
public class ViewModel
{
public KeyValuePair<int, RuleType> Foo { get; set; }
public KeyValuePair<string, RuleType> Bar { get; set; }
}
My real view model has 20+ field. Once my data is validated a generic list of type ViewModel is then returned to my MVC view and processed into a report. However, a feature request has come up where users wish to only see models with errors and warning, excluding valid entities. RuleType is a enumerator. A model is valid if all values of the key pair are RuleType.Success.
Is it possible to loop through each model and checking the RuleType without manually having to check every property? My GetAllModelsWithErrors() function would return a list of invalid models. I believe reflection could be a solution however I'm not sure if it's a good solution.
Try this:
private IEnumerable<ViewModel> GetInvalidModels(ViewModel[] viewModels)
{
return
from viewModel in viewModels
from prop in typeof(ViewModel).GetProperties()
let ruleType = ((KeyValuePair<object, RuleType>)prop.GetValue(viewModel, null)).Value
where ruleType != RuleType.Success
select viewModel;
}