Announcement Announcement Module
Collapse
No announcement yet.
WebFlow idea. Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • WebFlow idea.

    I am using web-flow and have found some moments I think should be noted.

    1. Looking at the web-flow DTD I saw, that the only strictly web stuff in it is actualy the name. If the web-flow is changed to spring-flow nothing changes, and the idea of the fact, that this flow has very little web-specific stuff, and MAY be later used even for RCP spring.

    2. I have had quite some problems with the multi-submit, resubmit and all those problems having to do with the fact, that after a POST request the result is directly rendered in the same connection without a redirect.
    My thought/proposal is to add support to the WebFlowController to 'render' the current 'view-state' view if no _action is specified, and after a succesfull call to the servlet it should 'redirect' to ITSELF with just the flow_id specified, and then just render (forward to) the 'current state' view.
    The fact is, that any call to the WebFlowServlet must at some point reach a view-state that will render the user-viewable page. Having this as a result of a POST request is quite annoying, just try to 'refresh'.

  • #2
    1. We have been making the web flow system a bit more abstract the last few days, so you are right: there is not to much web specific stuff left. Still, we originally designed it as a 'page flow system for web applications', and certainly not as 'a general purpose work flow (BPM) system'.

    2. Back button use and refresh are known limitations of the current web flow system. Regarding your suggestion to implement redirect-after-post: I had the same idea and it works great for normal view states. The problem is with a view rendered in an end state: in this case the flow ends during the POST request (because it reached an end state), so it will no longer be there to handle the GET that will follow to display the view...
    So far we haven't really come up with an elegant solution to this problem.

    Erwin

    Comment


    • #3
      For 2. and limitations of the back button...

      I haven't looked at WebFlow, but in other systems I've worked on that had workflow, the only approach that seems to deal with clicks of the back button is to embed the navigation/workflow state in the page.

      That way, the first thing that happens on submit is to see which page the user has submitted in the flow, and you can check if the page submitted is the one you were expecting.

      We just created a tag and put it in the std includes for the page so that navigation state was always hidden in the page.

      Comment


      • #4
        Maybe a redirect state?

        I guess one solution to the redirect-after-post problem would be adding a new state type that does nothing but redirect to one of its outgoing transitions. Overkill? Maybe, but it would fix the problem and would allow the user to see the places in the flow where redirects occur. I guess it would serve the purpose of documenting external redirects as well. Now that states use a context rather than the request/response directly, is it still possible to force a redirect from within a state?

        In regard to the previous post, I think a standard webflow taglib that stores flow/state context as hidden vars is a good idea. +1

        Thanks,
        Derek

        Comment


        • #5
          Regarding the comment gmatthews made: embedding the "current state id" in every page and thus submitting it in every request largely solves the back button/refresh problem when you have a single flow that does not use subflows. The web flow system already provides this. However, this does not solve the problem in all possible situations, and certainly not when you have subflows.
          Still, we are already putting it forward as a best practice to solve some of the back button/refresh related issues.


          Regarding the "redirect" state type suggested by dadams: it would work but it seems a bit iffy since normally the flow is not aware of POST or GET requests, so it seems strange that it would need to explicitly indicate where a redirect is required. It would be sweet if the web flow system automatically detects a POST (that's simple), and applies redirect-after-post in that case (not so simple).
          Also, you can still access the HttpServletRequest/HttpServletResponse from the RequestContext object by doing:
          Code:
          HttpServletRequest request=((HttpServletRequestEvent)context.getOriginatingEvent()).getRequest();
          HttpServletResponse response=((HttpServletRequestEvent)context.getOriginatingEvent()).getResponse();
          Erwin

          Comment


          • #6
            this does not solve the problem in all possible situations, and certainly not when you have subflows.
            You're obviously the expert on this, but I was thinking along the lines of what JSF does where the hidden "state" field isn't necessarily simple, but could be quite complex depending on what state needed to be embedded in the UI.

            You'd also need a WebflowStates.parse(myHiddenField) and webflowStates.toCanonicalForm() method or something??

            The effort involved would be in creating some notation that handled representing all currently active flows, and then making sure you can parse it back into some representation you can make sense of.

            Comment


            • #7
              General exception handler

              Capturing too much state on the client might be a bad idea. To truly capture the complete state you would have to serialize the entire flow execution stack including all of the attributes stored in flow scope. I do think that every view should send the flow id and state id so the engine can maintain context.

              I would be content with the flow engine recognizing that you have tried to transition to an illegal flow or state and throwing an InvalidStateException. There could be a default state for a flow that handles state exceptions by intelligently routing to a place that makes sense. For instance: I execute a search, see the results and exit the flow. I then use the back button to go back to a search results page in the dead flow. The engine would throw an exception, the error state would catch it and route to the search criteria page.

              Is that a viable option?

              Thanks,
              Derek
              dadams (at) gaijin-studio.org

              Comment


              • #8
                The ideas in the 2 posts above are both very interesting and warrant further investigation!

                Right now our priority is getting a web flow preview release ready to make it easy for people to give it a try.
                Once that is done, we will start looking into "back button/refresh" handling and how best to "support" it.

                Erwin

                Comment


                • #9
                  Originally posted by ev9d9
                  Once that is done, we will start looking into "back button/refresh" handling and how best to "support" it.
                  Erwin
                  Ajaxian components largely take care of this problem, and it will be interesting to see how spring handles this problem. Current component web frameworks (Echo2, Tapestry) are starting to support Ajax components, and are able to cope with several common web specific problems:
                  * double submit
                  * refreshing
                  * back buttons

                  Comment


                  • #10
                    I can handle most back button issues, with normal flows and subflows, by embedding the state in the page and using the following logic in a subclass of FlowController:

                    Code:
                        /**
                         * @see org.springframework.web.servlet.mvc.AbstractController#handleRequestInternal(javax.servlet.http.HttpServletRequest,
                         *      javax.servlet.http.HttpServletResponse)
                         */
                        protected ModelAndView handleRequestInternal(
                                final HttpServletRequest request, final HttpServletResponse response)
                                throws Exception {
                            ModelAndView mv = null;
                            try {
                                mv = manager.handleRequest(request, response);
                            } catch (NoSuchFlowStateException e) {
                                mv = getFallbackState(request, response, e);
                            } catch (EventNotSupportedException e) {
                                mv = getFallbackState(request, response, e);
                            }
                            return mv;
                        }
                    
                        /**
                         * Handles back button behaviour a little more elegantly. If the requested
                         * action cannot be found in the active flow, we fall back to the parent.
                         * If it cannot be found in any parent flow, the action must belong to a
                         * closed subflow. Therefore, we try and display the current page again.
                         * First we test if there is an error condition which we can signal. If not
                         * we redisplay the current view. If that is not possible, then we cannot
                         * fallback.
                         *
                         * @param request the request
                         * @param response the response
                         * @param cause the cause
                         * @return the resulting model and view
                         * @throws Exception if fallback fails
                         */
                        private ModelAndView getFallbackState(final HttpServletRequest request, final HttpServletResponse response,
                                final Exception cause) throws Exception {
                            FlowExecution execution = manager.getRequiredFlowExecution(request);
                            if (execution instanceof FlowExecutionStack) {
                                FlowExecutionStack stack = (FlowExecutionStack)execution;
                                if (!stack.getActiveFlow().equals(stack.getRootFlow())) {
                                    if (logger.isDebugEnabled()) {
                                        logger.debug("Closing current flow and attempting to resolve in parent.");
                                    }
                                    // End the current flow and recursively invoke the same action
                                    // on the parent flow
                                    stack.endActiveSession();
                                    return handleRequestInternal(request, response);
                                }
                            }
                            return fallbackToCurrentView(request, response, cause, execution);
                        }
                    
                        /**
                         * Try and display the current page again.
                         * First we test if there is an error condition which we can signal. If not
                         * we redisplay the current view. If that is not possible, then we cannot
                         * fallback.
                         *
                         * @param request the request
                         * @param response the response
                         * @param cause the cause
                         * @param execution he current flow execution
                         * @return the resulting model and view
                         * @throws Exception if fallback fails
                         */
                        private ModelAndView fallbackToCurrentView(final HttpServletRequest request, final HttpServletResponse response,
                                final Exception cause, final FlowExecution execution) throws Exception {
                            String event = execution.getLastEventId();
                            AbstractState state = execution.getCurrentState();
                    
                            if (state instanceof TransitionableState) {
                                if (((TransitionableState)state).supportsEvent("error")) {
                                    if (logger.isDebugEnabled()) {
                                        logger.debug("Unable to fallback to parent, execution error condition.");
                                    }
                                    return execution.signalEvent("error", state.getId(), request, response);
                                }
                            }
                    
                            if (state instanceof ViewState) {
                                if (logger.isDebugEnabled()) {
                                    logger.debug("Unable to fallback to parent, returning current view.");
                                }
                                return new ModelAndView(((ViewState)state).getViewName(), execution.getModel());
                            }
                            if (logger.isDebugEnabled()) {
                                logger.debug("Unable to handle flow request, starting a new flow.", cause);
                            }
                            return manager.handleRequest(new NewFlowWrapper(request), response);
                        }
                    The basic procedure is to retry the failed action in the current subflow, and all parents, and then try and find some kind of fallback to the current state in the root flow. It works for most situations that affect me. It would be nice to be able to inspect parent flows without closing the current one - otherwise this will only display the current state in the root flow, and close all child subflows below the root. My flows are never nested more than one deep, but if subflows open other subflows this logic will not work terribly well. Anybody know of a way to inspect parent flows without closing children first? Maybe a FlowExecutionLinkedList, rather than a stack.

                    Note that more recent versions of Webflow than the one I am using actually indicate the failed state in the NoSuchState/NotSupported exceptions that are thrown, so this logic could now be improved a lot - the section where I manually call an 'error' action works for me, because I always have one, so your mileage may vary.

                    Comment


                    • #11
                      This "end current flow and retry in parent" logic is indeed supprisingly robust. The original Ervacon web flow system provided a "SubFlowBackNavigationExceptionResolver" that also implemented this.

                      Anyway, it's still not a 100% solution and it does feel a bit "dirty". That's why we didn't add anything like that so far.

                      Erwin

                      Comment


                      • #12
                        Just letting you guys know that we have added continuations support to the web flow system recently. This should be included in the preview 2 release that's coming in a few days.

                        (For those not aware of continuations, read http://www-128.ibm.com/developerwork.../j-contin.html.)

                        Continuations allow you to make the flow 100% robust during free navigation (refresh, back button, ...), even when using sub flows. They also make it possible to do cool things like have multiple browser windows open in the same flow (e.g. using CTRL-N)!

                        We now have a system with pluggable flow execution storage strategies, so all of this is completely pluggable:

                        * classic storage in the HttpSession (like in SWF preview 1)
                        * server side continuations (stored in the HttpSession)
                        * client side continuations (stored on the client)
                        * ...

                        And all of this even without any impact on the pages! Each flow in your app can also have a different storage strategy!

                        So far we have only found 1 issue with this: application transactions don't work anymore when you're using continuations. We're not sure yet how we're going to tackle this. You could even argue "free browsing -- made possible by continuations -- is not compaticle with application transaction", so basically not supporting it. An advaced continuations storage strategy might also be able to work around the issue (e.g. clearing the token in all 'cloned' continuations in the storage when an application transaction ends).

                        Any input you guys might have would be much appreciated!

                        Erwin

                        Comment

                        Working...
                        X