简体   繁体   中英

How to do a not mandatory Drop Down Box in Spring Roo/Dojo?

In Spring Roo (1.1.5) I have an Entity "Book" that CAN have a reference to Entity "Publisher".

class Book {
   @ManyToOne(optional=true)
   Publisher publisher
}

Now I have the Roo generated Controller and JSPX files. In the GUI the for creating and updating the Book there is the Roo generated Drop Down Box (decorated by dijit.form.FilteringSelect ) to select the Publisher. But the user MUST select a Publisher; there is no "Empty" field!

My first try was simply to add a null value to the list which represents the options for the drop downbox. But that failed. ( java.lang.IllegalArgumentException: Bean object must not be null ) -- So this may be the wrong way.

So before I try to extend the select.tagx file by my own, I want to ask if someone have already solved that problem (having a optional drop downbox with Spring Roo/Dojo), or do I something completely wrong and it should work in normmal case with out implmenting something new?

I have found a solution, but this works only because my application is not a standard Roo application any more. Anyway, I will explain my solution, maybe someone find an way to adapt it for standard Roo applications.

The idea is to add an empty selection in the dropdown box when the required attribute is false . The main problem is that the dijti/dojo extension will not work correct if there is one option in the dropdown box without an value . So my solution was to give them for example the value "null" ( <option value="null></option> ). On server side one must change the converter that convert the database id (that is the normal value) to an Entity (by loading it from the database) a bit, so that it converts the String "null" to null instead of an entity.

But that is the problem with spring Roo. Roo uses the org.springframework.core.convert.support.IdToEntityConverter that is automatically registered (not documented https://jira.springsource.org/browse/SPR-7461 ) and will try to convert every object to an entity if the entity class as a static finder method. I have found no way to modify its behaviour.

But I personally have a lot of luck, because some time ago I changed my application that it does not have that static finder, so I have my own generic Id to entity converter that is easy to change. The converter converts String to Entity. If the String is "null" it returns null, else it converts the String to a number and load the Entity by this number/id.

For the view, it seams that one have to extend the select.tagx file.

The select.tagx file contains 12 different ways to fill the select box.

  • 6 of them are for multiple select, so they can stay like they are.
  • 2 of if the not multiple ones are for disabled form binding, there one must add this block right after the select tag

line 75, 130,

<c:if test="${not required}">
    <option value="null"></option>
</c:if>
  • the other 4 are a bit more complicated

...

<form:select id="_${sec_field}_id" items="${items}" path="${sec_field}" disabled="${disabled}" />
 ...
 <form:select id="_${sec_field}_id" items="${items}" path="${sec_field}" disabled="${disabled}" itemLabel="${sec_itemLabel}"/>
 ...
<form:select id="_${sec_field}_id" items="${items}" path="${sec_field}" disabled="${disabled}" itemValue="${fn:escapeXml(itemValue)}" />
 ...
<form:select id="_${sec_field}_id" items="${items}" path="${sec_field}" disabled="${disabled}" itemValue="${fn:escapeXml(itemValue)}" itemLabel="${sec_itemLabel}"/>

They one need to replace the complete tag by ( I will only demonstrate it for the last of that 4, but the other are similar, except one has to remove the itemVlaue and or itemLabel parameter )

<form:select id="_${sec_field}_id" path="${sec_field}" disabled="${disabled}">                          
    <c:if test="${not required}">                               
       <option value="null"></option>
    </c:if>
    <form:options items="${items}" itemValue="${fn:escapeXml(itemValue)}" itemLabel="${sec_itemLabel}"/>
</form:select>

Now it should work.


But it has a small flaw. If there is a Book with no Publisher, then the empty dropdown option will not have the select attribute. This is not so bad, because it is the top most option and will be displayed if no other option is selected.

If someone can not accept this flaw, then one way to handle this problem is to write an own jsp tag extending org.springframework.web.servlet.tags.form.Option (the class that does the spring option tag). There are only two things that one really need to change:

1) the method isSelected(Object resolvedValue) must return true if the bind status is null (so this method becomes really easy)

private boolean isSelected(Object resolvedValue) {
  BindStatus bindStatus = getBindStatus();  
  return bindStatus == null || bindStatus.getValue() == null || bindStatus.getActualValue() == null;
}

2) if the tag is rendered without or empty body (method renderDefaultContent ) the content of the rendered html option should be empty but not the value . So the second parameter of the renderOption(SpecialWay) method must be set fix to an empty string.

@Override
protected void renderDefaultContent(TagWriter tagWriter) throws JspException {
  Object value = this.pageContext.getAttribute(VALUE_VARIABLE_NAME);    
  renderOptionSpecialWay(value, "", tagWriter);
}

But because the isSelected method is private and can not be override, one must copy the renderOption (can rename it) and must change it so that it invokes the "new" isSelected method. The same must be done to the two methods renderDefaultContent and renderFromBodyContent because renderOption is private too.

So one came up with this class:

public class NullOptionTag extends OptionTag {

  @Override
  protected void renderDefaultContent(TagWriter tagWriter) throws JspException {
    Object value = this.pageContext.getAttribute(VALUE_VARIABLE_NAME);
    renderOptionSpecialWay(value, "", tagWriter);
  }

  @Override
  protected void renderFromBodyContent(BodyContent bodyContent, TagWriter tagWriter) throws JspException {
   Object value = this.pageContext.getAttribute(VALUE_VARIABLE_NAME);
   String label = bodyContent.getString();
   renderOptionSpecialWay(value, label, tagWriter);
  }

  private void renderOptionSpecialWay(Object value, String label, TagWriter tagWriter) throws JspException {
    tagWriter.startTag("option");
    writeOptionalAttribute(tagWriter, "id", resolveId());
    writeOptionalAttributes(tagWriter);
    String renderedValue = getDisplayString(value, getBindStatus().getEditor());
    tagWriter.writeAttribute(OptionTag.VALUE_VARIABLE_NAME, renderedValue);
    if (isSelected(value)) {
        tagWriter.writeAttribute("selected", "selected");
    }
    if (isDisabled()) {
        tagWriter.writeAttribute("disabled", "disabled");
    }
    tagWriter.appendValue(label);
    tagWriter.endTag();
  }

  private boolean isSelected(Object resolvedValue) {
    BindStatus bindStatus = getBindStatus();      
    return bindStatus == null || bindStatus.getValue() == null || bindStatus.getActualValue() == null;
  }
}

Next thing to do is add this class to an tag lib definition so that it can be used in the select.tagx

 <form:select id="_${sec_field}_id" path="${sec_field}" disabled="${disabled}">                         
   <c:if test="${not required}">                                
     <formExtension:nulloption value="null"></formExtension:nulloption>
   </c:if>
   <form:options items="${items}" itemValue="${fn:escapeXml(itemValue)}" itemLabel="${sec_itemLabel}"/>
</form:select>

There is another way of getting around this problem. My solution does not require changing the select.tagx file.

I made changes to two places.

Firstly, at the BookController , I had overwritten the populate method for this particular object, Publisher , for example.

@ModelAttribute("publisher")
public Collection<Publisher> populatePublisher() {
    Collection<Publisher> result = new ArrayList<Publisher>();

    // Add an empty item
    Publishertmp = new Publisher();
    tmp.setId(0L);
    result.add(tmp);
    result.addAll(Publisher.findAllPublisher());

    return result ;
}

It is important to set the Id to 0. If the Id is not 0, the first empty item will not be displayed.

Then I had overwritten the ApplicationConversionServiceFactoryBean

protected void installFormatters(FormatterRegistry registry) {

    registry.addConverter(new PublisherConverter());

    super.installFormatters(registry);
    // Register application converters and formatters 
}

static class PublisherConverter implements Converter<Publisher, String> {
    public String convert(Publisher publisher) {
        if (publisher.getId().intValue() == 0) {
            return "-- Not Selected --";
        }
        return new StringBuilder().append(publisher.toString());
    }
}

By changing the converter, you can see -- Not Selected -- on the first item. When the form is submitted to the controller, you just need to add code to identify if an empty publisher is selected and make adjustment to suit your logic. I believe that's about it.

You can leave the publisher drop down box as required = "true" and it will still work.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM