简体   繁体   中英

How do I implement wizard type page navigation in an ASP.NET MVC 2 application?

I am using ASP.NET MVC 2 & .Net 3.5 with Visual Studio 2008.

Ok, what I am referring to by 'Wizard type page navigation', is a site where you have a list of stages in a given process or workflow. There is some kind of visual denotation to indicate which part of the stage you are at. I have already implemented this part (albeit, it smells like a hack) via the following:

css class current denotes active page. css class notcurrent denotes in-active page (ie page you are not on)

I declared the following method in a class called NavigationTracker.

public static String getCss(String val, String currView)
{
    String result = String.Empty;
    String trimmedViewName = currView.Substring(currView.LastIndexOf("/") + 1).Replace(".aspx", "");
    if (val.ToLower().Equals(trimmedViewName.ToLower()))
        result = "current";
    else
        result = "notcurrent";

    return result;
}

I have my stages in a control like this:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%@ Import Namespace="TheProject.Models" %>

<link href="../../Content/custom.css" rel="stylesheet" type="text/css" />
<% 
   String currentView = ((WebFormView)ViewContext.View).ViewPath;
%>
<table width="100%">
    <tr>       
        <td class="<%= NavigationTracker.getCss("LogIn",currentView)%>"  style="width:18%;">Log In</td>
        <td class="<%= NavigationTracker.getCss("YearSelect",currentView)%>"  style="width:18%;">Year Section</td>
        <td class="<%= NavigationTracker.getCss("GoalEntry",currentView)%>"  style="width:18%;">Goals</td>
        <td class="<%= NavigationTracker.getCss("AssessmentEntry",currentView)%>"  style="width:18%;">Assessment</td>
        <td class="<%= NavigationTracker.getCss("SummaryEntry",currentView)%>"  style="width:18%;">Summary</td>                
    </tr>
</table>

******************* THIS IS WHERE I NEED HELP ***********

To supplement this process, I'd like to create a user control that just has Previous & Next buttons to manage going through this process. So far, one snag I've hit is that this control cannot be put in the master page, but would have to be included in each view, before the form ends. I don't mind that so much. Clicking either the Previous or Next button submit the containing form to the appropriate action; however, I'm unsure on how to do the following:

1) Detect whether the Previous or Next button was clicked 2) Show/Hide logic of Previous & Next buttons at the beginning & end of the process respectively.

Another oddity I'm noticing with my application in general is that, after going through several pages of the process, if I click the back button, some values from my model populate on the page and others do not. For example, the text entered for a text area shows, but the value that had been chosen for a radio button is not selected, yet when inspecting the model directly, the appropriate object does have a value to be bound to the radio button.

I may just need to put that last part in a new question. My main question here is with the navigation control. Any pointers or tips on handling that logic & detecting whether Next or Previous was clicked would be most helpful.

EDIT 1

I had a thought to put a hidden field in the control that displays the Previous & Next buttons. Depending on what button was clicked, I would use JavaScript to update the hidden fields value. The problem now seems to be that the hidden field is never created nor submitted with the form. I've amended the controller post arguments to accept the additional field, but it never gets submitted, nor is it in the FormCollection.

Here is the code for the hidden field. Note that its being generated in a user control that is called inside of the form on the parenting view (hope that makes sense).

<% Html.Hidden("navDirection", navDirection); %>

Thoughts?

EDIT 2

I have resolved all of these issues. I will post the code in detail on Tuesday at the latest. The solution presented below will probably be selected as the answer as that got my on the right thought process.

In short, the solution was to have a Navigation class like the one suggested with logic to determine the next or previous page based on the current view & a string list of all views. A partial view / user control was created to display the Previous / Next buttons. The user control had 2 hidden fields: 1) One with the value of the current view 2) a field indicating navigation direction (previous or next). JavaScript was used to update the hidden navigation field value depending on what button was clicked. Logic in the user control determined whether or not to display the 'Previous' or 'Next' buttons depending on the first and last views in the wizard when compared to the current view.

All said, I'm pretty happy with the results. I'll probably find some code smell issues when I return to this, but, for now, it works.

Thank you all for your help!

EDIT 3 - SOLUTION

Here is the code for the control I built to display the 'Next' & 'Previous' navigation buttons:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%@ Import Namespace="Project.Models" %>
<link href="../../Content/custom.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" >
    function setVal(val) {
        var nav = document.getElementById("NavigationDirection");
        nav.value = val;
    }
</script>

<% 
   String currentView = ((WebFormView)ViewContext.View).ViewPath;    
   String navDirection = "empty";
   currentView = NavigationTracker.getShortViewName(currentView);
%>

<input type="hidden" value="<%= currentView %>" name="CurrentView" id="CurrentView" />
<input type="hidden" value="<%= navDirection %>" name="NavigationDirection" id="NavigationDirection" />

<% if( currentView != NavigationTracker.FirstPage) {  %>
    <div style="float:left;">        
        <input type="submit" value="Previous" onclick="setVal('previous')" />      <!-- On click set navDirection = "previous" -->
    </div>
<% } %>

<% if (currentView != NavigationTracker.LastPage)
   {  %>
<div style="float:right;">
    <input type="submit" value="Next" onclick="setVal('next')"  />              <!-- On click set navDirection = "next" -->
</div>    
<% } %>

From there, you render the control just before the closing tag of a form on views you want it like so:

<% Html.RenderPartial("BottomNavControl"); %>
    <% } %>

Now I can't really post all of the NavigationTracker code, but the meat of how it works can be deduced from the selected answer and the following snippet that returns the name of the view, based on the current view and the direction (previous or next).

public String NextView
{
    get
    {
        if (String.IsNullOrEmpty(this.NavigationDirection)) return string.Empty;
        int index = this.MyViews.IndexOf(this.CurrentView);
        String result = string.Empty;
        if (this.NavigationDirection.Equals("next") && (index + 1 < MyViews.Count ))
        {
            result = this.MyViews[index + 1];
        }
        else if (this.NavigationDirection.Equals("previous") && (index > 0))
        {
            result = this.MyViews[index - 1];
        }
        return result;
    }
}

Now, doing all of this has a few side effects that could easily be considered code smell. Firstly, I have to amend all of my controller methods that are marked [HTTPPOST] to accept a NavigationTracker object as a parameter. This object contains helper methods and the CurrentView & NavigationDirection properties. Once this is done, I can get the next view the same way in all of my actions:

return RedirectToAction(nav.NextView);

where nav is of type NavigationTracker.

Another note is that the FirstPage & LastPage properties of NavigationTracker are static so I'm actually using NavigationTracker.FirstPage in my global.asax.cs file for my routing. This means I can go to my NavigationTracker class and change the flow in one place for the entire application.

I would love any comments or criticisms on this solution. I admit this might not be a terrific solution, but at first glance, I'm pretty happy with it.

Hope it helps.

You should implement a strong typed view or control. In this type define a property indicating wich step you are and other logic. EX:

public class WizardView
{
     public List<string> Steps { get; set; }
     public int CurrentStepNumber {get;set;}

     public bool ShowNextButton
     {
         get
         {
             return CurrentStepNumber < this.Steps.Count-1;
         }
     }  

     public bool ShowPreviousButton
     {
         get
         {
             return CurrentStepNumber > 0;
         }
     }  
}

And your control:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<WizardView>" %>


    <table width="100%">
       <tr>    

         <% 
            int index=0;
            foreach(string step in Model.Steps)
            {
            %>   
                <td class='<%=Model.CurrentStep==index?"current":"notcurrent" %> style="width:18%;">
                       <%= step %>
               </td>
           <%
                index++;  
             } %>
        </tr>
</table>

In your controller

public ActionResult MyAction(int step)
{
    return View (new WizardControl {
       Steps = Myrepository.getSteps();
       CurrentStep = step
     });
}

}

this may not be exactly what you're looking for, but what if you use jQuery to hide and show DIV tags. Then the user will get the experience of a wizard while all content is all inside a single view. Then it can all be handled nicely with a single controller

Another option might be to create a separate view "step" for each wizard step

http://example.com/page/step/1

Then you can create a Class Object that contains ALL fields, and add to it as you navigate the wizard by adding to a Session Object

(Custom.Class.Obj)Session["myWizard"]

This way it allows you to build standard Views and load what information you have from the Session Object.

Here is another way to go - expanding on the WizardView approach. What you describe is a state engine - something aware of states, transitions and the behaviors and triggers associated with each.

If you check out an implementation like stateless ( http://code.google.com/p/stateless/ ), you'll see that you can desribe the states (steps in the wizard view approach) and the triggers associated with each. Based on your description, you could either capture this information in your state engine wire up - or perhaps ignore it altogether by the fact that all transitions are discretely handled by the engine.

To go a step further, your view wire up can now become quite generic. No need for the view to be aware of the state fulness of the operations, just relying on the view model itself.

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