Spring MVC: path variable {studentId} not displaying correctly - url

This really confuses me.
Instead of proper URL with the value of one path variable {studentId}:
"http://somedomain.com:8080/someWebApp/essays/main/student/25/activity/add" (where 25 is the value of path variable {studentId})
I get this in my URL:
"http://somedomain.com:8080/someWebApp/essays/main/student/%7BstudentId%7D/activity/add"
This is my controller method for displaying some testPage and it works fine:
#RequestMapping(value="/{studentId}/activity/add", method = RequestMethod.GET)
public String getForm(#PathVariable Integer studentId, Model model) {
StudentActivityDTO studentActivityDTO = new StudentActivityDTO();
Student student = studentService.get(studentId);
studentActivityDTO.setStudent(student);
studentActivityDTO.getActivity().setEssayFlag("Essay");
model.addAttribute("studentActivityDTO", studentActivityDTO);
model.addAttribute("courseList", courseService.getAll());
model.addAttribute("teacherList", teacherService.getAll());
return "testPage";
}
And this is post controller method where this problem happens:
#RequestMapping(value="/{studentId}/activity/add", method = RequestMethod.POST)
public String postForm(#ModelAttribute("studentActivityDTO") StudentActivityDTO studentActivityDTO,
#PathVariable Integer studentId,
Model model) {
logger.debug("Received request to add new activity to student");
Activity activity = studentActivityDTO.getActivity();
activityService.add(studentId, activity);
return "success/addActivitySuccess";
}
In the first case #PathVariable works fine, in the second case it gives this error:
Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: "{studentId}"
Instead of some value of the {studentId} being in the URL, I get string "{studentId}".
Can someone please tell me why?
Update: This is the important part of the jsp page (it is a pretty big page):
<c:url var="studentUrl" value="/essays/main/student/{studentId}/activity/add" />
<form:form modelAttribute="studentActivityDTO" method="POST" action="${studentUrl}">
...
<input type="submit" value="Submit" />
</form:form>

Probably you wanted
<c:url var="studentUrl" value="/essays/main/student/${studentActivityDTO.student.id}/activity/add" />

Related

How can I persist a check box list in MVC

I'm trying to build an html helper for creating a list of checkboxes, which will have the check state persisted using sessions. It works for the most part, remembering check box states when you check and uncheck various boxes and click submit. However, if you have boxes checked and submitted, and you go back and clear the checkboxes and resubmit (when they are ALL cleared) - it seems to want to remember the last selections. Here is what I've written...
[HomeController]
public ActionResult Index()
{
TestViewModel tvm = new TestViewModel();
return View(tvm);
}
[HttpPost]
public ActionResult Index(TestViewModel viewModel)
{
viewModel.SessionCommit();
return View(viewModel);
}
[Index View]
#model TestApp.Models.TestViewModel
#{
ViewBag.Title = "Index";
}
#using (Html.BeginForm())
{
<p>Checkboxes:</p>
#Html.CheckedListFor(x => x.SelectedItems, Model.CheckItems, Model.SelectedItems)
<input type="submit" name="Submit form" />
}
[TestViewModel]
// Simulate the checklist data source
public Dictionary<int, string> CheckItems
{
get
{
return new Dictionary<int, string>()
{
{1, "Item 1"},
{2, "Item 2"},
{3, "Item 3"},
{4, "Item 4"}
};
}
}
// Holds the checked list selections
public int[] SelectedItems { get; set; }
// Contructor
public TestViewModel()
{
SelectedItems = GetSessionIntArray("seld", new int[0] );
}
// Save selections to session
public void SessionCommit()
{
System.Web.HttpContext.Current.Session["seld"] = SelectedItems;
}
// Helper to get an int array from session
int[] GetSessionIntArray(string sessionVar, int[] defaultValue)
{
if (System.Web.HttpContext.Current.Session == null || System.Web.HttpContext.Current.Session[sessionVar] == null)
return defaultValue;
return (int[])System.Web.HttpContext.Current.Session[sessionVar];
}
[The HTML helper]
public static MvcHtmlString CheckedList(this HtmlHelper htmlHelper, string PropertyName, Dictionary<int, string> ListItems, int[] SelectedItemArray)
{
StringBuilder result = new StringBuilder();
foreach(var item in ListItems)
{
result.Append(#"<label>");
var builder = new TagBuilder("input");
builder.Attributes["type"] = "checkbox";
builder.Attributes["name"] = PropertyName;
builder.Attributes["id"] = PropertyName;
builder.Attributes["value"] = item.Key.ToString();
builder.Attributes["data-val"] = item.Key.ToString();
if (SelectedItemArray.Contains(item.Key))
builder.Attributes["checked"] = "checked";
result.Append(builder.ToString(TagRenderMode.SelfClosing));
result.AppendLine(string.Format(" {0}</label>", item.Value));
}
return MvcHtmlString.Create(result.ToString());
}
public static MvcHtmlString CheckedListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, Dictionary<int, string> ListItems, int[] SelectedItemArray)
{
var name = ExpressionHelper.GetExpressionText(expression);
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
return CheckedList(htmlHelper, name, ListItems, SelectedItemArray);
}
I've read this SO question and I think this may be to do with the model binder not knowing when there are no checkboxes checked, but even though I've gone through that and various other posts - I'm no further forward.
In one post, I saw that a hidden field is often used in combination with the checkbox to pass the 'false' state of the checkbox, but I couldn't get it working with multiple checkboxes posting back to a single property.
Can anyone shed light on this?
EDITED : to include the demonstration project I've highlighted in this post. Hopefully this will help someone to help me!
Your main issue, and the reason why the previous selections are being 'remembered' when you un-check all items is that you have a constructor in your model that calls GetSessionIntArray() which gets the values you stored last time you submitted the form. The DefaultModelBinder works by first initializing your model (including calling its default constructor) and then setting the values of its properties based the form values. In the following scenario
Step 1: Navigate to the Index() method
Assuming its the first call and no items have been added to Session, then the value of SelectedItems returned by GetSessionIntArray() is int[0], which does not match any values in CheckItems, so no checkboxes are checked.
Step 2: Check the first 2 checkboxes and submit.
The DefaultModelBinder initializes a new instance of TestViewModel and calls the constructor. The value of SelectedItems is again int[0] (nothing has been added to Session yet). The form values are then read and the value of SelectedItems is now int[1, 2] (the values of the checked checkboxes). The code inside the method is called and int[1, 2] is added to Session before returning the view.
Step 3: Un-check all checkboxes and submit again.
Your model is again initialized, but this time the constructor reads the values from Session and the value of SelectedItems is int[1,2]. The DefaultModelBinder reads the form values for SelectedItems, but there are none (un-checked checkboxes do not submit a value) so there is nothing to set and the value of SelectedItems remains int[1,2]. You then return the view and your helper checks the first 2 checkboxes based on the value of SelectedItems
You could solve this by removing the constructor from the model and modifying the code in the extension method to test for null
if (SelectedItemArray != null && SelectedItemArray.Contains(item.Key))
{
....
However there are other issues with you implementation, including
Your generating duplicate id attributes for each checkbox (your use of builder.Attributes["id"] = PropertyName;) which is invalid html.
builder.Attributes["data-val"] = item.Key.ToString(); makes no sense (it generates data-val="1", data-val="1" etc). Assuming you want attributes for unobtrusive client side validation, then the attributes would be data-val="true" data-val-required="The SelectedItems field is required.". But then you would need a associated placeholder for the error message (as generated by #Html.ValidationMessageFor() and the name attribute of each checkbox would need to be distinct (i.e. using indexers - name="[0].SelectedItems" etc).
Your using the value of the property for binding, but the correct approach (as all the built in extension method use) is to first get the value from ModelState, then from the ViewDataDictionary and finally if no values are found, then the actual model property.
You never use the value of var metadata = ModelMetadata..... although you should be (so that you can remove the last parameter (int[] SelectedItemArray) from the method, which is in effect just repeating the value of expression.
Side note: The use of a hidden field is not applicable in your case. The CheckboxFor() method generates the additional hidden input because the method binds to a bool property, and it ensures a value is always submitted.
My recommendation would be to use a package such as MvcCheckBoxList (I have not tried that one myself as I have my own extension method), at least until you spend some time studying the MVC source code to better understand how to create HtmlHelper extension methods (apologies if that sounds harsh).

Grails command object is not validated

I am using grails web flow for multiform registration process in my project. I created the command object which implements Serializable.
class CustomerCommand implements Serializable{
String Name
Integer Age
Date DateOfBirth
String FatherOrHusbandName
String IdProof
String IdProofNumber
static constraints = {
}
}
My flow section
def customerRegisterFlow = {
enter {
action {
Customer flow.customer = new Customer()
[customer: flow.customer]
}
on("success").to("AddCustomer1")
}
AddCustomer1 {
on("next") { CustomerCommand cuscmd ->
if(cuscmd.hasErrors()) {
flash.message = "Validation error"
flow.cuscmd = cuscmd
return error()
}
bindData(flow.customer, cuscmd)
[customer: flow.customer]
}.to("AddCustomer2")
}
}
Now I am facing two problems.
1) When I click next button, the hasErrors() function is not properly validating the form input values. It simply redirects to AddCustomer2 page. It accepts blank values also.
2) I am not able to access the flow scope object in view page(GSP). This is required when I click back button from AddCustomer2, it should show the page with values which are already entered by the user from flow scope
<input type="text" class="input" name="Name" value="${customer?.Name}"/>
This is my input field in AddCustomer1. Kindly help me anyone to fix this issue which you might have faced already. Thanks in advance
you should call cuscmd.validate() before checking if the method cuscmd.hasErrors()
CustomerCommand class should have annotation #Validateable:
#grails.validation.Validateable
class CustomerCommand implements Serializable{
I think lukelazarovic already answered your first question. To answer your second question: Have to add the commandobj to the flow when the back button is clicked like this:
AddCustomer2 {
on("next") { CustomerCommand cuscmd ->
if(cuscmd.hasErrors()) {
flash.message = "Validation error"
flow.cuscmd = cuscmd
return error()
}
bindData(flow.customer, cuscmd)
[customer: flow.customer]
}.to("finish")
on("back"){CustomerCommand customer->
flow.customer= customer
}.to "AddCustomer1"
}
UPDATE
Try to be consistent in you naming of the command objects too to reduce confusion
For example above you are using flow.cuscmd and flow.customer. This will cause problems for you when you are rendering errors in your view e.g
<g:if test="${customer?.hasErrors()}">
<g:renderErrors bean="${customer}" as="list" />
</g:if>
In your case errors won't be rendered because you have named the object flow.cuscmd

missunderstanding mvc default binding

I have multiselect jquery plagin (Choosen) and when I use it in 'Multiple Select' mode I expect in controller next values:
posted string = 'value1,value2...'
really have
posted string = 'value2'
only if I reffer directly to FormCollection I'll get expected values as below:
[HttpPost]
public ActionResult TagSearech(/*string tagSelect*/FormCollection c)
{
// only one value here
// string[] names = tagSelect.Split(',');
// as expected: value1,....
string expectedValue = c['tagSelect'];
return View();
}
I cant understand what might cause this behavior.
EDIT
Here is View:
#using (Html.BeginForm("TagSearech", "Tag"))
{
#Html.DropDownList("tagSelect", Model, new { #class = "chzn-select", data_placeholder = "tag names", multiple = "" })
<input type="submit"/>
}
MVC will attempt to bind the input data on the URL into the model. I haven't seen how Chosen.js posts the data back to the server, but essentially its coming in in the wrong format, so MVC binds the first element it sees to the string Model.
The FormsCollection retrieves all of the data that was posted in the URL, which is why all of your selected values can be seen there.
Did you try changing the incoming model from string to string[], and see if all of the items are bound to the array?

MVC - Data back from controller to view

My question : If we already have made view -a strongly typed, then why do we need to return model object back from controller post method to view - MVC 4 asp.net ?
For example : I have calculator view :
#using (Html.BeginForm())
{
<p>Number One : #Html.TextBoxFor(m => m.numberOne)</p>
<p>Number Two : #Html.TextBoxFor(m => m.numberTwo)</p>
<input type="submit" value ="Addition" name="calculateOperation" />
<input type="submit" value ="Subtraction" name="calculateOperation" />
<input type="submit" value ="Multiplication" name="calculateOperation" />
<input type="submit" value ="Division" name="calculateOperation" />
}
#using (Html.BeginForm())
{
<p>Output : #Html.TextBoxFor(m => m.result)</p>
}
and controller :
public ActionResult Calculate(Calculator model, string calculateOperation)
{
if (calculateOperation.Equals("Addition"))
{
int[] array = { 1, 12, 5, 26, 7, 14, 3, 7, 2 };
model.result = model.numberOne + model.numberTwo;
}
if (calculateOperation.Equals("Subtraction"))
{
model.result = model.numberOne - model.numberTwo;
}
if (calculateOperation.Equals("Multiplication"))
{
model.result = model.numberOne * model.numberTwo;
}
if (calculateOperation.Equals("Division"))
{
model.result = model.numberOne / model.numberTwo;
}
return View(model);
}
If I don't return the model object, I don't get value of model.result.
Looking for a valid reason.
HTTP is a stateless protocol. Hence, when you do work on the server, if you want it to display something on the client, you have to send it back down. An MVC strongly-typed view is really just an abstraction on top of the rendering engine.
When you "Submit" the form you are just doing an HTTP POST back to your controller action (an http request).
Calling
return View(model)
means that you are sending an HTTP response that returns a rendered html page (using your view). In your case you're simply passing in the model as a parameter.
Well, you don't have to send back a model, you could just use the FormCollection parameter, but then you would have to fetch the values and cast them to the correct type your self.
public ActionResult Calculate(FormCollection form, string calculateOperation)
{
// Need to check if form["numberOne"] is null or empty or do a int.TryParse()
int numberOne = int.Parse(form["numberOne"]);
}
With a strongly typed model you get that for free by the model binders in asp.net mvc. The code looks much cleaner and it's easier to use.
With a model you also get the power of attributes, like validation and scaffolding. It's much cleaner and easier to validate most scenarios using a model with validation attributes.
In this case you need to send a model to the view simply because the view requires it. That's how it is designed. How would the model or the view know that you have made an calculation if you don't store it somewhere? Of course you could also use ViewBag:
ViewBag["result"] = model.numberOne + model.numberTwo;
And in your view:
<p>Output :#Html.TextBox("result", (string)ViewBag["result"])</p>
I always figured that this was to cover cases when there was some kind of explanation or response type data.
For example. You submit an address to be added to the database and you have a service which checks the address for correctness. If it is correct, it gets persisted, otherwise it gets corrected, added to a special field in the original object and sent back for confirmation.
There's no actual requirement for your controllers method to return anything that consumes that or any other model. As a result you still need to be explicit with what View and the data associated with it that you want to return.
They could add some sort of overload to View that would implicitly assume it should use some ViewModel in the method parameters, but that's non-intuitive and unnecessary.

struts2 - optiontransferselect - pass list of integers to the action

I have an optiontransferselect in a form but i dont know how to get the selected items in the rightlist back in my action.
I need to get a list with all the visited countries' ids. i tried in my action List (Integer) countriesVisitedId; but it returns nullPointerException. then i tried Integer id but it returns null.
this is what i have:
s:optiontransferselect
label="Select visited countries"
name="countriesNotVisitedId"
leftTitle="Not visited countries"
rightTitle="Visited Countries"
list="%{countriesNotVisited}"
listKey="id"
listValue="name"
headerKey="countryNotVisitedId"
headerValue="--- Please Select ---"
doubleName="countriesVisitedId"
doubleList="%{countriesVisited}"
doubleHeaderKey="countryVisitedId"
doubleHeaderValue="--- Please Select ---"
doubleListKey="id"
doubleListValue="name" />
how can I get the list with the Integers ids of the visited countries in my action?
I was banging my head on the wall wondering what I was doing wrong. It is pretty simple
doubleName="fields" is the tag field that is returned
public void setFields(String fields) { this is what needs to be in your action class.
The thing that I didn't realise is the elements need to be selected in order to be sent back. Or simple use ajax with in your header
Here's what I tried, it works fine.
Step 1: JSP to select the country from the left hand side into right hand.
<s:optiontransferselect
label="Favourite Characters"
name="leftSide"
id="left"
leftTitle="Left Title"
rightTitle="Right Title"
list="%{countriesNotVisited)"
multiple="true"
headerKey="headerKey"
doubleList="{}"
doubleId="right"
doubleName="rightSide"
doubleHeaderKey="doubleHeaderKey"
doubleMultiple="true" />
Step 2: Javascript code to auto select all data from the right hand side.
function selectall()
{
var list = document.getElementById("right");
for (var i = 0; i < list.options.length; i++)
{
alert(list.options[i].value)
list.options[i].selected = true;
}
var form = document.getElementById("right");
form.submit();
return true;
}
Step 3: call this function on submit, from the JSP side.
<s:submit id="submitid" value="Submit" action="insert" onclick="selectall()"/>
Step 4: In the action, make the getters and setters of object names of the left and right sides take strings and not string arrays.
private String leftSide;
private String rightSide;
public String getLeftSide() {
return leftSide;
}
public String getRightSide() {
return rightSide;
}
public void setRightSide(String rightSide) {
this.rightSide = rightSide;
}
public void setLeftSide(String leftSide) {
this.leftSide = leftSide;
}
Now if you try to print a value in the action, you will get values:
System.out.println("right side list " + ad.getRightSide());
In your action:
public void setCountriesVisitedId(String[] countriesVisitedId) {
this.countriesVisitedId = countriesVisitedId;
}

Resources