Take the 2-minute tour ×
Stack Overflow is a question and answer site for professional and enthusiast programmers. It's 100% free, no registration required.

Basically, the form-backing object from a form submit is always null. Can anyone see why this is so?

I have a simple controller that displays a list of devices. The devices can be selected and functions be applied to them. This is the controller:

@Controller
@RequestMapping
public class DeviceListController {

    private static final Logger LOG = LoggerFactory.getLogger(DeviceListController.class);

    @Autowired
        DeviceService deviceService;

    @RequestMapping("/devices/list")
    public String getDevicesPage(ModelMap model) {
        int page = 0;
        int size = 10;

        if(model.get("deviceCommand") != null) {
            DeviceCommand cmd = (DeviceCommand) model.get("deviceCommand");
            page = cmd.getPage();
            size = cmd.getSize();
        }
        Pageable pageRequest = new PageRequest(page, size);
        Page<Device> devices = deviceService.findAllPaginated(pageRequest);

        model.addAttribute("devices", DeviceMapper.map(devices));
        model.addAttribute("deviceCommand", new DeviceCommand(page, size));
        return "devices";
    }

    @RequestMapping("/devices/modify")
    public String modifyDevices(ModelMap model) {

        LOG.debug("Trying to get the device command {}.", model.get("deviceCommand"));

        if(model.get("deviceCommand") != null) {
            DeviceCommand cmd = (DeviceCommand) model.get("deviceCommand");

            LOG.debug("Processing directives {} and {} for {}.", cmd.getNewVersion(),cmd.getNewCommand(),cmd.getDeviceModificationIds());

        }
        return getDevicesPage(model);
    }
}

The device model object is a hibernate entity:

@Entity //TODO:  no connection between device and device details!
@Table(name="device")
public class Device {
    @Id
    @GeneratedValue
    private Long id;

    /**
     * This is an external representation of the device, and the ID for which the device is most
     * commonly searched for. The deviceID is a common reference point, used in T2 and in device reporting
     * on the client side.
     */
    @Column(name="device_id")
    private String deviceId;

    /**
     * A reference to the store number or store identification. Stores may have more than one device.
     */
    @Column(name="retailer_id")
    private String retailerId;

    /**
     * The name of the store where the device is situated.
     */
    @Column(name="retailer_name")
    private String retailerName;

    /**
     * The current version of the client software, which launches the browser. This is only applicable to integrated solutions
     */
    @Column(name="current_client_version")
    private String currentClientVersion;

    /**
     * The target version of the client software, which launches the browser. This is only applicable to integrated solutions
     */
    @Column(name="next_client_version")
    private String nextClientVersion;

    /**
     * Commands to be performed on the client, used most often to "clear the cache". As soon as hte se commands are run, they
     * are cleared from this column.
     */
    @Column(name="commands")
    private String commands;


    @Column(name = "language")
    private String language;

    /**
     * The signature is the client's position in the marketplace and used by other entities
     * to determine their applicability to the device.
     * RETAILER_GROUP:Tobaccoland|CHANNEL:Logistik|LOCALE:de-AT|INDUSTRY:5499
     * The * notation means any, the locale is using the standard locale code (up to 10 characters)
     * and the industry uses the merchant category codes, which in this case is
     * Miscellaneous Food Stores - Convenience Stores and Specialty Markets
     */
    @Column(name = "signature")
    private String signature;  //TODO: normalise?

    /**
     * This is the traceId that the device is currently up to
     */
    @Column(name = "trace_id")
    private Long traceId;
    /**
     * This is a tracing number that will be generated by touchpoint on each request.
     */
    @Column(name = "last_trace_id")
    private Long lastTraceId;

    /**
     * This is the transaction number that will come from Mercury in each response.
     */
    @Column(name = "last_transaction_id")
    private Long lastTransactionId;

    @ManyToMany(cascade=CascadeType.ALL)
    @JoinTable(name="device_role",
        joinColumns = {@JoinColumn(name="device_id", referencedColumnName="id")},
        inverseJoinColumns = {@JoinColumn(name="role_id", referencedColumnName="id")}
    )
    private List<Role> roles;

    @ManyToMany(cascade= CascadeType.ALL, mappedBy = "devices", fetch = FetchType.LAZY)
    private Set<User> users;

    // getters, setters, equals, hashcode, omitted
}

and is mapped to a DeviceViewModel with a DeviceMapper

The DeviceViewModel:

public class DeviceViewModel {

    private String id;
    private String retailerName;
    private String currentClientVersion;
    private String nextClientVersion;
    private String commands;
    private boolean shallModify;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getRetailerName() {
        return retailerName;
    }

    public void setRetailerName(String retailerName) {
        this.retailerName = retailerName;
    }

    public String getCurrentClientVersion() {
        return currentClientVersion;
    }

    public void setCurrentClientVersion(String currentClientVersion) {
        this.currentClientVersion = currentClientVersion;
    }

    public String getNextClientVersion() {
        return nextClientVersion;
    }

    public void setNextClientVersion(String nextClientVersion) {
        this.nextClientVersion = nextClientVersion;
    }

    public String getCommands() {
        return commands;
    }

    public void setCommands(String commands) {
        this.commands = commands;
    }

    public boolean isShallModify() {
        return shallModify;
    }

    public void setShallModify(boolean shallModify) {
        this.shallModify = shallModify;
    }
}

and the DeviceMapper:

public class DeviceMapper {

    public static DeviceViewModel map(Device device) {
        DeviceViewModel dto = new DeviceViewModel( );
        dto.setId( device.getDeviceId() );
        dto.setRetailerName( device.getRetailerName() );
        dto.setCurrentClientVersion( device.getCurrentClientVersion() );
        dto.setNextClientVersion( device.getNextClientVersion() );
        dto.setCommands( device.getCommands() );
        return dto;
    }

    public static List<DeviceViewModel> map(Page<Device> devices) {
        List<DeviceViewModel> dtos = new ArrayList<>();

        for(Device device : devices) {
            dtos.add( map( device ) );
        }

        return dtos;
    }
}

and now the DeviceCommand, which is my form-backing object:

public class DeviceCommand {

    private List<String> deviceModificationIds;
    private String newVersion;
    private String newCommand;
    private int page;
    private int size;

    public DeviceCommand() {}

    public DeviceCommand(int page, int size) {
        this.page = page;
        this.size = size;
    }

    public List<String> getDeviceModificationIds() {
        return deviceModificationIds;
    }

    public void setDeviceModificationIds(List<String> deviceModificationIds) {
        this.deviceModificationIds = deviceModificationIds;
    }

    public String getNewVersion() {
        return newVersion;
    }

    public void setNewVersion(String newVersion) {
        this.newVersion = newVersion;
    }

    public String getNewCommand() {
        return newCommand;
    }

    public void setNewCommand(String newCommand) {
        this.newCommand = newCommand;
    }

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }
}

and finally the relevant part of the devices.html page:

<form action="#" th:action="@{/devices/modify}" th:object="${deviceCommand}" method="post">
<table class="box-table-a">
    <h1 th:text="#{device.table.caption}">Site Users</h1>
    <thead>
        <tr>
            <th scope="col" th:text="#{device.check.label}">Select</th>
            <th scope="col" th:text="#{device.id.label}">(<span th:text="${device.retailer.name.label}"></span>)</th>
            <th scope="col" th:text="#{device.current.label}">Curr Version</th>
            <th scope="col" th:text="#{device.next.label}">Next Version</th>
            <th scope="col" th:text="#{device.commands.label}">Commands</th>
        </tr>
    </thead>
    <tbody>
        <tr th:each="d : ${devices}">
            <td><input type="checkbox" th:field="*{deviceModificationIds}" th:value="${d.id}"/></td>
            <td th:text="${d.id}">(<span th:text="${d.retailerName}"></span>)</td>
            <td th:text="${d.currentClientVersion}">Washington</td>
            <td th:text="${d.nextClientVersion}">gwash</td>
            <td th:text="${d.commands}">gwash</td>
        </tr>
        <tr>
        <td colspan="2">

            </td>
            <td th:text="#{device.change.version.label}"></td>
            <td th:text="#{device.add.command.label}"></td>
            <td></td>
        </tr>
        <tr>
        <td colspan="2">

            </td>
            <td><input type="text" th:field="*{newVersion}"/></td>
            <td><input type="text" th:field="*{newCommand}"/></td>
            <td><button type="submit" th:text="#{device.modify.action.button}">Action</button></td>
        </tr>
    </tbody>
</table>
</form>

Although the form action, does reach the modifyDevices method in the controller, but the deviceCommand form-backing object is null.

why is this so?

share|improve this question

1 Answer 1

up vote 1 down vote accepted

just add a parameter of type DeviceViewModel to your modifyDevices method
so it will be like this

@RequestMapping("/devices/modify")
public String modifyDevices(ModelMap model, DeviceViewModel deviceCommand ) {
/* access the submitted object as you want ....*/
}

you may find this question How to pass two objects to use in a form using thymeleaf? useful

share|improve this answer
    
brilliant - that's the answer. Actually, I have just discovered the wonders of the @ModelAttribute annotation –  Michael Coxon yesterday

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.