2

I'm creating an application where the user has to complete a series of steps sequentially. Within the controller I check if each step is complete or in progress and then based on this information, I add the style attributes to the ViewModel which are then passed to the view.

Here's the view with the steps: enter image description here

So in the controller I have the following code that is currently populating the view:

switch (WhatStep)
{
    case 1:
        ViewModel.WhatStep = 1;

        ViewModel.StepOneComplete = false;
        ViewModel.StepOnePanelColour = "info";
        ViewModel.StepOneStatusText = "<span class=\"text-info pull-right\"><strong>Next Step</strong></span>";
        ViewModel.StepOneCollapsedText = "open";

        ViewModel.StepTwoComplete = false;
        ViewModel.StepTwoPanelColour = "default";
        ViewModel.StepTwoStatusText = "<span class=\"text-default pull-right\"><strong>Upcoming</strong></span>";
        ViewModel.StepTwoCollapsedText = "collapsed";

        ViewModel.StepThreeComplete = false;
        ViewModel.StepThreePanelColour = "default";
        ViewModel.StepThreeStatusText = "<span class=\"text-default pull-right\"><strong>Upcoming</strong></span>";
        ViewModel.StepThreeCollapsedText = "collapsed";

        ViewModel.StepFourComplete = false;
        ViewModel.StepFourPanelColour = "default";
        ViewModel.StepFourStatusText = "<span class=\"text-default pull-right\"><strong>Upcoming</strong></span>";
        ViewModel.StepFourCollapsedText = "open";

        ViewModel.StepFiveComplete = false;
        ViewModel.StepFivePanelColour = "default";
        ViewModel.StepFiveStatusText = "<span class=\"text-default pull-right\"><strong>Upcoming</strong></span>";
        ViewModel.StepFiveCollapsedText = "collapsed";

        ViewModel.StepSixComplete = false;
        ViewModel.StepSixPanelColour = "default";
        ViewModel.StepSixStatusText = "<span class=\"text-default pull-right\"><strong>Upcoming</strong></span>";
        ViewModel.StepSixCollapsedText = "collapsed";

        break;
    case 2:
        ViewModel.WhatStep = 2;

        ViewModel.StepOneComplete = true;
        ViewModel.StepOnePanelColour = "success";
        ViewModel.StepOneStatusText = "<span class=\"text-success pull-right\"><strong>✔ Complete</strong></span>";
        ViewModel.StepOneCollapsedText = "collapsed";

        ViewModel.StepTwoComplete = false;
        ViewModel.StepTwoPanelColour = "info";
        ViewModel.StepTwoStatusText = "<span class=\"text-info pull-right\"><strong>Next Step</strong></span>";
        ViewModel.StepTwoCollapsedText = "open";

The controller goes on and on as the viewModel is populated for each step.

The html in view for each panel look like this:

<div class="panel panel-@Model.StepOnePanelColour">
    <div class="panel-heading">
        <h4 class="panel-title">
            <strong>Step One:</strong>
            <a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" class="@Model.StepOneCollapsedText"></a>
            @Html.Raw(Model.StepOneStatusText)
        </h4>
    </div>
    <div id="collapseOne" class="panel-collapse collapse" style="height: 0px;">
        <div class="panel-body">
            <p>Instructions here<p>
        </div>
    </div>
</div>

Although this approach is working fine, the controller contains over 500 lines of code, just to populate the viewModel depending on what step the user is at.

Is there a better approach to doing this?

Dez79
  • 527
  • 2
  • 9
  • 25
  • Consider an `enum` (e.g. `ViewModel.StepOne = Status.Complete` and then use the value of the enum to set a class name and use css for the styling (and use the psuedo `:before` and/or `:after` for the text) –  Jan 18 '17 at 12:12
  • Looks like id still have to do a lot of work in the controller for each step as there are other attributes to consider, The panel color, the panel text and also if the panel is collapsed or not. – Dez79 Jan 18 '17 at 12:26
  • 1
    All of those can be styled using css based on the class name - you only need one property (the enum). And you switch statements can simplified as well –  Jan 18 '17 at 12:28
  • And what goes in the `

    Instructions here

    `? Is that just some text, or are you generating form controls as well (I suspect all that can be simplified as well)

    –  Jan 18 '17 at 12:29
  • We're using the model.whatstep variable to insert a partialview that has different instructions depending on if the step is in progress or complete. These may just be text/form/ajax request. – Dez79 Jan 18 '17 at 12:38

2 Answers2

1

Not only could you reduce the controller code to just a few lines, you can significantly reduce the view code as well.

Rather than having a view models with multiple properties for each step, use a collection to represent the steps

public enum StepStatus
{
    Upcoming,
    Current,
    Complete
}
public class Step
{
    public int Number { get; set; } 
    public StepStatus Status { get; set; }
}

Then in the method which generates the view

public ActionResult Index(int currentStep)
{
    List<Step> model = new List<Step>();
    for (int i = 0; i < 10; i++) // generates 10 steps
    {
        model.Add(new Step() { Number = i + 1 });
    }
    // Set the status of the steps based on the current step
    for (int i = 0; i < currentStep; i++)
    {
        model[i].Status = StepStatus.Complete;
    }
    model[currentStep - 1].Status = StepStatus.InProgress;
    return View(model);
}

And the view simply uses a loop to generate each step

@model List<Step>
....
<div id="accordion">
    @for (int i = 0; i < Model.Count; i++)
    {
        <div class="panel @Model[i].Status.ToString().ToLower()"> // adds class name based on the status
            <div class=" panel-heading">
                <h4 class="panel-title">
                    <strong>Step @Model[i].Number</strong>
                    @{ string className = Model[i].Status == StepStatus.Current ? "open" : "collapsed"; }
                    <a data-toggle="collapse" data-parent="#accordion" href="#step@(Model[i].Number)" class="@className"></a>
                    <span class="text-default pull-right">
                        @if (Model[i].Status == StepStatus.Complete)
                        {
                            <span class="glyphicon glyphicon-ok" aria-hidden="true"></span> // adds the tick mark
                        }
                        <strong>@Model[i].Status</strong>
                    </span>
                </h4>
            </div>
            <div id="step@(Model[i].Number)" class="panel-collapse collapse">
                <div class="panel-body">
                    <p>Instructions here<p>
                </div>
            </div>
        </div>
    }
</div>

and add some css to mimic the panel-succcess, panel-infoand panel-default bootstap classes

.complete {
    border-color: #d6e9c6;
}
.complete > .panel-heading {
    color: #3c763d;
    background-color: #dff0d8;
    border-color: #d6e9c6;
}
.current {
    border-color: #bce8f1;
}
.current > .panel-heading {
    color: #31708f;
    background-color: #d9edf7;
    border-color: #bce8f1;
}
.upcoming {
    border-color: #ddd;
}
.upcoming > .panel-heading {
    color: #333;
    background-color: #f5f5f5;
    border-color: #ddd;
}

You have not indicated how your rendering the content within the <div class="panel-body"> element, but this can simply be done using @Html.Action() if you want to include content for all steps, or using ajax to load the content as required. Using some conventions, the controller method could simply be

public PartialViewResult Step(int number, StepStatus status)
{
    var model = .... // get the model based on the step number
    string viewName = string.Format("_Step{0}{1}", number, status)
    return PartialView(viewName, model);
}

where your partials are named _Step1Complete.cshtml, _Step1Current.cshtml etc (I assume you have different content based on the status), and the to generate the content in the view

<div class="panel-body">
    @{ Html.RenderAction("Step", new { number = Model[i].Number, status = Model[i].Status }) // assumes the Step() method is in the same controller
</div>
  • Stephen, Thanks for taking the time to do this, its exactly what I was after. When I try to run the fiddle however i'm getting an error 'Fatal Error: Execution time limit was exceeded' – Dez79 Jan 19 '17 at 09:32
  • 1
    DotNetFiddle gets overloaded at times - just check it again a bit later (just checked and its fine just now) –  Jan 19 '17 at 09:36
  • 1
    Also, just a side note - I changed `In Progress` (2 words) to `Current` so I can use it in an `enum`. If you really do want `In Progress`, then you can use a `DisplayAttribute` on the enum value and use an extension method to generate the display text (the comunity wiki answer [here](http://stackoverflow.com/questions/388483/how-do-you-create-a-dropdownlist-from-an-enum-in-asp-net-mvc) shows how you could do that. I also assumed that the `open` class would only be applied to the current step but not sure if that what you need –  Jan 19 '17 at 09:45
0

I would suggest to do the styling via javascript or jQuery and CSS. By evaluating your ViewModel. Let the browser do the handling of UI styling which was designed to that.

johnpili
  • 688
  • 6
  • 9