ASP.NET MVC 3 model and viewmodel classes - asp.net

I have a viewmodel and it has a nested class what is not in connection other model class. I can fill the necessary data in this nested class (ClassX) to show those in the view but I can't get out data from the nested class (through MyViewModel) in the post action just when I give it in post action as another parameter. It appears in original viewmodel (MyViewModel) but its attributes are null/0.
public class MyViewModel
{
public MyViewModel()
{
classX = new ClassX();
}
public ClassX classX;
public int attrA {get;set;}
...
}
public class ClassX {}
//
// POST: /MyModel/Create
public ActionResult Create(MyViewModel myvm, **ClassX cx**, FormCollection collection)
{}
My question: Can I read data out from the nested class through the viewmodel class?

http://goneale.com/2009/07/27/updating-multiple-child-objects-and-or-collections-in-asp-net-mvc-views/ this is a good article for you
MyViewModel myViewModel= new MyViewModel();
UpdateModel(myViewModel, "MyViewModel");
myViewModel.myViewModel= new myViewModel();
UpdateModel(myViewModel.classX, "User.classX");

If I understood your question correctly, you need BindAttribute.Prefix on your ClassX cx parameter of action method. This way, model binder will correctly bind values for it. The value for Bind.Prefix should be name of ClassX property in MyViewModel, in your example, the string - "classX"
//
// POST: /MyModel/Create
public ActionResult Create(MyViewModel myvm, [Bind(Prefix = "classX")]ClassX cx, FormCollection collection)
{}
Idea is in the following - on client side, when you submit the form, its values are sent to server like this
attrA=someValue
classX.SomeProperty=someValue
classX.SomeOtherProperty=someOtherValue
When passed to action parameters, this name=value string pairs are translated to objects. Names from left side of equality match to property names of MyViewModel, and the ClassX parameter stays empty. But then you specify Prefix = "classX", model binder matches strings after dot in left side of equality to ClassX property names, so that should fill values of ClassX too.

Related

Spring MVC #Validation with Marker Interface in Generic Controller Method

I have a Spring MVC survey application where the Controller method called by each form POST is virtually identical:
#PostMapping("/1")
public String processGroupOne (
Model model,
#ModelAttribute("pageNum") int pageNum,
#ModelAttribute(GlobalControllerAdvice.SESSION_ATTRIBUTE_NAME) #Validated(SurveyGroupOne.class) SurveyCommand surveyCommand,
BindingResult result) {
if (result.hasErrors()) {
LOG.debug(result.getAllErrors().toString());
model.addAttribute("pageNum", pageNum);
return "survey/page".concat(Integer.toString(pageNum));
}
pageNum++;
model.addAttribute("pageNum", pageNum);
return "redirect:/survey/".concat(Integer.toString(pageNum));
}
The only difference is what part of the SurveyCommand object is validated at each stop along the way. This is designated by the marker interface passed to the #Validated() annotation. The marker interfaces (SurveyGroupOne, SurveyGroupTwo, etc) are just that, markers:
public interface SurveyGroupOne {}
public interface SurveyGroupTwo {}
...
and they are applied to properties of objects in the SurveyCommand object:
public class Person {
#NotBlank(groups = {
SurveyGroupTwo.class,
SurveyGroupThree.class})
private String firstName;
#NotBlank(groups = {
SurveyGroupTwo.class,
SurveyGroupThree.class})
private String lastName;
...
}
My question: how can I make the method generic and still use the marker interface specific to the page being processed? Something like this:
#PostMapping("/{pageNum}")
public String processGroupOne (
Model model,
#PathVariable("pageNum") int pageNum,
#ModelAttribute(GlobalControllerAdvice.SESSION_ATTRIBUTE_NAME)
#Validated(__what goes here??__) SurveyCommand surveyCommand,
BindingResult result) {
if (result.hasErrors()) {
LOG.debug(result.getAllErrors().toString());
model.addAttribute("pageNum", pageNum);
return "survey/page".concat(Integer.toString(pageNum));
}
pageNum++;
model.addAttribute("pageNum", pageNum);
return "redirect:/survey/".concat(Integer.toString(pageNum));
}
How can I pass the proper marker interface to #Validated based solely on the pageNum #PathVariable (or any other parameter)?
Because #Validated is an annotation, it requires its arguments to be available during compilation and hence static. You can still use it but in this case you will have N methods, where N is a number of steps. To distinguish one step from another you can use params argument of #PostMapping annotation.
There is also another way where you need to inject Validator to the controller and invoke it directly with an appropriate group that you need.

ASP.NET MVC Cast BaseViewModel to DerivedViewModel

I'm developing a registration flow where user comes and fills 5 pages to complete a process. I decided to have multiple views and one controller and a ProcessNext action method to go step by step. Each time Process Next gets called it gets the origin view and next view. Since each view associated with there own view model i have created a base view model which all view specific view model derived from. Now the issue is, casting is throwing an exception.. here is the sample code
Base View Model
public class BaseViewModel
{
public string viewName;
}
Personal View Model
public class PersonalViewModel : BaseViewModel
{
public string FirstName;
// rest properties comes here
}
Index.cshtml
#Model PersonalViewModel
#using (Html.BeginForm("ProcessNext", "Wizard", FormMethod.Post, new { class = "form-horizontal", role = "form" }))
#Html.TextBoxFor(m => m.FirstName, new { #class = "form-control" })
<input type="submit" class="btn btn-default" value="Register" />
Basically, I'm binding the view with PersonalViewModel here
Now in Controller ProcessNext Action method looks like this.
public ActionResult ProcessNext(BaseViewModel viewModelData)
{
PersonalViewModel per = (PersonalViewModel) viewModelData;
}
This is failing and throwing a type case exception, why?..
My idea is to use only one action method to transform all these derived view model and send to a common class to validate and process. Please help me to get through this issue.. Thanks!
The reason that you see this exception is that your model type is BaseViewModel and not PersonalViewModel. Model binder is the one that creates a model and since your action's model is BaseViewModel it creates a BaseViewModel object.
I would recommend you to create separate actions for each one of your steps. Each action should have its corresponding model. I also think that you should prefer with composition instead of inheritance in this case.
public class FullModel
{
public FirstStepModel FirstStep {get;set;}
public SecondStepModel SecondStep {get;set;}
}
Then once you start your flow (on a first step for example) you can create a FullModel object and store it somewhere (session/cookie/serialize into a text and send to client - it is really up to you).
Then in controller you will have
[HttpGet]
public ActionResult ProcessFirst()
{
HttpContext.Session["FullModel"] = new FullModel(); //at the beginning store full model in session
var firstStepModel = new FirstsStepModel();
return View(firstStepModel) //return a view for first step
}
[HttpPost]
public ActionResult ProcessFirst(FirstStepModel model)
{
if(this.ModelState.IsValid)
{
var fullModel = HttpContext.Session["FullModel"] as FullModel; //assuming that you stored it in session variable with name "FullModel"
if(fullModel == null)
{
//something went wrong and your full model is not in session..
//return some error page
}
fullModel.FirstStep = model;
HttpContext.Session["FullModel"] = fullModel; // update your session with latest model
var secondStepModel = new SecondStepModel();
return View("SecondStepView", secondStepModel) //return a view for second step
}
// model is invalid ...
return View("FirstStepView", model);
}
[HttpPost]
public ActionResult ProcessSecond(SecondStepModel model)
{
var fullModel = HttpContext.Session["FullModel"] as FullModel; //assuming that you stored it in session variable with name "FullModel"
if(fullModel == null)
{
//something went wrong and your full model is not in session..
//return some error page
}
fullModel.SecondStep = model;
HttpContext.Session["FullModel"] = fullModel; // update your session with latest model
var thirdStepModel = new ThirdStepModel();
return View("ThirdStepModel", thirdStepModel); //return a view for a third step
}
Of course you should extract all the shared code to some reusable method.
And it is entirely up to you what persistence technique to use for passing FullModel between the request.
If you still prefer to go with one Action solution you need to create a custom model binder that is going create derived instances based on some data that is passed from the client. Take a look at this thread
I figured it out a generic way to handle this situation using Model Binders. Here it is..
You might need to have a extended model binder from DefaultBinder to implement to return your model type.
public class WizardModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var viewIdContext = bindingContext.ValueProvider.GetValue("ViewId");
int StepId = 0;
if (!int.TryParse(viewIdContext, out StepId))
throw new InvalidOperationException("Incorrect view identity");
//This is my factory who gave me child view based on the next view id you can play around with this logic to identify which view should be rendered next
var model = WizardFactory.GetViewModel(StepId);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, model.GetType());
bindingContext.ModelMetadata.Model = model;
return model;
}
}
You would register this binder from your gloab asx like
ModelBinders.Binders.Add(typeof(BaseViewModel), new WizardModelBinder());
Thanks to all who responsed to my query..!! Let me know if you guys have any questions.
Happy coding..!!

RedirectAttributes changes object id between controllers

I am using Spring MVC and Hibernate in my project. Also I have 2 controllers UserController and BookController where BookController redirects to the users page and I am passing a Book object in addition.
I've found I can do this with RedirectAttributes but the problem is that the id of the passed Book object is changed during this transition to user.id.
BookController.java
public class BookController {
#RequestMapping("/users/{user_id}/books/edit/{book_id}")
public String editBook(#PathVariable("user_id") int user_id, #PathVariable("book_id") int book_id, final RedirectAttributes redirectAttrs){
bookDetail = this.bookService.getBookById(book_id)
redirectAttrs.addFlashAttribute("bookDetail", bookDetail);
System.out.println(bookDetail);
return "redirect:/users/"+user_id;
}
}
Prints: id=8, title=Motylek, description=Some description, user_id=2.
UserController.java
public class UserController {
#RequestMapping("/users/{id}")
public String detailUser(#ModelAttribute("bookDetail") Book bookDetail, #PathVariable("id") int id, Model model){
User u = this.userService.getUserById(id);
model.addAttribute("user", u);
model.addAttribute("bookDetail", bookDetail);
System.out.println(bookDetail);
return "user";
}
}
Prints: id=2, title=Motylek, description=Some description, user_id=2.
Do you have and idea why this happens or is it a bug? Thanks.
I'm going to assume that your Book class has a property called id, ie. a getter or setter called getId() and setId(..).
When Spring parses the request URL, it stores path segments as declared in the corresponding #RequestMapping. So for
/your-app/users/2
and
#RequestMapping("/users/{id}")
It will store
id=2
as a request parameter.
Spring will then proceed to generate an argument for
#ModelAttribute("bookDetail") Book bookDetail
It will check the various request, session, servlet attributes for an entry with the name bookDetail. (If it doesn't find one, it will create one and add it to the request attributes.) In your case, it will have found the object in the HttpSession. It will then bind any request parameters to matching object properties. Since the parameter above is called id, it will be bound to the Book property id.
You should be good by changing
#RequestMapping("/users/{id}")
to
#RequestMapping("/users/{user_id}")
along with the corresponding #PathVariable.

ASP.NET MVC4 Generalized Field Initialization

In ASP.NET MVC4, I want to initialize fields with values. When the user gets the page, before he has posted it back, I want to have it start out with values in some fields. The values will be drawn from the query string, not hard coded. Say a user is filling out a form: He's logged in, you know his name and address. As a courtesy, make those the default values. Doesn't matter where they come from, really, except that it's a set of key-value pairs, and the values are strings. I just want to put something in fields without the user posting the form first and I'd really like to do it without hard-coding a long, long list of assignments to every property in a rather complicated model.
Right now it's done in a JS loop in $(document).ready(), but it belongs on the server. I'd like to replicate that logic, though: Treat the query param names as unique identifiers.
In the Index() method of my controller, I tried calling ModelState.TrySetModelValue() (which when ModelState is populated, identifies each field by one unique string) but at this stage, ModelState is empty, so of course that didn't work. I tried changing Index() to expect an instance of the model as a parameter, but that doesn't help.
Must I rewrite every #Html.EditorFor()/TextBoxFor()/etc. call in the application? That seems crazy. Properly, this is something I'd do in a loop, in one place, not scattered around in multiple spots in each of a growing number of views.
I have a feeling that I'm failing to grasp something fundamental about the way MVC4 is intended to work.
UPDATE 2
It turns out that if you decorate your action method with [HttpGet], and you have it expect the model as a parameter, then if you use the field names (foo.bar) rather than IDs (foo_bar) in the query string, it does what I want automatically. ModelState is populated. I must not have had the action method decorated with [HttpGet] when I looked at ModelState.
If a field is set via query string automatically, that supersedes whatever's in your model. That's reasonable; the whole point is to override the model's default values. But if you want to in turn override possible query string values (e.g., say there's a checkbox for an "electronic signature"; that should always require an explicit effort on the user's part), then you've got to do that via ModelState.
That means that my first solution, below, had no actual effect (provided I had the [HttpGet] property on the action method). It only set properties of the model which had already been set in ModelState by the framework, and whose values in the model were therefore ignored.
What's a little bit stranger is that ModelState gives fields a different key if they're not in the query string. foo.bar.baz uses just that as a key if it's in the query string, but if it isn't, the key becomes foo.footypename.bar.bartypename.baz. There appears to be an exception if the property's name is the same as it's type: I have a Name model class, and another model class has a property public Name Name { get; set }. Properties of type Name, which are named name, are never followed by their type name in the ModelState keys. However, I have not yet ruled out other possible reasons for that particular property having its typename excluded. That's a guess. The typenames are excluded for "leaf" properties in all cases in my model. Is that because they're types known to the system, or "leaves", or what? I don't know.
In any case, a leaf property of the "root" class of the model always uses its own name as a key in ModelState.
So the generalized answer is you assign to the model. But there's a different specific answer for initialization from a query string.
UPDATE
Solution -- much code snipped
// Controller base
public abstract class ControllerBase<TModel> : Controller
{
[HttpGet]
public virtual ActionResult Index(TModel model)
{
HttpContext.Request.QueryString.CopyTo(model);
return View("Index", model);
}
}
public static class Extensions
{
/// <summary>
/// Given NameValueCollection of keys/values in the form
/// "foo.bar.baz" = "text", and an object which is the *parent* of
/// foo, set properties of foo accordingly.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="src"></param>
/// <param name="model"></param>
public static void CopyTo<T>(this NameValueCollection src, T target)
{
String strkey;
Object objval;
foreach (var key in src.Keys)
{
strkey = "" + key;
objval = src[strkey];
target.TrySetPropertyValue(strkey, objval);
}
}
/// <summary>
/// Given a reference to an object objThis, the string "foo.bar.baz",
/// and an object o of a type optimistically hoped to be convertible
/// to that of objThis.foo.bar.baz, set objThis.foo.bar.baz = o
///
/// If foo.bar is null, it must have a default constructor, or we fail
/// and return false.
/// </summary>
/// <param name="objThis"></param>
/// <param name="propPathName"></param>
/// <param name="value"></param>
/// <returns></returns>
public static bool TrySetPropertyValue(this object objThis,
string propPathName, object value)
{
if (string.IsNullOrWhiteSpace(propPathName))
{
throw new ArgumentNullException(propPathName);
}
var names = propPathName.Split(new char[] { '.' }).ToList();
var nextPropInfo = objThis.GetType().GetProperty(names.First());
if (null == nextPropInfo)
return false;
if (names.Count > 1)
{
var nextPropValue = nextPropInfo.GetValue(objThis, null);
if (null == nextPropValue)
{
nextPropValue = Activator
.CreateInstance(nextPropInfo.PropertyType);
nextPropInfo.SetValue(objThis, nextPropValue);
}
names.RemoveAt(0);
return nextPropValue.TrySetPropertyValue(
String.Join(".", names), value);
}
else
{
try
{
var conv = System.ComponentModel.TypeDescriptor
.GetConverter(nextPropInfo.PropertyType);
value = conv.ConvertFrom(value);
nextPropInfo.SetValue(objThis, value);
}
catch (System.FormatException)
{
return false;
}
return true;
}
}
}
You can initialize your model in controller with default values and then use it like
#Html.TextBoxFor(m => Model.Name)
Initialization in Controller:
public ActionResult Index()
{
MyModel model = new MyModel();
model.Name = "myname";
return View("myview", model);
}
You can also set the attributes in TextBoxFor
#Html.TextBoxFor(m => Model.Name, new { value = "myname"})
Update
If your url looks like mysite/Edit?id=123 try decalring your controller action like
public ActionResult Edit(string id)
{ ...
Also try decorating it with HttpPost or HttpGet attribute

asp.net mvc custom model binding in an update entity scenario

Hi I have a question about model binding. Imagine you have an existing database entity displayed in a form and you'd like to edit some details, some properties eg createddate etc are not bound to the form, during model binding, these properties are not assigned to the model as they are not on the http post data or querystrong etc, hence their properties are null. In my controller method for update , Id just like to do
public ActionResult Update( Entity ent)
{
//Save changes to db
}
but as some properties are null in ent, they override the existing database fields which are not part of the form post data, What is the correct way to handle this? Ive tried hidden fields to hold the data, but model binding does not seem to assign hidden fields to the model. Any suggestions would be appreciated
You shouldn't be sending your entity to your view, you should send a slim version of it called a DTO (data transfer object) or ViewModel.
Only send the properties to the view (and hence the form that gets posted back) that you want the user to update with that action.
In your POST handler you can then validate and copy the properties across to your entity.
an AutoMapper can help if you have lots of properties
something like:
public class User
{
int id;
string name;
string email;
}
public class EditUserEmailDto
{
string email;
}
// get
public ActionResult EditEmail(int id,)
{
return View("EditEmail", new EditUserEmailDto());
}
// post
public ActionResult EditEmail(int id, EditUserEmailDto dto)
{
if(!ModelState.IsValid)
return View("EditEmail", dto);
var user = userRepo.Get(id);
user.email = dto.email;
userRepo.Save(user);
return;
}

Resources