Announcement Announcement Module
Collapse
No announcement yet.
How to configure a global transition for a back button? Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • How to configure a global transition for a back button?

    Hello,

    I'd like to configure a global transition for a form-based "back"-button. Is that possible?


    Best

    Oliver

  • #2
    It all depends on what 'Back' is supposed to do. Should back go to a fixed url or to the 'previous view state'? The latter is easy, just include a global transition to go where you need to go when the back event is signaled. The former is a bit harder. You'll have to track the view states you hit in your flow, e.g. using a FlowExecutionListener, and store the 'history' in flow scope. You could then have a global transition that looks something like this:

    <transition on="back" to="${flowScope.history.previousViewStateId}"/>

    You'll need to do some custom coding (the FlowExecutionListener and the history object to store in flow scope) to make this possible.

    Erwin

    Comment


    • #3
      Yes, I want the 'back' button to go to the 'previous view state'. Wouldn't that (an automatically defined '${flowScope.history.previousViewStateId}') be another cool feature for a new SWF version? I think the need for a 'back' button is pretty common in form wizards, isn't it?


      Best

      Oliver

      Comment


      • #4
        A universal definition for 'back' is not that common. What is the 'previous view state' really? Is it the last view state entered in the flow or the previous 'step' in the flow or something else still? How does this play with subflows?

        Questions like this have so far prevented us from implementing something generic enough to be in SWF itself.

        Erwin

        Comment


        • #5
          Originally posted by klr8 View Post
          A universal definition for 'back' is not that common. What is the 'previous view state' really? Is it the last view state entered in the flow or the previous 'step' in the flow or something else still?
          In my opinion it is the last view state entered in the flow. But there might be other cases I currently don't know of.

          Originally posted by klr8 View Post
          How does this play with subflows?
          I don't know ;-) - I haven't used subflows so far.

          For all other SWF users that need a simple 'global back to last view FlowExecutionListener' - here it is:

          Code:
          import java.util.LinkedList;
          
          import org.springframework.webflow.definition.StateDefinition;
          import org.springframework.webflow.engine.ViewState;
          import org.springframework.webflow.execution.FlowExecutionListenerAdapter;
          import org.springframework.webflow.execution.FlowSession;
          import org.springframework.webflow.execution.RequestContext;
          
          public class BackToLastViewStateFlowExecutionListener
              extends FlowExecutionListenerAdapter
          {
          
              private String viewStatesName = "GLOBAL_BACK_LISTENER_VIEW_STATES";
              private String backEventId = "back";
          
              public void setViewStatesName(String viewStatesName)
              {
                  this.viewStatesName = viewStatesName;
              }
          
              public void setBackEventId(String backEventId)
              {
                  this.backEventId = backEventId;
              }
          
              @Override
              public void sessionStarted(final RequestContext context,
                                         final FlowSession session)
              {
                  session.getScope().put(viewStatesName, new LinkedList<String>());
              }
          
              @Override
              public void stateEntered(final RequestContext context,
                                       final StateDefinition previousState,
                                       final StateDefinition state)
              {
                  if (!(previousState instanceof ViewState))
                      return;
          
                  @SuppressWarnings("unchecked")
                  final LinkedList<String> viewStates =
                      (LinkedList<String>)context.getFlowScope().get(viewStatesName);
          
                  if (viewStates == null)
                      throw new IllegalStateException("viewStates is null");
          
                  final String previousStateId;
          
                  if (context.getLastEvent().getId().equals(backEventId))
                  {
                      viewStates.removeLast();
                      previousStateId = viewStates.getLast();
                  }
                  else
                  {
                      previousStateId = previousState.getId();
                      viewStates.add(previousStateId);
                  }
          
                  context.getFlowScope().put("previousViewStateId", previousStateId);
              }
          
          }

          BTW: Any tips for improvement?


          Best

          Oliver

          Comment


          • #6
            Bugfix

            The last if then else should read

            if (context.getLastEvent().getId().equals(backEventId ))
            {
            previousStateId = viewStates.getLast();
            viewStates.removeLast();
            }
            else
            {
            previousStateId = previousState.getId();
            viewStates.add(previousStateId);
            }

            In the first compound statement if you remove the last state and then attempt to access is (as was happening in the previous version) you will get an exception.

            wm

            Comment


            • #7
              Originally posted by wee_malky View Post
              In the first compound statement if you remove the last state and then attempt to access is (as was happening in the previous version) you will get an exception.
              No, that's not right. Example:

              Code:
              LinkedList<String> l = new LinkedList<String>();
              
              l.add("A");
              l.add("B");
              l.add("C");
              
              System.out.println(l.removeLast());
              System.out.println(l.getLast());
              That will print C and then B because LinkedList's internal iterator will be adjusted when removeLast() is called.

              My code will only throw an exception if you have a back button on the first page because after the removeLast() there's no element left. If you change the order of the removeLast() and getLast() calls (as you did), then code shouldn't work anymore, does it?

              Anyway, thanks for the review!


              Best

              Oliver

              Comment


              • #8
                Fixed version

                Oliver,

                Quite right. Some wooly thinking on my behalf. As you move back you remove the last 'previous' entry so that the remaining one is the new valid value.

                I have another refinement for you. When you are on a form and you enter invalid values the bindAndValidate will take you back to the same page with the error messages displayed. This transition will of course get added into the viewStates list. Oh dear. Pressing the back button on current form-with-errors will take you back to the form-without-errors.

                To fix this we must catch the transition to form-with-errors page and remove the incorrect previous state (which would have taken us back to form-without-errors). No better way to explain than to produce some code, voila!

                Code:
                    @Override
                    public void stateEntered(final RequestContext context,
                                             final StateDefinition previousState,
                                             final StateDefinition state)
                    {
                        @SuppressWarnings("unchecked")
                        final LinkedList<String> viewStates = (LinkedList<String>)context.getFlowScope().get(viewStatesName);
                        
                        String previousStateId = null;
                        
                        if (previousState instanceof ViewState)
                        {
                            // Back button pressed
                            if (context.getLastEvent().getId().equals(backEventId))
                            {
                                if (!viewStates.isEmpty())
                                { 
                                    viewStates.removeLast();
                                    if (!viewStates.isEmpty()) previousStateId = viewStates.getLast();
                                }
                            }
                            else
                            {
                                previousStateId = previousState.getId();
                                viewStates.add(previousStateId);
                            }
                            
                            log.info("previousStateId = " + previousStateId);
                            if (previousStateId!=null) context.getFlowScope().put("previousViewStateId", previousStateId);
                        }
                        else 
                            if (state instanceof ViewState && viewStates!=null)
                            {
                                if (!viewStates.isEmpty())
                                {
                                    // If we an re-entering a state which is already stored then roll back 
                                    if (state.getId().equals(viewStates.getLast()))
                                    {
                                        viewStates.removeLast();
                                        if (!viewStates.isEmpty()) previousStateId = viewStates.getLast();
                                        
                                        log.info("previousStateId = " + previousStateId);
                                        if (previousStateId!=null) context.getFlowScope().put("previousViewStateId", previousStateId);
                                    }
                                }
                            }
                    }
                I _think_ this is last piece in puzzle of how to implement a proper wizard flow.

                WM

                Comment


                • #9
                  Originally posted by wee_malky View Post
                  I have another refinement for you. When you are on a form and you enter invalid values the bindAndValidate will take you back to the same page with the error messages displayed. This transition will of course get added into the viewStates list. Oh dear. Pressing the back button on current form-with-errors will take you back to the form-without-errors.
                  Oh, thanks for pointing that out.

                  What do you think about this (somewhat less complex) code to solve this. Have I overlooked something?

                  Code:
                      private LinkedList<String> getViewStates(final RequestContext context)
                      {
                          @SuppressWarnings("unchecked")
                          final LinkedList<String> viewStates =
                              (LinkedList<String>)context.getFlowScope().get(viewStatesName);
                  
                          if (viewStates == null)
                              throw new IllegalStateException("viewStates is null");
                  
                          return viewStates;
                      }
                  
                      @Override
                      public void stateEntered(final RequestContext context,
                                               final StateDefinition previousState,
                                               final StateDefinition state)
                      {
                          // If there's no previous ViewState, there's nothing we can do...
                          if (!(previousState instanceof ViewState))
                              return;
                  
                          // If we're entering the same state (due to a reload or
                          // binding error), ignore that...
                          if (previousState.getId().equals(state.getId()))
                              return;
                  
                          final LinkedList<String> viewStates = getViewStates(context);
                  
                          final String previousStateId;
                  
                          if (context.getLastEvent().getId().equals(backEventId))
                          {
                              viewStates.removeLast();
                              previousStateId = viewStates.isEmpty() ? "" : viewStates.getLast();
                          }
                          else
                          {
                              previousStateId = previousState.getId();
                              viewStates.add(previousStateId);
                          }
                  
                          context.getFlowScope().put("previousViewStateId", previousStateId);
                      }
                  Best

                  Oliver

                  Comment


                  • #10
                    Good stuff guys!
                    Might be a good idea to attach this to a JIRA issue so that it's available for everybody and for possible inclusion in a future version of SWF!

                    Erwin

                    Comment


                    • #11
                      Originally posted by klr8 View Post
                      Good stuff guys!
                      Might be a good idea to attach this to a JIRA issue so that it's available for everybody and for possible inclusion in a future version of SWF!

                      Erwin
                      Done - http://opensource.atlassian.com/proj...browse/SWF-238


                      Best

                      Oliver

                      Comment


                      • #12
                        Useful for subflows

                        Thanks for this guys. Erwin previously asked how a this would behave with subflows.

                        Well that's exactly what I'm using it for, and it works a treat.

                        Our requirement is to have a cancel button on any page, which then prompts you to confirm cancellation. The "confirm cancel" page has two buttons, "yes, please cancel" and "don't cancel". The "don't cancel" button must return you to where you left off in the flow (i.e. previous view state).

                        I think this is a pretty common requirement. Are there any plans for common subflows to be included in SWF ?

                        So I've modelled this cancellation as a subflow. There are two global transitions: one ("cancel") which enters the cancellation subflow, and one ("continuedApplication") which restores the previous state on "don't cancel". I've been a bit verbose, on the assumption that somebody else might find it useful. Please ignore the "HTML Code" marker, it's just xml but easier to read than with the [ CODE / ] tags:

                        HTML Code:
                            <!-- Enter a subflow when the user hits "cancel", so that if they
                                 change their mind at the "confirm cancel" page, we can bring
                                 them back to where they left off just by ending the subflow. -->
                            <subflow-state id="confirmCancel" flow="cancel-flow" >
                                <!-- If the user confirmed cancellation, transfer to the "cancel"
                                     end state. -->
                                <transition on="confirmedCancellation" to="cancelState"/>
                            </subflow-state>
                        
                            <!-- end state for when user has confirmed cancellation -->
                            <end-state id="cancelState" view="cancelConfirmed"/>
                        
                            <global-transitions>
                                <transition on="cancel" to="confirmCancel"/>
                                <transition on="continuedApplication" to="${flowScope.previousViewStateId}"/>
                            </global-transitions>
                        
                        <!-- subflow for cancellation.  prompt the user, and fire the appropriate
                             end-state event based on which button the user has pressed -->
                            <inline-flow id="cancel-flow" >
                                <start-state idref="promptConfirmCancel" />
                                <view-state id="promptConfirmCancel" view="cancelApplication">
                                    <transition on="confirmCancelFlow" to="confirmedCancellation" />
                                    <transition on="continuedApplication" to="continuedApplication" />
                                </view-state>
                                <end-state id="continuedApplication" />
                                <end-state id="confirmedCancellation" />
                            </inline-flow>
                        This is configured using Oliver and wee_malkey's BackToLastViewStateFlowExecutionListener , as below:
                        HTML Code:
                            <!-- Launches new flow executions and resumes existing executions. -->
                            <flow:executor id="flowExecutorApplication" registry-ref="flowRegistryApplication" repository-type="continuation">
                                <flow:execution-attributes>
                                    <flow:alwaysRedirectOnPause value="true"/>
                                </flow:execution-attributes>
                                <flow:execution-listeners>
                                    <flow:listener ref="listener" criteria="application-flow"/>
                                </flow:execution-listeners>
                            </flow:executor>
                        
                            <bean id="listener" class="com.mycompany.listeners.webflow.BackToLastViewStateFlowExecutionListener"/>
                        Interestingly, the listener seems to be ignoring the criteria, and applying itself to other flow IDs (maybe a bug in FlowIdFlowExecutionListenerCriteria ?). No great issue for me, but something worth noting.
                        Last edited by cunneen; Jan 16th, 2007, 06:25 PM. Reason: added config.

                        Comment


                        • #13
                          Are there any plans for common subflows to be included in SWF ?
                          What exactly do you mean with 'common subflows'? You can ofcourse just define your 'common subflows' as top-level flows and reference them from several 'parent flows'.

                          Erwin

                          Comment


                          • #14
                            Interestingly, the listener seems to be ignoring the criteria, and applying itself to other flow IDs (maybe a bug in FlowIdFlowExecutionListenerCriteria ?). No great issue for me, but something worth noting.
                            Are you sure about that? I tried to reproduce this and for me (with SWF 1.0.1) it's working correctly.

                            Erwin

                            Comment


                            • #15
                              Originally posted by klr8 View Post
                              What exactly do you mean with 'common subflows'? You can ofcourse just define your 'common subflows' as top-level flows and reference them from several 'parent flows'.

                              Erwin
                              Yes, this is exactly what I have done.

                              My example is of a wizard, from which the user can press a "cancel" button at any time. This enters a "confirm cancel" subflow.

                              A "confirm cancel" subflow is one example of a subflow that I imagine to be quite common.

                              Comment

                              Working...
                              X