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);
}
Related
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();
I have Masterpage which appear after I click on button.
Here is my code.
App.cs
public App()
{
// The root page of your application
MainPage = new NavigationPage(new Login());
}
Login.cs
public void button(object sender, EventArgs e)
{
MasterDetailPage fpm = new MasterDetailPage();
fpm.Master = new MasterPage(**test.Text**) { Title = "Main Page" }; // You have to create a Master ContentPage()
fpm.Detail = new NavigationPage(new PageOne()); // You have to create a Detail ContenPage()
Application.Current.MainPage = fpm;
}
From login page i'm passing parameter to Masterpage (i.e.) test.text, I want to pass this parameter to all pages in the Mastepage, but I dont know how to do it.
MasterPage.cs
public MasterPage(string id)
{
InitializeComponent();
BindingContext = new MasterViewModel();
}
MasterViewModel.cs
public ICommand NavigationCommand
{
get
{
return new Command((value) =>
{
// COMMENT: This is just quick demo code. Please don't put this in a production app.
var mdp = (Application.Current.MainPage as MasterDetailPage);
var navPage = mdp.Detail as NavigationPage;
// Hide the Master page
mdp.IsPresented = false;
switch(value)
{
case "1":
navPage.PushAsync(new PageOne());
break;
case "2":
navPage.PushAsync(new PageTwo());
break;
}
});
}
}
So basically what I need is after login I want to pass id to Masterpage, so all pages in Masterpage can get that id and based on id I can work on this project.
You can use the messaging center.
You send like this :
MessagingCenter.Send<MainPage> (this, "Hi");
You receive like this :
MessagingCenter.Subscribe<MainPage> (this, "Hi", (sender) => {
// do something whenever the "Hi" message is sent
});
And don't forget to unsubscribe :
MessagingCenter.Unsubscribe<MainPage> (this, "Hi");
I am currently attempting to implement DayPilot Lite MVC on my project. I've set up my events and they are clickable(tested with a Debug statement). However what i am looking to do is process the click and load a new View, using a ViewModel built from the id of the event I clicked.
public ActionResult Booking()
{
ApplicationDbContext db = new ApplicationDbContext();
int id = Dpc.eventID;
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Lesson lesson = db.Lessons.Find(id);
if (lesson == null)
{
return HttpNotFound();
}
return View(lesson);
}
class Dpc : DayPilotCalendar
{
public int eventID;
protected override void OnInit(InitArgs e)
{
ApplicationDbContext db = new ApplicationDbContext();
Events = from ev in db.Lessons select ev;
DataIdField = "ClassID";
DataTextField = "ClassLevel";
DataStartField = "ClassStartDate";
DataEndField = "ClassEndDate";
Update();
}
protected override void OnEventClick(EventClickArgs e)
{
//Test to check the method was firing
Debug.WriteLine("Hey I clicked you");
//Parse the event ID for processing
eventID = int.Parse(e.Id);
//Redirect to the booking page
Redirect("Schedule/Booking");
}
}
When I tried to code the Booking method, it resulted in telling me that Dpc.eventID needs an object reference. I have tried making the class and the variable public, but the error persists. I can't make the eventClick public as then i can't override the original. I'm using Redirect because if i try any other link it fails for the same reason. I'm guessing it's because it's a non-inherited subclass, but should i not still be able to access its member attributes if they have a public scope?
I have 2 classes and want to send object using Messenger while navigating from page to another and it works but only when navigate to the page and come back then try again not from first try.
ManivViewModel code:
public void GoToDetial(object parameter)
{
try
{
var arg = parameter as ItemClickEventArgs;
var item = arg.ClickedItem as Item;
Messenger.Default.Send<Item>(item, "Mess");
_navigationService.Navigate(typeof(DescriptionPage));
}
catch { }
}
DescriptionViewModel code:
public DescriptionViewModel(IRSSDataService rssService, INavigationService navigationService, IDialogService dialogService)
{
_dataService = rssService;
_navigationService = navigationService;
_dialogService = dialogService;
load();
LoadCommand = new RelayCommand(load);
GoToUrlCommand = new RelayCommand<object>(GoToUrl);
ShareSocialCommand = new RelayCommand(ShareSocial);
}
private void load()
{
Messenger.Default.Register<Item>(
this,
"Mess",
selectedItem =>
{
Item = selectedItem;
// Prepare content to share
RegisterForShare();
GetFromHTML(Item.Link);
});
}
I found it. I just need to pass in "true" to the Register call in the SimpleIoc to create the instance of the DescriptionViewModel immediately like this
SimpleIoc.Default.Register<DescriptionViewModel>(true);
I have to click twice to be able to receive a message in my messenger what am I doing wrong? Am I not initiating the Messenger correctly I think ??
In my ViewModelLocator class:
public class ViewModelLocator
{
static ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<IPortabilityFactory, PortabilityFactory>();
SimpleIoc.Default.Register<INavigationService, NavigationService>();
SimpleIoc.Default.Register<IMessenger>((() => (IMessenger)new Messenger()));
bla bla...
}
bla bla...
In my Navigation class i have:
public class NavigationService : INavigationService
{
private IMessenger _messenger { get; set; }
public NavigationService(IMessenger messenger)
{
_messenger = messenger;
}
bla bla...
public void NavigateToViewByChannelPage(Channel parameter)
{
//Sending info to ViewModel
_messenger.Send(new IdParameter() { Id = parameter.Id }, typeof(ViewByChannelPageViewModel));
//Going to page
this.Navigate(typeof(ViewByChannelPage));
}
bla bla...
and here is my first VM that calls the NavigationService and that navigates me to my page class:
private void NavigateToChannel(Channel chan)
{
if (chan != null)
{
SimpleIoc.Default.GetInstance<INavigationService>().NavigateToViewByChannelPage(chan);
}
}
And here is my second VM that should receive the message page:
public ViewByChannelPageViewModel(IMessenger messenger, IPortabilityFactory factory)
: base(messenger)
{
_factory = factory;
//Getting value passed
messenger.Register<IdParameter>(this, typeof(ViewByChannelPageViewModel), message => UpdateContentPane(message.Id));
}
private async void UpdateContentPane(string videoId)
{
Bla bla....
}
Basically I have to go twice to the same page for the messenger to work for the first time, then once I have done this the messenger will work perfectly.
It would seem that I have to initiate the messenger? has any one had this problem before? As you can see I am new at using MVVM Light...
Thank for your help!
If you need more code ask away!
The problem came from my navigation class, i needed to initialize my ViewByChannelPageViewModel before i could pass a value to it:
public void NavigateToViewByChannelPage(Channel parameter)
{
//Must create an instance so that the page can recieve the info
Needed this --> SimpleIoc.Default.GetInstance<ViewByChannelPageViewModel>();
//Sending info to ViewModel
_messenger.Send(new IdParameter() {Id = parameter.Id});
//Going to page
this.Navigate(typeof(ViewByChannelPage));
}
By the way: if you have a better way of navigating or improvements that I could make in this code I am all ears!
Thx