MAUI: Relative routing to shell elements is currently not supported - c#

I recreated a new app maui and to make the navigation I redid what worked on another app that I made (on the other app it still works). As soon as I navigate on a new page everything goes well but it's as soon as I want to come back on the page where I was or I have a bug.
NavigationService.cs
public async Task NavigateAsync(NavigationPages page)
{
try
{
await Shell.Current.GoToAsync(page.ToString(), true);
}
catch (Exception ex)
{
}
}
ViewModel.cs
public ICommand GoBacKCommand { get; }
public ParamViewModel(INavigationService navigationService)
{
GoBacKCommand = new Command(GoBacKExecute);
this.navigationService = navigationService;
}
private void GoBacKExecute()
{
navigationService.NavigateAsync(BusinessModels.NavigationPages.MainPage);
}
MauiProgram.cs
Routing.RegisterRoute(nameof(MainPage), typeof(MainPage));
services.AddTransient<MainPage>();
services.AddTransient<MainViewModel>();
and the Exception is : Relative routing to shell elements is currently not supported. Try prefixing your uri with ///: ///MainPage
but if I do what it says (///MainPage) the navigation does not work anymore
How can I solve this problem?

If you want go back to a page (like PopUp) you need to need use GotoAsync as below
ViewModel.cs
public ICommand Back_Command { get; set; }
public ViewModel()
{
Back_Command = new Command(Back);
}
private async void Back()
{
await Shell.Current.GoToAsync("..");
}
Page.xaml
<Shell.BackButtonBehavior>
<BackButtonBehavior
Command="{Binding Back_Command}"/>
</Shell.BackButtonBehavior>
Further explanation has been stated in this documentation https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/shell/navigation

Related

Blazor updated Component Parameter is delayed

I have a page, which looks something like this:
<div>
<MyToggle ToggleState="#ToggleState" ToggleStateChanged="#OnToggleStateChanged"/>
<MyData ToggleState="#ToggleState"/>
</div>
public partial class MyToggle
{
[Parameter]
public EventCallback<bool> ToggleStateChanged { get; set; }
[Parameter]
public bool ToggleState { get; set; }
private async Task OnButtonClicked()
{
this.ToggleState = !this.ToggleState;
await this.ToggleStateChanged.InvokeAsync(this.ToggleState);
}
}
public partial class MyData
{
private IObservableCollection<Data> Data;
[Parameter]
public bool ToggleState { get; set; }
[Inject]
protected IRefreshService RefreshService { get; set; }
protected override async Task OnInitializedAsync()
{
this.RefreshService.OnRefresh += async () => await this.RefreshData();
await base.OnInitializedAsync();
await this.RefreshData();
}
private override async Task<int> LoadData()
{
if (this.ToggleState)
{
return await this.Logic.GetDataA();
}
return await this.Logic.GetDataB(this.Company);
}
private async Task RefreshData()
{
this.Data = await this.LoadData();
}
}
public partial class MyPage
{
private bool ToggleState { get; set; }
[Inject]
private IRefreshService RefreshService { get; set; }
private async Task OnToggleStateChanged(bool toggleState)
{
this.ToggleState = toggleState;
await this.RefreshService.Refresh();
}
}
Basically, I want to toggle, which data is loaded. As soon as the toggle state changes, the an event is emitted, which forces all child components to load their respective data. The problem I have is, that as soon as I press the toggle button, MyPage.OnToggleStateChanged gets called. This is expected and works perfectly. The parameter of said method is always correct. As you can see, the property MyPage.ToggleState is then set to the correct value. Afterwards, the event to refresh the data is emitted (also expected). When I set a breakpoint at MyData.RefreshData the method is now called as intended. Therefore, the workflow is correct.
The "funny" thing is, that as soon as I set a breakpoint at MyData.LoadData, MyData.ToggleState still holds the old value. I observed this with a breakpoint and the property MyData.ToggleState is set AFTER MyData.RefreshData is called. For me this does not make any sense, that there is a small delay.
I tried using StateHasChanged in MyPage.OnToggleStateChanged after the MyPage.ToggleState is set, but this does not make any difference. The only thing which works is, to make a small delay of 10ms after I have set the value.

Login Screen before Master Detail View in Xamarin

I am trying to write a xamarin app that will display a login page before a master detail page but I am running into issues.
Right now I have my app.xaml calling an appbootstrapper as follows:
public App()
{
this.InitializeComponent();
RxApp.SuspensionHost.CreateNewAppState = () => new AppBootstrapper();
RxApp.SuspensionHost.SetupDefaultSuspendResume();
this.MainPage = RxApp.SuspensionHost
.GetAppState<AppBootstrapper>()
.CreateMainPage();
}
/// <summary>Gets the Router associated with this Screen.</summary>
public RoutingState Router { get; } = new RoutingState();
With the app bootstrapper as follows:
public class AppBootstrapper : ReactiveObject, IScreen
{
public AppBootstrapper(IMutableDependencyResolver dependencyResolver = null)
{
SetupLogging();
this.RegisterParts(dependencyResolver ?? Locator.CurrentMutable);
this.Router.Navigate.Execute(new LoginPageViewModel(this));
}
/// <summary>Gets the Router associated with this Screen.</summary>
public RoutingState Router { get; } = new RoutingState();
public Page CreateMainPage()
{
return new RoutedViewHost();
}
private static void SetupLogging()
{
var logger = new Logger { Level = LogLevel.Debug };
Locator.CurrentMutable.RegisterConstant(logger, typeof(ILogger));
}
private void RegisterParts(IMutableDependencyResolver dependencyResolver)
{
dependencyResolver.RegisterConstant(this, typeof(IScreen));
dependencyResolver.Register(() => new LoginPage(), typeof(IViewFor<LoginPageViewModel>));
dependencyResolver.RegisterConstant(new LoginService(), typeof(ILoginService));
}
}
This gets me to my login screen no problem, and I can perform my login operation. Then, once login is successful, I try to navigate to the master detail page, but this is where I run into issues.
public LoginPageViewModel(IScreen screen)
{
this.loginService = Locator.Current.GetService<ILoginService>();
this.HostScreen = screen ?? Locator.Current.GetService<IScreen>();
this.PrepareObservables();
}
........................................................
private void PrepareObservables()
{
...
this.LoginCommand = ReactiveCommand.CreateFromTask(
async execute =>
{
var loginSuccessful = await this.loginService.Login(this.Username, this.Password);
if (loginSuccessful)
{
this.HostScreen.Router.NavigateBack.Execute().Subscribe();
}
}, canExecuteLogin);
...
You can see that my login command is trying to perform a navigate and reset to go to the Main Page (which is my master detail page). This is not working and is resulting in an unhandled exception stating:
An object implementing IHandleObservableErrors has errored, thereby breaking its observable pipeline. To prevent this, ...>
Does anyone know what to do here? I need a good pattern for handling the use case of Login -> Master Detail Page in Xamarin Forms using ReactiveUI. Thanks.
this.LoginCommand = ReactiveCommand.CreateFromTask(
async execute =>
{
var loginSuccessful = await this.loginService.Login(this.Username, this.Password);
if (loginSuccessful)
{
this.HostScreen.Router.NavigateBack.Execute().Subscribe();
}
}, canExecuteLogin);
The above code is navigating back on successful login. I think you mean to use Router.NavigateAndReset.Execute(new MainPageViewModel()).Subscribe();

Task async Call not returning creates deadlock

I am very new to c# so this could be very easy. I am busy creating a xamarin forms app and need the data for the next page on the UI. Its a tabbeb page and all 3 needs the info that will be returned by the base class.
The problem I am having is that I need to call google api service for company info. Due to this I created a asynchronous call. So now the code returns due to this. Due to the tabbed page I need the data to bind to the screen and I now need to wait for the data.So basically need it to be synchronous.
I have tried everything I could find on this topic. Maybe the way I am doing this is wrong but hopefully my code will show that.
This is the Tabbed Page:
public BusinessTabbedPage(string PlaceId)
{
Children.Add(new BusinessSpecialsPage(PlaceId));
InitializeComponent();
}
This will be one of the pages on the app that calls the viewmodel
public BusinessSpecialsPage(string PlaceId)
{
BindingContext = new BusinessSpecialsPageViewModel(PlaceId);
InitializeComponent();
}
Due the 3 pages needing the same data I created a base class. This will get the data and pass everything back to the UI.
public BusinessBaseViewModel(string placeId)
{
Task<List<CompanyProfileModel>> task = GBDFromGoogle(placeId);
task.Wait();
}
public async Task<List<CompanyProfileModel>> GBDFromGoogle(string PlaceId)
{
var info = await ApiGoogle.GetGoogleCompanySelectedDetails(PlaceId);
var Companyresult = info.result;
CompanyProfileModel CompList = new CompanyProfileModel
{
ContactDetails = Companyresult.formatted_phone_number,
Name = Companyresult.name,
Website = Companyresult.website,
};
ComPF.Add(CompList);
return ComPF;
}
This is the api call which i think is adding a new task and then the process deadlocks?
public static async Task<GoogleSelectedPlaceDetails> GGCSD(string place_id)
{
GoogleSelectedPlaceDetails results = null;
var client = new HttpClient();
var passcall = "https://maps.googleapis.com/maps/api/place/details/json?placeid=" + place_id + "&key=" + Constants.GoogleApiKey;
var json = await client.GetStringAsync(passcall);
//results = await Task.Run(() => JsonConvert.DeserializeObject<GoogleSelectedPlaceDetails>(json)).ConfigureAwait(false);
results = JsonConvert.DeserializeObject<GoogleSelectedPlaceDetails>(json);
return results;
}
I need the process not to deadlock. It needs to wait for the tasks to be done so that I can return the data to the screen.
Task.Wait will block the current thread so you should never use it in Xamarin applications and even less in the constructor, otherwise your application will freeze until you receive some data which might never happen if the service is down or the user loses connection.
Instead of calling the data in your ViewModel's constructor you could initialize it on the Appearing event of your ContentPage view.
To do that, you could create a custom behaviour or even better, you can use the following library which already does this for you:
Using NuGet: Install-Package Behaviors.Forms -Version 1.4.0
Once you have installed the library, you can use the EventHandlerBehavior to wire events to ViewModel commands, for example:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:behaviors="clr-namespace:Behaviors;assembly=Behaviors"
xmlns:viewModels="clr-namespace:YourApp.ViewModels"
Title="Business Page"
x:Class="YourApp.Views.BusinessPage">
<ContentPage.BindingContext>
<viewModels:BusinessViewModel />
</ContentPage.BindingContext>
<ContentPage.Behaviors>
<behaviors:EventHandlerBehavior EventName="Appearing">
<behaviors:InvokeCommandAction Command="{Binding AppearingCommand}" />
</behaviors:EventHandlerBehavior>
</ContentPage.Behaviors>
[...]
The ViewModel:
public BusinessBaseViewModel(string placeId)
{
AppearingCommand = new Command(Appearing);
PlaceId = placeId;
}
public ICommand AppearingCommand { get; private set; }
public string PlaceId { get; private set; }
private ObservableCollection<CompanyProfileModel> _googleGbd;
public ObservableCollection GoogleGbd
{
get { _googleGbd?? (_googleGbd = new ObservableCollection<CompanyProfileModel>()); };
set
{
if (_googleGdb != value)
{
_googleGdb = value;
NotifyPropertyChanged();
}
}
}
private async void Appearing()
{
var companyResult = await ApiGoogle.GetGoogleCompanySelectedDetails(PlaceId);
CompanyProfileModel companyProfile = new CompanyProfileModel
{
ContactDetails = companyResult.formatted_phone_number,
Name = companyResult.name,
Website = companyResult.website,
};
GoogleGbd.Add(companyProfile);
}
If you only want the data to be loaded the first time your View appears, then you can add a bool flag to know that you have already loaded the data.

Adding Api interface in ViewModel constructor and navigation stops working Prism

I'm using VS 17 for Xamarin Forms. I've set up Prism in my Xamarin.Forms app and I just added a reference to my Api interface (in ViewModel Constructor) and it makes the app stop navigation to the second page. I need to do this in order to pass parameters etc. I followed this guide:
https://blog.qmatteoq.com/prism-for-xamarin-forms-basic-navigation-and-dependency-injection-part-2/
This is what I did to make the navigation stop working:
private readonly IService _Service;
private ObservableCollection<TodoItem> _topSeries;
public ObservableCollection<TodoItem> TopSeries
{
get { return _topSeries; }
set { SetProperty(ref _topSeries, value); }
}
This is the constructor:
public SecondPageViewModel(IService Service, INavigationService navigationService)
{
_Service = Service;
_navigationService = navigationService;
}
So I cant even reach the above viewmodel because of the above code that I added. I tried to put break points on the DelegateCommand (on first ViewModel) but it just stops after InitializeComponent(); and then nothing happens. No error messages! Thanks!
Update:
My Service class that fetches data:
public class Service : IService
{
public List<TodoItem> TodoList { get; private set; }
HttpClient client;
Service()
{
client = new HttpClient();
client.MaxResponseContentBufferSize = 256000;
}
public async Task<List<TodoItem>> DataAsync()
{
TodoList = new List<TodoItem>();
var uri = new Uri(string.Format(Constants.RestUrl, string.Empty));
try
{
var response = await client.GetAsync(uri);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
TodoList = JsonConvert.DeserializeObject<List<TodoItem>>(content);
Debug.WriteLine(content);
}
}
catch (Exception ex)
{
Debug.WriteLine(#"ERROR {0}", ex.Message);
}
return TodoList;
}
}
This is my App.Xaml.cs
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<NavigationPage>();
containerRegistry.RegisterForNavigation<View.MainPage, MainPageViewModel>();
containerRegistry.RegisterForNavigation<View.SecondPage, SecondPageViewModel>();
containerRegistry.Register<IService, Service>();
}
My Interface:
public interface IService
{
Task<List<TodoItem>> DataAsync();
}
This is how I navigate (click from listview):
private EventItem _selectedEvent { get; set; }
public EventItem SelectedEvent
{
get { return _selectedEvent; }
set
{
if (_selectedEvent != value)
{
if (Device.RuntimePlatform == Device.iOS)
{
_selectedEvent = null;
}
else
{
_selectedEvent = value;
}
NavigationParameters navParams = new NavigationParameters();
navParams.Add("PassedValue", _todoItem.name);
_navigationService.NavigateAsync("SecondPage", navParams);
}
}
}
Edit:
When I debug without the ApiService code the command is taking me to new new constructor in the new viewmodel. With the code it does not reach the contructor.
According to your code you have declared constructor like this:
Service()
{
// ...
}
You didn't set access modifier, therefore the default one is internal. Here is the definition:
Internal types or members are accessible only within files in the same
assembly.
Most likely you have your Service.cs declared in another Assembly and Prism can't access its constructor.
Your navigation doesn't work because dependency injection fails. To fix it, just change your access modifier to public:
public Service()
{
// ...
}

Xamarin Forms MVVM Clear Navigation Stack from INavigationService

The project I am working on contains the following structure:
When app is launched, user sees a Welcome page. At that point user has two options. They can either login or register. If logged in == true; then go to master detail page. Or in registration, if register == success then go to login page and follow the same process and end up in the master detail page.
-> Login Page ||
Welcome Page >> ================== || => MasterDetailPage
-> Register Page -> Login page ||
I am using MVVM Light to handle my navigation stack via INavigationService as my UI and business logic is separated via MVVM. Everything works pretty good except for I need to reset the navigation stack so the user will not be able to access any page before the "MasterDetailPage" showed above. Right now users can go back to login or registration or whatever page they were before, by using the hardware back button on Android or swiping from the left edge on iOS. Plus, There is a navigation back button on top navigation bar anyway.
My App.cs looks something like this
public App()
{
var nav = RegisterNavigationService();
SimpleIoc.Default.Register<INavigationService>(() => nav);
InitializeComponent();
var initialPage = new NavigationPage(new WelcomePage());
nav.Initialize(initialPage);
MainPage = initialPage;
}
private NavigationService RegisterNavigationService()
{
var nav = new NavigationService();
nav.Configure(Locator.LoginForm, typeof(LoginForm));
nav.Configure(Locator.RegisterSuccessPage, typeof(RegisterSuccessPage));
nav.Configure(Locator.RegistrationForm, typeof(RegistrationForm));
nav.Configure(Locator.WelcomePage, typeof(WelcomePage));
nav.Configure(Locator.MasterMainPage, typeof(MasterMainPage));
return nav;
}
On my view models, I handle the navigation commands like this:
public class LoginFormViewModel : BaseViewModel
{
private readonly INavigationService _navigationService;
public Command NavigateToMainPage { get; }
public LoginFormViewModel(INavigationService navigationService)
{
_navigationService = navigationService ?? throw new ArgumentNullException("navigationService");
NavigateToMainPage = new Command(() => NavigateToMainApp());
}
private void NavigateToMainApp()
{
_navigationService.NavigateTo(Locator.MasterMainPage);
}
}
Finally, my NavigationService.cs looks like this... I barely touched this part of the code... The only thing I tried is the 'ClearNavigationStack' method but that was a failure.
public class NavigationService : INavigationService, INavigationServiceExtensions
{
private Dictionary<string, Type> _pagesByKey = new Dictionary<string, Type>();
private NavigationPage _navigation;
public string CurrentPageKey
{
get
{
lock (_pagesByKey)
{
if (_navigation.CurrentPage == null)
{
return null;
}
var pageType = _navigation.CurrentPage.GetType();
return _pagesByKey.ContainsValue(pageType)
? _pagesByKey.First(p => p.Value == pageType).Key
: null;
}
}
}
public void GoBack()
{
_navigation.PopAsync();
}
public void NavigateTo(string pageKey)
{
NavigateTo(pageKey, null);
}
public void NavigateTo(string pageKey, object parameter)
{
lock (_pagesByKey)
{
if (_pagesByKey.ContainsKey(pageKey))
{
ConstructorInfo constructor;
object[] parameters;
var type = _pagesByKey[pageKey];
if (parameter == null)
{
constructor = type.GetTypeInfo()
.DeclaredConstructors
.FirstOrDefault(c => !c.GetParameters().Any());
parameters = new object[] { };
}
else
{
constructor = type.GetTypeInfo()
.DeclaredConstructors
.FirstOrDefault(
c =>
{
var p = c.GetParameters();
return p.Count() == 1
&& p[0].ParameterType == parameter.GetType();
});
parameters = new[] { parameter };
}
if (constructor == null)
{
throw new InvalidOperationException("No suitable constructor found for page " + pageKey);
}
var page = constructor.Invoke(parameters) as Page;
_navigation.PushAsync(page);
}
else
{
throw new ArgumentException(
string.Format("No such page: {0}. Did you forget to call NavigationService.Configure?", pageKey), "pageKey");
}
}
}
public void Configure(string pageKey, Type pageType)
{
lock (_pagesByKey)
{
if (_pagesByKey.ContainsKey(pageKey))
{
_pagesByKey[pageKey] = pageType;
}
else
{
_pagesByKey.Add(pageKey, pageType);
}
}
}
public void ClearNavigationStack()
{
lock (_pagesByKey)
{
foreach (var pageKey in _pagesByKey.Keys)
{
_pagesByKey.Remove(pageKey);
}
}
}
public void Initialize(NavigationPage navigation)
{
_navigation = navigation;
}
}
I've taken this bit from the following git repo: https://github.com/mallibone/MvvmLightNavigation.XamarinForms
by following this tutorial:
https://mallibone.com/post/xamarin.forms-navigation-with-mvvm-light
Note: It is a PCL.
Any suggestion is welcome as I've been on this for the last 2 days.
EDIT: Just now, I've managed to "hide" the nav stack by setting my MainPage to something like this
App.Current.MainPage = new MasterMainPage();
But it seems like a code smell and looks like a horrific hack. Plus I am not too sure if it "violates" the concepts I am following... And I guess this navigation stack will never be gone anyway as I will do other navigation stacks inside the master detail pages.
From your picture I see that you have Master/Detaied page inside Navigation page. Xamarin doesn't recommend to do that. I don't know how you are going to do it in MVVM Light but in regular Forms you have couple options to achieve what you want:
If you ever need to go back to your Login or register page you should use
await Navigation.PushModalAsync(new YourMasterDetailPage());
Then you can popmodal to get back to them BUT in this case Hardware button will still bring you to Login. You can use part of method 2 to clear stack after you navigated to you master-detail page but be careful - you cannot remove a page from stack if it is root and currently displayed page, so you will need to clear regular navigation stack only after login page is not displayed.
I wouldn't recommend that option as "Modal views are often temporary and brought on screen only long enough for the user to complete a task."
http://blog.adamkemp.com/2014/09/navigation-in-xamarinforms_2.html
If you don't need to go back you can use the follow to clear Navigation stack, it will also remove Back button
await Navigation.PushAsync(new YourMasterPage());
var pages = Navigation.NavigationStack.ToList();
foreach (var page in pages)
{
if (page.GetType() != typeof(YourMasterPage))
Navigation.RemovePage(page);
}

Categories