Telerik blogs
BlazorT4_1200x303

Some of our difficult tasks involve volatile processes where we add or remove steps. See how to do that, how to disallow users from jumping around, and how to integrate process code.

As I discussed in an earlier post, one of the characteristics that make a task “difficult” is that the task is volatile—the items involved in the task change while the user is performing the task. One of the benefits of using the Stepper component from the Telerik UI for Blazor components is that it allows you to add or remove steps from your process either at the start of the process or even while the user is working through the process.

In this post, we’ll look at changing the process (thus renumbering our steps), when that might make you want to disallow users from choosing what step they go to next, and how to integrate process code no matter what the user is allowed to do.

Creating Dynamic Processes With the Stepper

Changing the process while the user is performing isn’t something that you should do without considering the impact on the user. The point of breaking a difficult task down into steps is to not just to simplify the task but to give the user a map of the process. This stepper, for example, gives the user an overview of the process and, thanks to the labels, a clue about what happens in each step:

 A five step process. Each circle in the step has an icon. The first step has the information icon (the lower case letter i inside a circle), the second step has a calendar, the third step has a link in a chain, the fourth step has an open folder, and the fifth step has a disk. Each step also has a label below it: “Overview” for step 1, “Pick Date Range” for step 2, “Select Occurrences” for step 3, “Define Case” for step 4, and “Save Changes” for Step 5.

Changing the process while the user is performing the task may actually make the process harder to understand: Not only is the task volatile but, now, so is the process.

Having said that, your goal in creating a process is to match the user’s mental model of how the “difficult” task should be handled. If the user’s mental model supports adding or removing steps, well, then, you have a compelling argument for supporting that in your Stepper. And, in fact, there’s at least a couple of common UI design patterns where you do want to be able to dynamically alter the steps in the process.

Generating the Steps at Runtime

For complete freedom in building your process in Blazor apps, you can build your Stepper dynamically at run time by adding its StepperSteps from code. You could, for example, retrieve a set of objects from some data source with each object holding the data you need to configure a StepperStep.

This example just uses a collection of strings to set the Label attribute on the StepperStep elements it creates (I’ve also used the StepType attribute on the TelerikStepper element to configure the stepper to only show labels):

<TelerikStepper StepType="@StepperStepType.Labels" @bind-value="currentStep" >
    <StepperSteps>
        @foreach(string lbl in Labels)
        {
            <StepperStep Label="@lbl"></StepperStep>
        }
        </StepperSteps>
</TelerikStepper>

In my code section, all I need is a collection of strings that will be used as the labels in each step:

string[] Labels;

protected override Task OnAfterRenderAsync(bool firstRender)
{
    Labels = WizardRepo.GetStepsByWizardName("BuildCase");
    return base.OnAfterRenderAsync(firstRender);
} 

Adding Steps

Generating all the steps at run time is probably a more radical solution than you need. Assuming that you need a dynamic process at all, you’ll typically just want to add one or two steps to handle special cases in the process.

There’s at least one “special case” that’s pretty common: Processes often begin with an “overview” step that describes the process to the user who is unfamiliar with it. Typically, these steps include a “Don’t show this again” checkbox to suppress that overview once the user is familiar with the process.

You can implement that UI design pattern just enclosing the affected StepperStep elements in an if block. This Razor code, for example, only displays the step with the “Overview” label when the overviewRequired field is set to true:

<StepperSteps>    
        @if (overviewRequired)
            {
                 <StepperStep Label="Overview" 
                              Icon="info-circle" ></StepperStep>
            }    

The bound overviewRequired field might look like this and be updated when the component is displayed with code in the OnAfterRenderAsync event:

bool overviewRequired = true;
protected override Task OnAfterRenderAsync(bool firstRender)
{
    overviewRequired = WizardRepo.GetUserSettingsByWizardName("BuildCase", userId);
    return base.OnAfterRenderAsync(firstRender);
}

Configuring the Process

As I said, I’d be uncomfortable with making a step appear or disappear while the user is performing the process. Here, though, I’m using this option to control the initial display of the process: The user still gets a well-defined process when they open the component, it’s just that the first step may be different. Effectively, I’m configuring the process before the user starts it.

In fact, this feature gives you the ability to turn the problem of configuring the process over to the user. You can, for example, provide the user with a one or more checkboxes that allow the user to add or remove the steps they want in the process (though when steps are added using this technique, they are always appear at the end of the Stepper).

To empower the user to decide whether to display the Overview step, you might add a checkbox like this to your component:

Show Overview: <input type="checkbox" 
                                @bind-value="overviewRequired"
                               checked="@overviewRequired" />

Now the user can decide if they want to see the Overview by selecting the checkbox.

A Warning

It might be obvious to say this but, when you skip a step in your process, that step is no longer part of the process … and that changes the numbering that you’ve been using to identify your steps. In my sample code, for example, I’ve been binding my Blazor Stepper UI component to a field called currentStep. When all the steps are displayed (i.e., including the Overview step), then, when the user selects the second step (the one labeled “Pick Date Range”), the currentStep field will be set to 1.

On the other hand, if the Overview step isn’t displayed, then the “Pick Date Range” step becomes the first step. Now, when the user selects that step, currentStep will be set to 0. If you have logic that’s driven by the bound field, then you’ll need to adjust that logic for any steps that you’re adding or removing.

As I’ll discuss in my next post, using the StepperStep’s ValueChanged event can give you another way of dealing with this problem. Alternatively, you can “omit” steps without actually removing them and, as a result, avoid renumbering the following steps.

Disabling Steps

To avoid renumbering steps at run time, rather than adding or removing steps, show all the steps in the Stepper and disable any steps that aren’t required. Disabling a step is supported by setting the StepperStep’s Disabled property to true.

With this Razor code, for example, it’s just a matter of setting the caseDisabled field to true or false to enable/disable the step:

        <StepperStep Label="Define Case"
                     Icon="save"
                     Disabled="@caseDisabled”></StepperStep>

When a step is disabled, it’s grayed out in the UI and the user can no longer select that step. The step continues to be part of the process, though, and so none of your steps are renumbered.

The same process as shown before. However, in this display the Define Case step is grayed out to indicated that it is disabled

There’s a caveat here also, though: While disabling a step prevents the user from selecting the step in the Stepper’s UI, it does not stop you from making that disabled step into the current step by setting the field the Stepper is bound to (currentStep, in my example). It’s your responsibility to make sure that, in your code, you don’t set the bound field to any step that you’ve disabled.

You now have the tools you need to dynamically create the UI that will help your user through their difficult task. Up until now, however, I’ve been assuming that it’s always OK for the user to interact with the Stepper to pick the “next step” in the process. In reality, there are some issues to deal with if you let the user pick the “next step.” Next I’ll show how you can deal with that (including not letting the user select the “next step”).

Controlling the User in the Stepper

One of the benefits of using the Blazor Stepper component from Telerik is the control it gives you over how the user can move through the process: You can give the user as much, or as little, control over selecting the “next step” as you want. In practice, you may want to give the user no control at all.

Setting Up the Stepper

For this example, I just need the simplest possible Stepper design: a process with four steps. This example shows that the user has either been advanced to the third step by the component’s code or that the user has selected the third step. Either way, that third step is the “next step.”

A basic display of a four step process: four circles laid out horizontally and joined by a line with the circles numbered 1, 2, 3, and 4.

This is all the Razor code I need to create that (admittedly) simple display:

<TelerikStepper @bind-value="@currentStep">
    <StepperSteps >
            <StepperStep></StepperStep>
            <StepperStep></StepperStep>
            <StepperStep></StepperStep>
            <StepperStep></StepperStep> 
   </StepperSteps>
</TelerikStepper>

The simplest way to manage the user’s progress through these steps is to implement two-way data binding between your Stepper and an integer field (or property) using the bind-value attribute, as I’ve done in my example. Typically, you’ll initialize the bound field to zero to position the user on the first step. In my case, that code looks like this:

private int currentStep = 0;

With this design, both you and your users are empowered to select the “next step”:

  • You can control the “next step” by setting the value of the bound field (currentStep, in my case).
  • Users can control the “next step” by clicking on a step in the UI.

As I’ve discussed elsewhere, you’ll also need to coordinate your UI as the user moves through the steps. However, in this post I want to discuss the problems that are created when you give the user some control—specifically, the ability leap over steps or return to an earlier step.

Living With Problems

If you allow the user to skip steps, they can arrive at the last page with “incomplete” data. You can avoid forcing the user to go back and fill in missing data by setting appropriate defaults for all input values the user will make in each step.

Allowing the user to skip around in the process opens the opportunity for the user to return to an earlier step and make a change that invalidates entries in later steps. If you’ve managed to ensure that steps are independent of each other or that later steps aren’t dependent on an earlier step, allowing the user to skip around isn’t a problem. Since that’s probably not a realistic option, the solution is to update the defaults for values that will be set in later steps to ensure you “compatible values” at the last step … and make sure that the user knows that you’ve made those changes.

Another solution is to give up: Don’t try to prevent the user from arriving at the final step with “incompatible” results. If the user skips around in the process and creates a problem, you’ll find those problems in the last step, report them, and let the user fix them.

However, if you’re a decent human being, in addition to reporting the problems, you’ll also flag the step where the user can fix the problem. The StepperStep element’s Valid attribute is designed to let you do this. All you have to do is to bind the StepperStep’s Valid attribute to a field, property or method that reflects whether the step is valid.

This example binds the Valid attribute to a field named step2Valid that could be set in the final step if a problem is found with the values set in Step 2:

<TelerikStepper Valid="@step2Valid">

In this example, setting step2Valid to false causes the step the field is bound to be flagged with an x.

A circle from a Stepper sequence with an x inside of the circle, replacing any icon the step was displaying

Alternatively, you could create a method that assesses the data related to the step and have that method return a Boolean value (true when the data is valid, false when it is not). Once that method is created, you could bind it to the Valid attribute with code similar to my previous example.

Disempowering the User

Or you could limit or deny the user any control over the “next step.” You can, for example, limit the user to picking the steps on either side of the current step as their “next step” by setting the TelerikStepper’s Linear attribute to true:

<TelerikStepper Linear="true">

If that’s still giving the user too much power, you can use the TelerikStepper’s ValueChanged event to prevent the user from selecting one or more of the steps in your process. You might, for example, allow the user to skip ahead to a later step but prevent the user from returning to any previous step.

The first step in disempowering your user is to bind the steps you don’t want the user to select to some method. This example binds every step to a method called StepSelected:

<TelerikStepper @bind-value="currentStep">
    <StepperSteps>
            <StepperStep OnChange="@StepSelected"></StepperStep>
            <StepperStep OnChange="@StepSelected"></StepperStep>
    	…more StepperSteps…
    </StepperSteps>
</TelerikStepper>

With this syntax, the bound method must accept a single parameter of type StepperStepChangeEventArgs, like this:

private void StepSelected(StepperStepChangeEventArgs e)
{

}

It’s the e parameter that’s passed to this event that gives you the ability to disempower your user from selecting the step: Just set the parameter’s IsCancelled property to true, like this:

private void StepSelected(StepperStepChangeEventArgs e)
{
      e.IsCancelled = true;      
}

Now, if the use clicks on the step in the Stepper, nothing will change: The “next step” is controlled entirely through whatever field or property you’ve bound the Stepper to. Of course, now that the user can’t select the next step, you’ll need to provide some other mechanism (e.g., “Next” or “Previous” buttons) to update the bound field (currentStep in my example) and move the user to the “next step.”

If writing a method seems like too much work, at the cost of some copying and pasting, you can bind each step to a lambda expression that sets the IsCancelled property. That’s what this example does:

<StepperStep OnChange="@(e => e.IsCancelled = true)></StepperStep> 

If you want to dynamically pick the steps your user can select, you can integrate a field from your code that allows you to dynamically keep your user from selecting a step. This would, for example, let you prevent the user from returning to earlier steps while allowing them to select later steps.

That code might look like this which uses fields called step1Deny and step2Deny to turn off specific steps:

<StepperSteps>
   <StepperStep OnChange="@(e => e.IsCancelled = step1Deny)"></StepperStep>
  <StepperStep OnChange="@(e => e.IsCancelled = step2Deny)"></StepperStep>
      …more steps…
</StepperSteps> 

One note: Making a step “unselectable” from your code doesn’t change the appearance of the step in the user interface. From a UX point of view, preventing only some steps from being selected is problematic because both selectable and unselected steps look alike.

While using the IsCancelled property to make all steps unselectable makes sense (at least to me), if you’re only making some steps unselectable, leveraging the step’s Disable attribute might be a better choice. The Disable attribute not only prevents the user from selecting a step but flags those steps in the Stepper’s UI.

What’s left to discuss is how to integrate processing into each step (including sharing control over the “next step” with your user). That’s next.

Integrating Process Code into the Blazor Stepper

You’ll probably find there’s some processing you need to include as the user moves from step to step. You can certainly tie whatever code you want to the various components in each step’s UI (binding a method to a button’s click event, for example). But the Stepper also gives you tools for integrating code as the user moves from step to step no matter what the user does (including the ability to control the user’s “next step”).

In fact, if you’re letting the user select the “next step” in the process by clicking on steps in the Stepper’s UI, this code may be essential in order to deal with the problems created as users either skip steps or move back to previous steps.

Managing the User’s Progress

Users are automatically given the ability to select the next step just by using two-way data binding to tie the Stepper to an integer field (or property) in your code. This example ties a Stepper to a field called currentStep:

<TelerikStepper @bind-value="currentStep">
  <StepperSteps>
            <StepperStep Label="Overview" 
                                     Icon="info-circle" ></StepperStep>
            <StepperStep Label="Pick Date Range" 
                                     Icon="calendar" ></StepperStep>
            <StepperStep Label="Select Occurrences"
                                     Icon="link" ></StepperStep>
            <StepperStep Label="Define Case"
                                     Icon="folder-open" ></StepperStep>
            <StepperStep Label="Save Changes"
                                     Icon="save" ></StepperStep>       
    </StepperSteps>
</TelerikStepper>

That gives this UI:

A five-step process. Each circle in the step has an icon. The first step has the information icon (the lower case letter i inside a circle), the second step has a calendar, the third step has a link in a chain, the fourth step has an open folder, and the fifth step has a disk. Each step also has a label below it: “Overview” for step 1, “Pick Date Range” for step 2, “Select Occurrences” for step 3, “Define Case” for step 4, and “Save Changes” for Step 5.

Typically, you’ll initialize that bound field to zero to position the user on the first step:

private int currentStep = 0;

The Stepper gives you two events for integrating your own code with the user’s ability to navigate through the steps, including selecting the “next step”:

  • The ValueChanged event on the parent TelerikStepper element
  • The OnChange event on the StepperStep element

These events fire only when the user selects the “next step” in the Stepper’s UI (i.e., these events don’t fire if your code selects the next step). (I discussed how you can use the OnChange event on the StepperStep to stop the user from selecting a step above.)

Tracking the Step

If you decide to bind a method to the TelerikStepper’s ValueChanged event, then you’ll have to give up using two-way data binding and settle for one-way data binding with the Stepper. This code, for example, binds the Stepper’s ValueChanged event to a method called StepperChanged and binds the Stepper to a field called currentStep using one-way data binding:

<TelerikStepper Value="@currentStep" ValueChanged="@StepperChanged">

Because of the switch to one-way data binding, when the user clicks on a step in the UI, the bound field will not be updated … which can be too bad if you need the value of the current step in your code. Fortunately, the value of the new step is passed to the bound method (StepperChanged, in my example) and you can use that parameter to update a field in your code:

private int currentStep;
private void StepperChanged(int newStep)
{
   currentStep = newStep;
}

Another option for determining the current step is to add a ref attribute to your Stepper and bind it to a field of type TelerikStepper. Here’s the Stepper control with a ref attribute bound to a field called (cleverly) stepper:

<TelerikStepper @ref="stepper" value="@currentStep" ValueChanged="@StepperChanged">

After defining the field the ref attribute is bound to, you can then use ref field’s Value property to determine the current step:

TelerikStepper stepper;

 private void UpdateUI()
 {
    switch(stepper.Value) 
    {
        case 0:
             DisplayFirstStep();     
             break;

While Value reports the current step, you can’t use it to move the user to another step (in fact, if you do, you’ll get a message that the Value should only be changed within the component). Among other issues, the Stepper won’t re-render when Value is updated.

Centralizing Control

While giving up two-way data binding is too bad, using the ValueChanged event allows you to centralize your process code into the single method bound to the ValueChanged event. In addition to letting you do the right thing for whatever step the user selects, you can also use the Stepper’s bound field (currentStep in my example) to control the user’s “next step.” The following code, for example, automatically moves the user from the second step to the fourth step if the FormatSet variable is true:

private void StepperChanged(int newStep)
{
   if (newStep == 2 && FormatSet)
   {
      currentStep = 4
   }
}

This code does the reverse: It won’t let the user go to the fourth step until a Boolean value named FormatSet is true—if the user tries to get to the fourth step before FormatSet becomes true, the user is left on their current step:

private void StepperChanged(int newStep)
{
   if (FormatSet)
   {
      currentStep = newStep
   }
   else if (newStep < 3)
   {
         currentStep = newStep;
   }     
}

Localizing Control

The major problem with using the ValueChanged method is that, as the number of steps increases, the method can get big and complicated. If you use the StepperStep’s OnChange method, you can divide the code for each step over multiple, dedicated methods.

Using the StepperStep’s OnChange event also lets you use two-way data binding with the Stepper (i.e., setting the currentStep field will update the current step in the Stepper). However, unlike the ValueChanged event, you can’t from within an OnChange change the currently selected step.

This example binds each step to individual methods called Step1Selected and Step2Selected:

<TelerikStepper @bind-value="currentStep">
    <StepperSteps>
            <StepperStep OnChange="@Step1Selected"></StepperStep>
            <StepperStep OnChange="@Step2Selected"></StepperStep>
    	…more StepperSteps…
    </StepperSteps>
</TelerikStepper>

With this syntax, the bound method must accept a single parameter of type StepperStepChangeEventArgs, like this:

private void Step1Selected(StepperStepChangeEventArgs e)
{

}

If you want, you can write a single ValueChanged method that incorporates the code for every step. With that design, you’ll use the parameter’s TargetIndex property to do the right thing for each step:

private void StepChanged(StepperStepChangeEventArgs e)
{
   switch (e.TargetIndex)
   {
       case 0:
          //...do step 1 stuff...
          break;
       case 1:
          //...do step 2 stuff...
          break;
    }
}

You’re not restricted to just accepting the default event argument for this event. If you rewrite the event code as a lambda expression that accepts the default event parameter, you can pass any additional parameter to the method (or, for that matter, any set of parameters you want).

This example passes a string value as a second parameter to the OnChange handler:

<StepperStep OnChange='@(e => StepChanged(e,"Cart"))'></StepperStep>

Of course, you’ll need to rewrite your method to accept your new set of parameters. This example will accept that second parameter of type string:

private void StepChanged(StepperStepChangeEventArgs e, string status)

This technique also works with Stepper’s ValueChanged event.

One of the benefits of passing an additional parameter is that you use that “extra parameter” to identify the step you’re working with instead of using the step’s position using the TargetIndex property. Normally, identifying a step by its position isn’t a problem but, as I discussed above, you can dynamically add or remove steps from your Stepper. Unfortunately, if you do add or remove steps, you’ll also change every following steps’ position. Passing a second parameter in OnChange event gives you way to identify a step even if its position changes.

Effectively, the ValueChanged and OnChange events let you ensure that some code is automatically incorporated into your process’s steps—think of these methods as the equivalent of a Form_Load event. Combined with any code you’ve tied to UI components in individual steps, you can ensure that, no matter what the user does, you’ll successfully complete the user’s task.

Stepper Series Summary

This completes the deep dive in the UI for Blazor’s Stepper component. As you’ve seen, the Stepper component is flexible enough to allow you to create the UX for any complicated task that you might want to help your users out with. However, you should recognize that you have a potentially simpler solution for creating a “wizard-based solution” by using the Wizard component, which is coming up next. The Wizard component could be the less complicated solution for your users’ complicated task.


Peter Vogel
About the Author

Peter Vogel

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter also writes courses and teaches for Learning Tree International.

Related Posts

Comments

Comments are disabled in preview mode.