1

I have Blazor Server application on .NET 8. I am exploring different options for navigation inside my application. I have implemented two ways of that - changing the @Body of the MainLayout with navigation manager extension for extra data around it or Index.razor that is a wrapper for dynamic component and inside the dynamic I'll change the pages by type.

Below are the two implementations. My question is which way is better for my business case and more straight forward for an app that is public and will be used by hundreds and thousand of users. What is stopping me to choose navigation manager direction is that I cannot easily pass parameters to a certain page I am redirecting to (like input parameters).

public class NavigationProvider
{
    private readonly NavigationManager _navigationManager;

    public NavigationProvider(NavigationManager navigationManager)
    {
        _navigationManager = navigationManager;
    }

    public StepEnum CurrentStep { get; private set; } = Step.Undefined;

    public Dictionary<string, object> PassedParameters { get; private set; }

    public void NavigateTo(OutcomeEnum outcome, Dictionary<string, object> passedParameters)
    {
        this.PassedParameters = passedParameters;

        StepEnum nextStep = ChooseNextStep(outcome);

        this.Navigate(nextStep);
    }

    private StepEnum ChooseNextStep(OutcomeEnum outcome)
    {
        StepEnum nextStep = StepEnum.Undefined;

        switch (CurrentStep)
        {
            case StepEnum.Login:
                nextStep = StepEnum.Dashboard;
                break;
            case StepEnum.Dashboard:
                nextStep = StepEnum.ExecuteAction;
                break;
            case StepEnum.ExecuteAction:
                nextStep = StepEnum.EndOfUserAction;
                break;
            default:
                nextStep = StepEnum.ErrorPage;
                break;
        }

        return nextStep;
    }

    private void Navigate(StepEnum nextStep)
    {
        _navigationManager.NavigateTo(nextStep.ToString());
    }
}

public enum OutcomeEnum
{
   ErrorInBusinessLogic,
   SuccessfulRedirect
}

public enum StepEnum 
{
   Login,
   Dashboard,
   ExecuteAction,
   EndOfActionExecute,
   ErrorPage
}

I want to be able to navigate to routes with different set of parameters and save the current step between the screens. Also based on the outcome of the screen - success/error

I have to be able to know where to navigate: for example on success on the next functional screen or on error navigate to the ErrorPage.

Is this the way to navigate inside Blazor server app ? How to create different view model with input parameters for every screen and to pass it with navigation ?

Another idea I explore is to create in Index.razor wrapper for a dynamic component like this:

<DynamicComponent Type="ComponentType" Parameters="ComponentParameters" />

Here ComponentType is the actual razor page cast as type and I can skip the Navigation Manager and just change the component type based on the outcome of the screen. Inside my index.razor.cs, I'll have a function like this:

public Type ComponentType { get; set; } = typeof(Login);

public Dictionary<string, object> ComponentParameters { get; set; }

protected override void OnInitialized()
{
    this.ComponentParameters = new Dictionary<string, object>
    {
        { "NavigateRequest", new Action<NavArgs>(HandleNavigateRequest) }
    };

    base.OnInitialized();
}

public void HandleNavigateRequest(NavArgs args)
{
    this.ComponentType = args.CurrentStep switch
    {
        StepEnum.Registration => typeof(Dashboard),
        StepEnum.Dashboard=> typeof(ExecuteAction),
        StepEnum.ExecuteAction=> typeof(EndOfActionExecute),
        _ => this.ComponentType // default case, keeps the current ComponentType unchanged
    };
}

and in every page I can just emit to the index.razor:

[Parameter]
public Action<NavArgsType> NavigateRequest { get; set; }

public void RequestNavigate()
{
    var args = new NavArgs()
    {
        CurrentStep = StepEnum.Login,
    };
    this.NavigateRequest.Invoke(args);
}

UPDATE 1 -- business case info:

My business case for the steps looks like this:

Steps:

  • Represent individual tasks within the wizard.

Each step:

  • Knows its own operation.
  • Can determine success or failure of its operation.
  • Does not decide the next step.

Step Router:

  • Central component responsible for:
    • Receiving step name, outcome, and optional parameters.
    • Determining the next step based on predefined logic considering:
      • Step name
      • Step outcome (success/failure)
      • Returning the next step name and any additional parameters to be passed.

Step Data:

  • An object (similar to MVC TempData) for passing data between steps.
  • This object can be managed by the Step Router or a separate service.

I don't need bookmark of the step or hackable url to be able to jump between the steps. I want to persist the last executed step (wizard progress) in a database and be able to resume the process if the process is interrupted.

My question here is: can I use the standard Navigation Manager with a little tweak to achieve this design or I need another approach like the dynamic component or something else ?

9
  • You focus a lot on tech details, for an answer I would like to know more about your 'business case'. Should users be able to bookmark a step (url) and/or skip steps by editing the address? Commented Mar 19, 2024 at 13:29
  • Should it support static rendering? How much do the components (step pages) know about the stepping? Commented Mar 19, 2024 at 13:53
  • This looks like a state machine, so you need to think about it in that context. In the early days of Blazor I wrote a demo Visitor Booking system. It was based on the same design as your second option. One landing page: no urls. A ViewManager instead of the Router. The applicatiion switched between Form components by calling the ViewManager on state changes. Worked well, just like a desktop application. The problem was no navigation was just too alien to people. So my take on this is go with step 2. Commented Mar 19, 2024 at 21:36
  • @HenkHolterman thank you for the questions. I've update the question to provide more information about the business case. Commented Mar 20, 2024 at 15:13
  • @MrCakaShaunCurtis What I am concerned is that it do looks like desktop application. I didn't understand did it work well or the users were confused and you changed the approach at some time ? Commented Mar 20, 2024 at 15:20

4 Answers 4

2

If you want to pass complex objects between different pages, then you should maintain that object in a scoped service, and inject that scoped service in whichever page you want. Passing it as URL parameter is not a good design pattern for SPA framework like Blazor server. For example:

public class WizardStep
{
    public int Current {get; set;}
  // Additional step data that you have collected and want to use for the wizard
}

In program.cs

builder.Services.AddScoped<WizardStep>();

Then in the Wizard Step:

@page "/wizard"
@inject WizardStep WizardStep


@code{
    
    // either navigate to the specific page depending on WizardStep.StepName
    // or use if...else in razor markup to show component specific to the step. 

    
}

If you use if else for every wizard step

@page "/wizard"
@inject WizardStep WizardStep

@if(WizardStep.Current == 1)
{
   ////
}
@if(WizardStep.Current == 2)
{
   ////
}
/// etc


@code{

    void OnNext(){
          
        WizardStep.Current++;
    }
    void OnPrevious(){
          
        WizardStep.Current--;
    }

  
}

// change the OnPrevious, OnNext logic if you want to use navigationManager. 
Sign up to request clarification or add additional context in comments.

Comments

1
+500

I think all suggestions so far (except maybe the one from Mayur Ekbote) are over-complicated. A stepping wizard is basically a TabControl without the tabs. Either pass the top component as a Cascading Value or have everybody implement an EventCallback<bool> Oncompleted.

@if(currentStep == 1)
{
   <Login OnCompleted="NextStep" />
}
else if (currentStep == 2)
{
   <Dashboard SomeData="dahsData" OnCompleted="NextStep"  />
}
... // etc


@code{
  int currentStep = 1;

  // keep all your data right here
  Commondata data = ...;
  DashData dashData = ...; 

  void NextStep(bool wasOk)
  {
     // compute next step
  }

}

You should of course keep the stepEnum, I just skipped that a little.

2 Comments

You are right - there is no need to over-complicate the idea. I like your suggestion. I'll explore it a little bit more the data transfer between the steps before accepting the answer. One thing that bothers me is that i have to have all the Data classes defined inside the code behind of this router component. And the second thing is that if nextStep method has to return more data from the child component that i call I have to create new function as call back like: OnCompleteFromDashboard that would do stuff with the additional parameters and then call this.NextStep() inside it.
You will have to manage the data and callbacks somewhere, this seems to be not a bad place. But you can of course add injectable services, interfaces etc.
0

Still straggle to understand why to create you customize navigation implementation when you have everything already pretty-well defined out of the box.. especially when the default implementation meets your demands

From the docs:

@page "/user/{id:int}/{option:bool?}"  <-- define the route of current page

<p>
    Id: @Id  <-- print the value of "id" parameter
</p>

<p>
    Option: @Option  <-- print the value of "option" parameter (if provided)
</p>

@code {  <-- define parameters in code
    [Parameter]
    public int Id { get; set; }

    [Parameter]
    public bool Option { get; set; }
}

I do say that although your app seems like a management app that would probably won`t need to share links, actual page navigation is preferred when a link share is required (less for SEO in your case, more for collaborating between team members)

If (for some reason) your solution uses different architecture you may want to look at this article, describing how to achieve the same effect with different types of net core web apps

Cheers

2 Comments

Hi, thanks for the reply. I want to pass complex objects between views and url route parameters is not the way i want to do that. That's why I am looking for a way to bypass "hackable" urls even for simple redirects and use Shared Service for Data instead. Also about the question why I implement a wrapper of the navigation - I want to keep the current step of the wizard, while navigating through the app - so I thought of an extension when navigating to mark that I moved to that current step.
it's a very specific use case. wizard = component and therefore should not manipulate page navigation. Also, you don`t have to "pass" complex objects but to store the state inside your component. Even if your component uses other components - you can bind complex objects as properties from parent component into child components. Maybe you want to refine your question?
-2

As you are in .Net8 I think you can do what you need with the NavigationManager property HistoryEntryState. This allows you to pass a string during the navigation that is not seen or part of the URI like a query parameter would be.

In your case this could be a key to lookup whatever data you need in some form of cache. That could be a database or an object you could @inject using DI

Below is a simplfied example showing how you can use RegisterLocationChangingHandler to inspect the navigation before it happens and change it. In this case to add a HistoryEntryState value before passing it on.

//@inject NavigationManager navigationManager (in your razor)

protected override async Task OnInitializedAsync()
{
    //Add a location change function
    navigationManager.RegisterLocationChangingHandler(this.ChangingLocation)
    
    //is this a load with a Key Set?
    if (navigationManager.HistoryEntryState != null)
    {
       //look up your data based on the HistoryEntryState value
       //and decide what to do
    }
 }

private ValueTask ChangingLocation(LocationChangingContext context)
{     

     if (context.HistoryEntryState != null)
     {
        //HistoryEntryState is set so continue with the navigation 
        return ValueTask.CompletedTask;
     }
     
    //HistoryEntryState not set 
    //start a new Navigation and add the Key 
    //this is done using the NavigationOptions.HistoryEntryState property   
    navigationManager.NavigateTo(context.TargetLocation, new NavigationOptions() { HistoryEntryState = "yourkeyhere" });

    //stop the initial navigation as we started a new one
    context.PreventNavigation();
    return ValueTask.CompletedTask;
}

Hope this helps

1 Comment

Thank you for your response. What bothers me here is the serialization and especially the deserialization of the object with parameters. I've reached the conclusion that I want something similar to TempData in MVC and this is as close as it gets, but the conversion could potentially lead to data mapping issues and missing information.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.