Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for nested routing #6

Open
TheExiledCat opened this issue Jan 7, 2025 · 7 comments
Open

Add support for nested routing #6

TheExiledCat opened this issue Jan 7, 2025 · 7 comments

Comments

@TheExiledCat
Copy link

TheExiledCat commented Jan 7, 2025

This one is a big bit and completely understandable if out of scope but i really like this library but theres one thing ive always felt was missing for apps that have subviews.

Nested routing.

Comparing it to routing libraries like vue router or the react routers
a nested routing solution inside this library would be amazing and would make this the perfect replacement for ReactiveUis router.

The way this would have to be implemented (again, comparing it to vue -router) is that when creating your IOC container you would do something like this

services.AddRoute<viewmodelT>()
.WithChildren(
//insert some array system here to create nested routes
//eg new SubRoute<childModelT>()
)

then when routing it i could for example do something like this
as usual

Router.GoTo<viewmodelT>() 

and the router could have some kind of property like CurrentRoute.SelectedChild to bind to which we can then bind in the subview

@sandreas
Copy link
Owner

sandreas commented Jan 7, 2025

Wouldn't it be easier to just do:

var mainRoute = Router.GoTo<viewmodelT>();
var subRoute = Router.Goto<subViewmodelT>();
mainRoute.SelectedChild = subRoute;

However I already thought of having some extensions to make:

  • Router.Current - current view model
  • Router.GetHistoryItem(int index);
  • Router.GoTo<TViewModel>(Func<TViewModelBase, List<TViewModelBase>, int, TViewModel> routingCallback) - where TViewModelBase is current, List is History and int is currentIndex

Is there an example project where I could read some code about what you are trying to achieve?

@TheExiledCat
Copy link
Author

TheExiledCat commented Jan 10, 2025

Wouldn't it be easier to just do:

var mainRoute = Router.GoTo<viewmodelT>();
var subRoute = Router.Goto<subViewmodelT>();
mainRoute.SelectedChild = subRoute;

However I already thought of having some extensions to make:

  • Router.Current - current view model
  • Router.GetHistoryItem(int index);
  • Router.GoTo<TViewModel>(Func<TViewModelBase, List<TViewModelBase>, int, TViewModel> routingCallback) - where TViewModelBase is current, List is History and int is currentIndex

Is there an example project where I could read some code about what you are trying to achieve?

Yeah so basically, (and ill be using vue-router as an example here even tho its a web framework it fits the case here i think)

In vue, you define your routes and their components (views/viewmodels) from the app settings (DI container in .net)

from there you can define children for them as well. Instead of having a regular ContentControl that just binds directly to the current route of the view, it has a special usercontrol that not only binds the content to it, but also is aware of where inside of the routing hierarchy it is. But ofcourse with the way the routing is based on viewmodels in c# you dont really need to specify subroutes at DI level if you can just do it at Router.GoTo time.

so i have a route like

Router.GoTo<ParentViewModel>().WithChild<ChildViewModel>()
there would be some kind of special user control, (lets name it routerview) which would automatically be bounded by the router to the correct instance of the viewmodel specified in the router. So instead of manually binding a contentcontrol to the routers content, the usercontrol handles it and it has a reference in the background to both the router, routes, and the current view model to figure out where in the hiearchy it is.

So lets say i have my parent window with a router view, it would show the TopLevel route inside of it, and if whatever is inside of there has another router view in it, that would show the viewmodel of the first child, that one could have another on inside it etc.

to see how vue-router does it:

const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        // UserProfile will be rendered inside User's <router-view>
        // when /user/:id/profile is matched
        path: 'profile',
        component: UserProfile,
      },
      {
        // UserPosts will be rendered inside User's <router-view>
        // when /user/:id/posts is matched
        path: 'posts',
        component: UserPosts,
      },
    ],
  },
]

which defines the routes (and arguments in a web scenario) and then the component (view/viewmodel)

then, everywhere u want nested routes:

<router-view "/>

which binds to the route recursively based on which viewmodel it is in the hierarchy, so the Parentviewmodels router view will have a content binding of the child routes viemodel, that one of its child etc.

for a better understanding i will just show the vue router docs:
Nested routing in vue-router

hopefully this gives u an idea of what i mean. This would be amazing for this framework as its a very simple thing to add and would make it alot more functional especially in apps with sub navigation like tabs, nav bars etc.

another cooll thing from vue-router is the router-link:

<nav>
    <RouterLink to="/">Go to Home</RouterLink>
    <RouterLink to="/about">Go to About</RouterLink>
  </nav>

which is a wrapping for any kind of control that makes it so that when it is clicked like a button, it will forwards to the route specified (which means you can do direct bindings with no code behind necessary for quick and dirty routes)

it could look like this in avalonia:

<RouterLink To="{Binding ViewModelTypeHere}/>

@TheExiledCat
Copy link
Author

For simplicity sake lets say the RouterView Usercontrol just looks like this

<RouterView IsRoot = "True"/> //the top level router view is the main window

then that routerview would have some code in it that scans all

if(IsRoot){
 //Some function to get the Top Level route from the Router
//Some function to scan if the currently loaded routes view contains another Routerview, if so, pass it the router, and an index counter recursively so it knows which route in the hierarchy it is
}

@sandreas
Copy link
Owner

I don't think that this vue approach of declaring and visiting routes can be easily transferred to (a)xaml and c#... The approach used in this project assumes that Routes are not configured via strings (e.g. /settings/general/editor) but via generic types (e.g. <TViemodel1, TViewmodel2, TViewmodel3>). This could be achieved by overloading GoTo<T1, T2, T3, ...> or GoTo(string route, Func<ViewModelTree> callback) but I'm not sure, how nested ViewModels would work with ViewLocators in Avalonia...

Maybe you have some C# Avalonia Code to illustrate the problem you're trying to solve? Like a ViewModel with a SubViewModel using subsequent views? JavaScript examples are helpful for the general Concept but I'm still not sure how this is gonna map to the Avalonia concept...

@TheExiledCat
Copy link
Author

I made a little app to demonstrate this (and some issues i had because of the lack of nesting)

this is just a template as i didnt wanna create a full app for the example but imagine you have some kind of spotify like library app which has:

  • A main window
    • With a page on it (HomeView in this example)
      • With a Nav bar on the left whos buttons bind to viewmodels
      • A content section on the right (the subview) that should be bound based on the selected route of the router

since the router binds the content of the main window itself it will replace the entire window on route change. making it impossible to move routes but instead bind the inner content control.

Here are the various views i have to demonstrate and their viewmodels

MainWindow:

<Window xmlns="https://github.com/avaloniaui"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:vm="using:routingexample.ViewModels"
   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
   x:Class="routingexample.Views.MainWindow"
   x:DataType="vm:MainWindowViewModel"
   Icon="/Assets/avalonia-logo.ico"
   Title="routingexample">

   <Design.DataContext>
       <!-- This only sets the DataContext for the previewer in an IDE,
            to set the actual DataContext for runtime, set the DataContext property in code (look at
       App.axaml.cs) -->
       <vm:MainWindowViewModel />
   </Design.DataContext>

   <ContentControl Content="{Binding Content}" />

</Window>

MainWindowViewModel (directly taken from the simplerouter example)

using Avalonia.Controls;
using Avalonia.SimpleRouter;
using CommunityToolkit.Mvvm.ComponentModel;
using routingexample.Views;

namespace routingexample.ViewModels;

public partial class MainWindowViewModel : ViewModelBase
{
   [ObservableProperty]
   private ViewModelBase content = default!;

   public MainWindowViewModel(HistoryRouter<ViewModelBase> router)
   {
       // register route changed event to set content to viewModel, whenever
       // a route changes
       router.CurrentViewModelChanged += viewModel => Content = viewModel;

       // change to HomeView
       router.GoTo<HomeViewModel>();
   }
}

that all works fine, if i do route.goto it loads in the home view following next:

HomeView:

<UserControl xmlns="https://github.com/avaloniaui"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:vm="using:routingexample.ViewModels"
  xmlns:local="using:routingexample.Views"
  x:DataType="vm:HomeViewModel"
  mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
  x:Class="routingexample.Views.HomeView">
  <SplitView DisplayMode="Inline" OpenPaneLength="400" IsPaneOpen="True">
    <SplitView.Pane>

      <local:NavbarView DataContext="{Binding Navbar}">
      </local:NavbarView>


    </SplitView.Pane>

    <ContentControl Content="{Binding Content}"></ContentControl>
  </SplitView>
</UserControl>

as you can see home view has both a navbar (custom control) and another content control to which i wanna bind the route

obviously when changing routes the only way to bind to the inner route is by replacing the routers CurrentViewModelChanged event to replace the content of the home views subview instead. This would be insanely hard to manage in a real code base and makes it alot of boiler plate.

This is what it looks like (imagine i actually properly made the layout and set the buttons to look nice:
image
then when clicking one of the buttons what happens is the entire view gets replaced obviously:
image

now the way this nested routing system could work is:

  1. Add some kind of special usercontrol called a RouterView which is aware of any other routerviews inside or outside of it (so it can build a hierarchy)
  2. When a route gets pushed, allow some kind of syntax to add a childview or a childchildview etc (e.g. Router.GoTo<T1>().WithChild<T2>().WithChild<T3> //etc
  3. Then when the router moves to a new route, it will loop through all the routerviews in the app, check where in that hierarchy they are, and based on it provide them with the correct view to load.
  4. This way we can have nested routing and navigation without having to constantly overwrite the CurrentVMChanged event

I hope this makes a bit more clear what i mean, if not tell me i can try a different kind of example.

PS: another thing (unrelated to this issue) is the possibility to route to a route without using a generic function (maybe by vm name or a Type argument), for my navigation i had to bind viewmodels to the buttons like this (every button it bound to a routemap):

using System;
using Avalonia.SimpleRouter;
using Dumpify;
using routingexample.ViewModels;

namespace routingexample.Models;

public class RouteMap(HistoryRouter<ViewModelBase> router)
{
    public string Name { get; set; }
    public ViewModelBase ViewModel { get; set; }

    public void PushRoute()
    {
        Type vmType = ViewModel.GetType();
        router.GetType().GetMethod("GoTo").MakeGenericMethod(vmType).Invoke(router, null);
        ViewModel.Dump();
    }
}

using reflection here isnt so pretty, but again, unrelated to the current issue

@sandreas
Copy link
Owner

Wow thanks for your effort , I try to take a look in the next days... May take some time.

@TheExiledCat
Copy link
Author

Wow thanks for your effort , I try to take a look in the next days... May take some time.

No worries mate! love this project as its my go to router for smaller avalonia apps so i really hope these things can be worked on. I am also willing to contribute if you need it!.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants