How do I navigate to a page within a TabViewItem using Uno Platform's NavigateViewAsync?

174 Views Asked by At

I have a NavigationView that displays a dynamically generated tree of items, and when an item is clicked, it's supposed to open a new tab. However, instead of navigating inside the TabViewItem's content, calling Navigator().NavigateViewAsync<...>() overwrites the contents of the entire window with the new view, as opposed to the specific region i want.

Relevant XAML:

<Grid utu:SafeArea.Insets="All">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <utu:NavigationBar Grid.Row="0" Content="{Binding Title}" />
    <TabView uen:Region.Name="tabview"  uen:Region.Attached="True" Grid.Column="1" TabCloseRequested="TV_TabClose" x:Name="projectTabs" IsAddTabButtonVisible="False" />
    <Grid Grid.Row="1">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>

        </Grid.ColumnDefinitions>
        <NavigationView Grid.Column="0" x:Name="sidebar" IsBackButtonVisible="Collapsed" IsSettingsVisible="False">
            <NavigationView.PaneCustomContent>
                <TreeView ItemInvoked="TreeItemClicked" CanDragItems="False" CanReorderItems="False" x:Name="ProjectTree" ItemTemplateSelector="{StaticResource TplSelector}" ItemsSource="{Binding wi}"/>
            </NavigationView.PaneCustomContent>
        </NavigationView>
    </Grid>
</Grid>

Function as written in original attempt

public async void NavToProject(WorkspaceProject? p)
{
    if (p != null)
    {
        var newTab = new TabViewItem();
        newTab.Content = new Frame();
        await this.NavigateViewAsync<Project>(this, data: p.Project);

    }
}

As well, I've tried initializing my own instances of Navigator and FrameNavigator...

public class MainModel {
    ILogger<Navigator> log1 { get; set; }
    ILogger<FrameNavigator> log2 { get; set; }

    IDispatcher dispatcher { get; set; }
    public MainModel(
        IStringLocalizer localizer,
        INavigator navigator, IConfiguration config, 
        IRouteResolver routeresolver, 
        IDispatcher idispatcher, ILogger<Navigator> log1, ILogger<FrameNavigator> log2)
    {
        viewmapresolver = routeresolver;
        this.log1 = log1;
        this.log2 = log2;
        this.dispatcher = idispatcher;
        this.config = config;
        config.GetSection("RecentProjects").Bind(appopts);
        _navigator = navigator;
    }
    public async Task<TabViewItem?> NavToProject(WorkspaceProject? p)
    {
        if (p != null)
        {
            var newTab = new TabViewItem();
            newTab.Content = new Frame();
            var nb = new Navigator(log1, dispatcher, new NavigationRegion(view: newTab.Content as FrameworkElement), views);
            var fn = new FrameNavigator(null, dispatcher, new NavigationRegion(view: newTab.Content as FrameworkElement), views, new RegionControlProvider()
            {
                RegionControl = newTab.Content
            }) ;
                // this throws
            await fn.NavigateViewAsync<Project>(fn, data: p.Project);
                // this also throws
            await nb.NavigateViewAsync<Project>(nb, data: p.Project);
            return newTab;
        }
        return null;
    }
}

Both attempts throw an ArgumentNullException from within Uno's internal code.

My current approach looks like this:

App.cs routes

private static void RegisterRoutes(IViewRegistry views, IRouteRegistry routes)
        {
            views.Register(
                new ViewMap(ViewModel: typeof(ShellModel)),
                new ViewMap<MainPage, MainModel>(),
                new DataViewMap<ListPage, ListPageModel, G.Project>(),
                new DataViewMap<turquoise.Presentation.Project, ProjectModel, G.Project>(),
                new DataViewMap<TaskTree, TreeModel, G.Section>()
            );

            routes.Register(
                new RouteMap("", View: views.FindByViewModel<ShellModel>(),
                    Nested: new RouteMap[]
                    {
                        new RouteMap("Main", View: views.FindByViewModel<MainModel>()),
                        new RouteMap("/Project/List", View: views.FindByViewModel<ListPageModel>()),
                        new RouteMap("/Project", View: views.FindByViewModel<ProjectModel>()),
                    }
                )
            );
        }

MainModel.cs

{
    public partial class MainModel
    {
        private INavigator _navigator;
        private IConfiguration config;
        public static G.Project CurrentProject;

        public IDispatcher dispatcher { get; set; }
        public IRouteResolver viewmapresolver { get; set; }

        public MainModel(
            INavigator navigator, IConfiguration config,
            IRouteResolver routeresolver,
            IDispatcher idispatcher)
        {
            dispatcher = idispatcher;
            viewmapresolver = routeresolver;
            this.config = config;
            config.GetSection("RecentProjects").Bind(appopts);
            _navigator = navigator;
        }

        public async Task<TabViewItem?> NavToProject(WorkspaceProject? p)
        {
            if (p != null)
            {

                var newTab = new TabViewItem();
                newTab.Header = p.Project.Name;

                var cc = new Frame();

                Region.SetParent(cc, newTab);
                Region.SetName(cc, "/Project");
                Region.SetAttached(cc, true);
                cc.SetValue(Region.ParentProperty, this);
                cc.SetValue(Region.NameProperty, "./Project");
                cc.SetValue(Region.AttachedProperty, true);

                var nnr = new NavigationRegion(cc, Ioc.Default.GetRequiredService<IServiceProvider>());

                var nav = new FrameNavigator(
                    Ioc.Default.GetRequiredService<ILogger<FrameNavigator>>(),
                    dispatcher,
                    nnr,
                    Ioc.Default.GetRequiredService<IRouteResolver>(),
                    new RegionControlProvider()
                    {
                        RegionControl = cc
                    }
                );
                cc.SetNavigatorInstance(nav);
                cc.SetValue(Region.NavigatorProperty, "/Project");
                nnr.ReassignParent();

                NavigationResponse? resp = await nav?.NavigateViewAsync<Project>(cc, data: p.Project);
                
                newTab.Content = cc;
                return newTab;
            }
            return null;
        }
    }
}

I'm still getting multiple ArgumentNullExceptions, mostly regarding the Navigator's IDispatcher instance being null.

2

There are 2 best solutions below

0
The Tablet On BEST ANSWER

Figured it out!

Basically, what I had to do was explicitly set the DataContext of the Frame, as simply navigating within it using NavigateViewAsync(...) does NOT populate it like I naively thought it would.

Final code is below:

public async Task<TabViewItem?> NavToProject(WorkspaceProject? p)
        {
            if (p != null)
            {

                var newTab = new TabViewItem();
                newTab.Header = p.Project.Name;

                var cc = new Project();

                Region.SetParent(cc, newTab);
                Region.SetName(cc, "Project");
                cc.SetValue(Region.ParentProperty, this);
                cc.SetValue(Region.NameProperty, "Project");
// initialize navigation region and attach it to the frame
                var nnr = new NavigationRegion(cc);
                cc.SetInstance(nnr);
// initialize another navigation region, with an IServiceProvider, to be used in the actual navigator. This step is important because the navigator does not play nice with NavigationRegions initialized with services.
                nnr = new NavigationRegion(cc, Ioc.Default.GetRequiredService<IServiceProvider>());


                var nav = new FrameNavigator(
                    Ioc.Default.GetRequiredService<ILogger<FrameNavigator>>(),
                    dispatcher,
                    nnr,
                    Ioc.Default.GetRequiredService<IRouteResolver>(),
                    new RegionControlProvider()
                    {
                        RegionControl = newTab
                    }
                );
                cc.SetValue(Region.NavigatorProperty, "Project");
                nnr.ReassignParent();

                NavigationResponse? resp = await nav?.NavigateViewAsync<Project>(cc, data: p.Project);

                resp = await cc.Navigator()?.NavigateViewAsync<Project>(this, data: p.Project);
// initialize the content's DataContext (very important)
                cc.DataContext = new ProjectModel(p.Project, cc.Navigator() ?? _navigator, ent);
                newTab.Content = cc;
                return newTab;
            }
            return null;
        }
0
Nick Randolph On

So firstly glad to see that you were able to find a workable solution for using navigation. Here are a couple of pointers: