dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
21.98k stars 1.72k forks source link

Shell should support setting queryparameters in a ShellContent instance (in XAML) #3868

Open sander1095 opened 2 years ago

sander1095 commented 2 years ago

Description

I would love to be able to set queryparameters in the route for a ShellContent instance so that I can re-use existing pages by passing different data to them.

This is already supported by using something like Shell.Current.GoToAsync(...); but this is only useful when the user is actually navigating. Take a look at this example:

Our goal is that the ReminderPage would show you reminders from a specific day. in this case, the first tab shows reminders from yesterday, the 2nd tab from today and the third one for tomorrow.

<Shell>
    <TabBar>
        <Tab Title="Foo">
            <ShellContent Title="Yesterday" ContentTemplate="{DataTemplate pages:ReminderPage}"/>
            <ShellContent Title="Today" ContentTemplate="{DataTemplate pages:ReminderPage}"/>
            <ShellContent Title="Tomorrow" ContentTemplate="{DataTemplate pages:ReminderPage}"/>
        </Tab>
         <!-- Just some other tab, not relevant -->
        <Tab Title="Bar">
            <ShellContent ContentTemplate="{DataTemplate pages:BarPage}"/>
        </Tab>
    </TabBar>
</Shell>

Currently we are just rendering the same page 3 times with the same data ,so there is no way for the page to know what date it needs to retrieve reminders from. Unless I am mistaken, there is no good way to make this possible? (More on this in the Intended Use-Case paragraph)

Wouldn't it be great to be able to do the following:

<Tab Title="Foo">
    <ShellContent Route="reminders?date=2021-12-13" Title="Yesterday" ContentTemplate="{DataTemplate pages:ReminderPage}"/>
    <ShellContent Route="reminders?date=2021-12-14" Title="Today" ContentTemplate="{DataTemplate pages:ReminderPage}"/>
    <ShellContent Route="reminders?date=2021-12-15" Title="Tomorrow" ContentTemplate="{DataTemplate pages:ReminderPage}"/>
        </Tab>

Note: Of course this would need to be bindable so the date can be set dynamically.

Note: In my example I re-use the reminders route 3 times. I think that this is also not supported, but it would be great if it was!

Now the ReminderPage could use the queryparameter attribute or implement that query attribute interface mentioned in the docs to deal with this.


This is currently not possible. And this is a feature that people would really like to have. For example, take a look at this SO post or this one. You can find a lot of other questions like this on other sites as well..

Take a look at the answers given by the community on those SO posts. Such workarounds should not be the given standard for a problem like this. This means that implementing this feature would result in better code with a better Shell experience.

Public API Changes

Intended Use-Case

I have already explained this in my description. But I can explain why this feature would be super handy:

The current workarounds for this problem can be summarized in these options:

sander1095 commented 2 years ago

If anyone knows of any current ways to support my scenario, let me know! Perhaps I am wrong about using bindingcontexts?

PureWeen commented 2 years ago

Or maybe mess around with some binding contexts and MVVM, but Shell currently doesn't work well with MVVM so I feel like this is a bad idea?

Elaborate? AFAIK Shell works just fine (slightly better) with MVVM as non-Shell apps

Note: Of course this would need to be bindable so the date can be set dynamically.

Route is a bindable property. If the query parameter should be bindable separately then it might need to be a separate property like "RouteParameter" where you could possible define a key/value pair?

@rmarinho don't you have some attached property examples where you basically achieve this?

sander1095 commented 2 years ago

Hi @PureWeen Thanks for your reply

Elaborate? AFAIK Shell works just fine (slightly better) with MVVM as non-Shell apps

You have a good point; I was mistaken!

Route is a bindable property. If the query parameter should be bindable separately then it might need to be a separate property like "RouteParameter" where you could possible define a key/value pair?

I am not super sure about this. If we were to extend the Route property, it would function the same as the GoToAsync() method where you can just add ?something=123&else=456. If you add a RouteParameter, it would either need to accept a list of key/value pairs (how would you achieve this in XAML?) so you can add multiple things which then get serialized to a querystring, or just a querystring formatted by the user (which is prone to typo's or weird serialization issues if the user doesn't do it correctly like escaping data with a ? or & in it)

This sounds a bit complicated for a single queryparam.

Perhaps both approaches would be good to support:

sander1095 commented 2 years ago

In any case, I thought some more about this issue and I strongly feel that we should be able to have something like this:

2 routes in app shell, but with the same name and different parameters.

<ShellContent Route="reminders?date=2021-12-13" Title="Yesterday" ContentTemplate="{DataTemplate pages:ReminderPage}"/>
<ShellContent Route="reminders?date=2021-12-14" Title="Today" ContentTemplate="{DataTemplate pages:ReminderPage}"/>

Even without query params, this is currently not possible because routes need to be unique.

Not supporting this scenario would mean that I would need to do something like reminders?date=2021-12-13 and reminders1?date=2021-12-13 to avoid duplicate routes, which feels very silly.

Just some wild guesses here without any proof, I might misunderstand things completely.

I haven't really looked into the codebase, but I feel like this could be pretty difficult? my guess is that the routes from the XAML are retrieved and saved in a unique list somewhere during startup, which would mean that this would not be achievable. To go a bit further, i feel like underwater something likes this would happen:

Routing.RegisterRoute("reminders", typeof(RemindersPage));
Routing.RegisterRoute("reminders", typeof(RemindersPage)); // Error: Duplicate route

If other devs also agree with me that duplicate routes should be allowed in this case (or perhaps depending on a specific condition*), I think that this might need to become a different issue entirely (or this issue should be scoped better) and that that issue should be done FIRST.


codemonkey85 commented 2 years ago

I strongly agree that this should be something we can do.

streetwiseherc commented 2 years ago

Looking at ShellContent.cs in the .NET MAUI source code, I wonder what the purpose of the QueryAttributesProperty property is?

I wonder if that method was marked as public and the ShellRouteParameters class was also public if you could specify QueryAttributes?

The IShellContentController.GetOrCreateContent() method of ShellContent looks like it would honor those attributes with this code:

if (GetValue(QueryAttributesProperty) is ShellRouteParameters delayedQueryParams)
    result.SetValue(QueryAttributesProperty, delayedQueryParams);

The code for Xamarin.Forms is roughly the same, but uses a Dictionary<> instead of the ShellRouteParameters class.

SailDev commented 1 year ago

Since this is still a problem, i am using the following concept as a workaround:

            // Different routes for the same page
            Routing.RegisterRoute("page1", typeof(APage));
            Routing.RegisterRoute("page2", typeof(APage));

            // Subscribe shell Navigating event
            Shell.Current.Navigating += OnNavigating;

            private void OnNavigating(object sender, ShellNavigatingEventArgs e)
            {
                // Get page viewmodel
                var viewModel = ((Shell.Current.CurrentPage).BindingContext) as APageViewModel;

                // Set a property, depending of the target
                if (e.Target.Location.Equals("//page1"))
                {
                    viewModel.Info = "APage 1";
                }
                else if (e.Target.Location.Equals("//page2"))
                {
                    viewModel.Info = "APage 2";
                }
            }
Artiom-Evs commented 1 year ago

Hello! I also faced this problem. While debugging, I found the following feature: When I bind data to ShellContent, that binding is passed to the associated page. This allowed me to pass required data to the rendered page. I use it like this:

AppShell.xaml:

<Shell ...>
    ...
    <ShellContent
        BindingContext="{x:Static models:Schedules.GroupsB1}"
        Title="{Binding Title}"
        Route="GroupsB1"
        ContentTemplate="{DataTemplate pages:SchedulesListPage}" />
    <ShellContent
        BindingContext="{x:Static models:Schedules.GroupsB2}"
        Title="{Binding Title}"
        Route="GroupsB2"
        ContentTemplate="{DataTemplate pages:SchedulesListPage}" />
    ...
</Shell>

Schedules.cs:

public record Schedules
{
    private Schedules() { }

    public string Title { get; init; }
    // some other properties

    public static Schedules GroupsB1 =>
        new Schedules { Title = "Groups (Building 1)" };

    public static Schedules GroupsB2 =>
        new Schedules { Title = "Groups (Building 2)" };
}

SchedulesListPage.xaml.cs:

public partial class SchedulesListPage : ContentPage
{
    private SchedulesListPageViewModel _viewModel;

    public SchedulesListPage()
    {
        InitializeComponent();
        this.BindingContextChanged += SchedulesListPage_BindingContextChanged;
    }

    private void SchedulesListPage_BindingContextChanged(object sender, EventArgs e)
    {
        if (this.BindingContext is Schedules current)
        {
            this.BindingContext = this._viewModel = new SchedulesListPageViewModel(current);
        }
    }
}
RuddyOne commented 1 year ago

Agreed, this should be built into Shell. I am having to use properties that are not in use to get an enum to pass to the ViewModel.

When navigating via await Shell.Current.GoToAsync you can include parameters, why would this be any different?

angelru commented 1 year ago

I have the same problem, I have a FlyoutItem menu with several ShellContents that use the same ContentTemplate and the same ViewModel but I need to pass a parameter when loading the page and ViewModel, any ideas?

AndreKraemer commented 1 year ago

I'm using this as a workaround @angelru. Maybe this helps. In our case we have a page called CategoryPage and this is used in multiple ShellContents with the same ViewModel. We have to pass a categoryId to the viewmodel however.

This is similar to the solution from @SailDev. However we're parsing the parameter in the target page so that we don't have to listen for navigation events in the shell.

appshell.xaml:

        <ShellContent Title="Ensaladas" 
            ContentTemplate="{DataTemplate menu:CategoryPage}" Route="categories/ensaladas" >
        </ShellContent>
        <ShellContent Title="Sopas" 
            ContentTemplate="{DataTemplate menu:CategoryPage}" Route="categories/sopas">
        </ShellContent>
        <ShellContent Title="Tapas" 
            ContentTemplate="{DataTemplate menu:CategoryPage}" Route="categories/tapas">
        </ShellContent>

CategoryPage.xaml.cs

public partial class CategoryPage : ContentPage
{
    public CategoryPage(CategoryViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = _viewModel = viewModel;
    }

    protected override async void OnNavigatedTo(NavigatedToEventArgs args)
    {
        // Hack: Get the category Id
        _viewModel.CategoryId = GetCategoryIdFromRoute();

        await _viewModel.Initialize();  
        base.OnNavigatedTo(args);
    }

    private int GetCategoryIdFromRoute()
    {
        // Hack: As the shell can't define query parameters
        // in XAML, we have to parse the route. 
        // as a convention the last route section defines the category.
        // ugly but works for now :-(
        var route = Shell.Current.CurrentState.Location
            .OriginalString.Split("/").LastOrDefault();
        return route switch
        {
            "ensaladas" => 1,
            "sopas" => 2,
            "tapas" => 3,
            _ => 0,
        };
    }
angelru commented 1 year ago

It would be important for the shell to have an optional parameter to pass an object at startup: <ShellContent Title="Yesterday" ContentTemplate="{DataTemplate pages:ReminderPage} Parameter="object" />

plppp2001 commented 4 months ago

I strongly agree that this should be something we can do.

Yes I need to be able to do this within my app.