maui
maui copied to clipboard
Shell should support setting queryparameters in a ShellContent instance (in XAML)
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
-
ShellContent.Route
should support the queryparameters - It should be bindable
- if we make the re-use of
reminders
as a route possible (likereminders?date=2021-12-31
andreminders?date=2021-12-30
) (which I believe is currently not allowed), that would also require under-the-hood 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:
- I could make 3 different pages (
YesterdayPage
,TodayPage
andTomorrowPage
) and set the date there and try to make aView
to re-use as much UI logic as possible, but this is super ugly. - 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?
If anyone knows of any current ways to support my scenario, let me know! Perhaps I am wrong about using bindingcontexts?
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?
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:
-
Route="details?something=123"
-
Route="details" RouteParameter="dont_know_an_example"
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.
- with conditions I mean stuff like:
- 2 routes to the same page are allowed as long as they lead to the same page
- Perhaps we need to support REQUIRED query params?
I strongly agree that this should be something we can do.
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.
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";
}
}
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);
}
}
}
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?
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?
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,
};
}
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" />
I strongly agree that this should be something we can do.
Yes I need to be able to do this within my app.