When I navigate from page A to page B, I need to remove page A.
How can I do this with Prism's navigation service in Xamarin Forms?
There are a few scenarios that people run into on this one.
As a common example say you have a LoginPage, and once the user successfully logs in you want to Navigate to the MainPage. Your code might look something like the following:
public class App : PrismApplication
{
protected override async void OnInitialized()
{
await NavigationService.NavigateAsync("LoginPage");
}
protected override void RegisterTypes()
{
Container.RegisterTypeForNavigation<LoginPage>();
Container.RegisterTypeForNavigation<MainPage>();
}
}
public class LoginPageViewModel : BindableBase
{
public DelegateCommand LoginCommand { get; }
private async void OnLoginCommandExecuted()
{
// Do some validation
// Notice the Absolute URI which will reset the navigation stack
// to start with MainPage
await _navigationService.NavigateAsync("/MainPage");
}
}
Now if what you're looking for is some flow where your navigation stack looks like MainPage/ViewA and what you want is MainPage/ViewB and you don't want to reinitialize MainPage, then this is something that we are currently evaluating and wanting to improve this so you could do something like _navigationService.NavigateAsync("../ViewB"). In the mean time what I might suggest is something like this:
public class ViewAViewModel : BindableBase
{
public DelegateCommand ViewBCommand { get; }
private async void OnViewBCommandExecuted()
{
var parameters = new NavigationParameters
{
{ "navigateTo", "ViewB" }
};
await _navigationService.GoBackAsync(parameters);
}
}
public class MainPageViewModel : BindableBase, INavigatedAware
{
public async void OnNavigatingTo(NavigationParameters parameters)
{
if(parameters. GetNavigationMode() == NavigationMode.Back &&
parameters.TryGetValue("navigateTo", out string navigateTo))
{
await _navigationService.NavigateAsync(navigateTo);
return;
}
}
}
Given: "NavigationPage/ViewA/ViewB/ViewC/ViewD"
Navigate from ViewD with:
NavigationService.NavigateAsync("../../../ViewE");
Results in: "NavigationPage/ViewA/ViewE"
Referred from here
Need Prism >= 7.0
Another approach would be to have your page implement INavigationAware and in the OnNavigatedFrom, call Navigatin.RemovePage(this).
I do it that way, it's simpler.
navigationService.NavigateAsync("../PageB");
I am using Prims 7.0.0.396.
Related
I have a problem with this sample from Mvvmcross5 documentation :
public class MyViewModel : MvxViewModel
{
private readonly IMvxNavigationService _navigationService;
public MyViewModel(IMvxNavigationService navigation)
{
_navigationService = navigationService;
}
public async Task SomeMethod()
{
var result = await _navigationService.Navigate<NextViewModel, MyObject, MyReturnObject>(new MyObject());
//Do something with the result MyReturnObject that you get back
}
}
public class NextViewModel : MvxViewModel<MyObject, MyReturnObject>
{
public async Task Initialize(MyObject parameter)
{
//Do something with parameter
}
public async Task SomeMethod()
{
await Close(new MyReturnObject());
}
}
It seems to work with a simple Xamarin app with Mvvmcross project. Now, i need to use it with Xamarin.Forms : the close method is called but after showing the previous page, nothing happen :/
In output : "Requesting presentation change" maybe a clue ?
I am beginning using MVVMLight with Xamarin.Forms and need some help to get the IOC set up to create my first ContentPage in App.GetMainPage.
My ViewModels have constructors like so -
public class NewsType1ViewModel : ViewModelBase
{
private readonly IContentService contentService;
private readonly INavigation navigation;
public List<ContentModel> ContentModels { get; set; }
public NewsType1ViewModel (INavigation navigation, IContentService contentService)
{
this.contentService = contentService;
this.navigation = navigation;
}
My ContentPages have constructors like so -
public partial class NewsType1CP : ContentPage
{
private NewsType1ViewModel vm;
public NewsType1CP (NewsType1ViewModel vm)
{
InitializeComponent ();
I am using a ViewModelLocator class as follows -
public class ViewModelLocator
{
public ViewModelLocator ()
{
ServiceLocator.SetLocatorProvider (() => SimpleIoc.Default);
// Services
if (App.StubMode) {
SimpleIoc.Default.Register<IContentService, ContentServiceStub> ();
} else {
SimpleIoc.Default.Register<IContentService, ContentService> ();
}
// How do I wire up INavigation?
// I could not just register default NavigationPage() as it has 2
// possible constructors so tried [PreferredConstructor] on my derived class
SimpleIoc.Default.Register<INavigation, AppNavigationPage> ();
// View Models
SimpleIoc.Default.Register<NewsType1ViewModel> ();
SimpleIoc.Default.Register<NewsDetailsViewModel> ();
}
public NewsType1ViewModel NewsType1ViewModel {
get {
return ServiceLocator.Current.GetInstance<NewsType1ViewModel> ();
}
}
public NewsDetailsViewModel NewsDetailsViewModel {
get {
return ServiceLocator.Current.GetInstance<NewsDetailsViewModel> ();
}
}
public static void Cleanup ()
{
// TODO Clear the ViewModels
}
}
public class AppNavigationPage : NavigationPage
{
[PreferredConstructor]
public AppNavigationPage ()
{
}
}
My App.cs is "in progress" as follows -
public static class App
{
public static AppNavigationPage Nav;
public static ViewModelLocator Locator = new ViewModelLocator ();
public static bool StubMode = true;
public static Page GetMainPage ()
{
try {
// Want to return a Nav with NewsType1CP as the starting Page
NewsType1CP newsType1CP = new NewsType1CP(ServiceLocator.Current.GetInstance<NewsType1ViewModel> ());
Nav.PushAsync (newsType1CP);
return Nav;
} catch (Exception ex) {
//
Exception baseexception = ex.GetBaseException ();
Debug.WriteLine (baseexception.Message);
}
return null;
}
}
My latest exception is -
Cannot cast from source type to destination type.
Am I barking up the wrong tree trying to supply an INavigation to each of my ViewModels like so?
Update: After a couple of answers showing other ways of controlling the Navigation in Xamarin Forms, I think it would help if someone could clarify why attempting constructor injection for Navigation is not such a good thing.
I think my example is a bit confusing with AppNavigationPage as a static, ideally I would like this to be in the IOC also, so I could just call return ServiceLocator.Current.GetInstance< AppNavigationPage >(), but I had a go with various factory methods and am in the middle of debugging this, so the code is obviously half baked ...
If you want a ready to use solution, instead of MVVM Light, you could try to use Xamarin Forms Labs ViewModel base, it injects a Navigation propety in your ViewModel:
that way you cold do something like this:
public Command NavigateToViewModel
{
get
{
return navigateToViewModel ?? (navigateToViewModel = new Command(
async () => await Navigation.PushAsync<NewPageViewModel>(),
() => true));
}
}
I'm not using MvvmLight, but I can tell you that yes, you're barking up the wrong tree in trying to supply an INavigation to each ViewModel.
The easiest way to achieve what you're trying to do is to use a public static on your App.
public static INavigation Navigation
{
get;
set;
}
public static Page GetMainPage()
{
var firstPage = new NavigationPage(new MyRootPage())
{
Navigation = firstPage.Navigation;
return firstPage;
}
}
Now this falls apart when you're using a MasterDetail page because you need your Navigation to wrap your DetailPage, not your MasterDetailPage. Therefore, don't set Navigation in the GetMainPage method, but instead from within the MD Page.
var master = new MainMenuPage();
var detail = new NavigationPage(new FirstPage());
if (App.Navigation == null)
{
App.Navigation = detail.Navigation;
}
Master = master;
Detail = detail;
Oh ok, when looking better at your code i maybe spotted the problem:
SimpleIoc.Default.Register<INavigation, AppNavigationPage> ();
Shoult it be:
SimpleIoc.Default.Register<INavigation, AppNavigationPage.Navigation> ();
Is there anyway to define and add method filters for hub functions (like ActionFilters in mvc)
I mean something like this :
public class MyHub : Hub
{
[Log]
public string RegisterUser(UserModel model){
...
}
}
where I can do some control inside the LogAttribute implementation.
You should be able to achieve similar functionality to action filters in ASP.NET MVC by using SignalR's Hub pipeline:
public class LoggingPipelineModule : HubPipelineModule
{
protected override bool OnBeforeIncoming(IHubIncomingInvokerContext context)
{
Debug.WriteLine("Invoking '{0}.{1}({2})'.",
context.MethodDescriptor.Hub.Name,
context.MethodDescriptor.Name,
string.Join(", ", context.Args));
return base.OnBeforeIncoming(context);
}
protected override object OnAfterIncoming(object result, IHubIncomingInvokerContext context)
{
Debug.WriteLine("Finished Invoking '{0}.{1}'. Returned '{2}'.",
context.MethodDescriptor.Hub.Name,
context.MethodDescriptor.Name,
result);
return base.OnAfterIncoming(result, context);
}
}
If you only want to log for methods with a custom attribute attached, you can check for your custom attribute before logging:
protected override bool OnBeforeIncoming(IHubIncomingInvokerContext context)
{
if (context.MethodDescriptor.Attributes.OfType<MyAttribute>().Any())
{
// Log here.
}
return base.OnBeforeIncoming(context);
}
You can register your module before your call to MapSignalR:
public void Configuration(IAppBuilder app)
{
GlobalHost.HubPipeline.AddModule(new LoggingPipelineModule());
app.MapSignalR();
}
I have following logic in Admin screen. I need similar logic in Logs screen also. Hence I am planning to move this logic into base page. In base page, how do I recognize the current page? (How do I distinguish between Admin screen and Logs screen?).
Based on the page the value retrieved from the config is different.
What are the different ways to achieve this? What is the best way out of these approaches?
//Admin Screen
List<string> authorizedRoles = new List<string>((ConfigurationManager.AppSettings["AdminScreenRoles"]).Split(','))
if (!authorizedRoles.Contains(userRole))
{
Response.Redirect("UnauthorizedPage.aspx");
}
//Logs Screen
List<string> authorizedRoles = new List<string>((ConfigurationManager.AppSettings["LogsScreenRoles"]).Split(','))
if (!authorizedRoles.Contains(userRole))
{
Response.Redirect("UnauthorizedPage.aspx");
}
Don't put code in base that recognize the class that inherit it. Add an abstract property that the child will have to override.
In base:
public abstract string AppSettingsRolesName { get; }
List<string> authorizedRoles = new List<string>((ConfigurationManager.AppSettings[AppSettingsRolesName]).Split(','))
if (!authorizedRoles.Contains(userRole))
{
Response.Redirect("UnauthorizedPage.aspx");
}
In Logs:
public override string AppSettingsRolesName
{
get { return "LogsScreenRoles"; }
}
In Admin:
public override string AppSettingsRolesName
{
get { return "AdminScreenRoles"; }
}
The easiest way would be to look into forms authentication, as it will handle all of this for you through a configuration file. There are a number of good articles on this dotted around the web - here's one:
http://ondotnet.com/pub/a/dotnet/2003/01/06/formsauthp1.html
However, if you're looking for a quick fix, the easiest way is to move your code into the base page as you said, and use an interface property to make inherited pages indicate what role type to use - e.g. something along the lines of:
public abstract class BasePage : Page
{
protected abstract string AuthorisedRoles { get; }
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
List<string> authorizedRoles = new List<string>((ConfigurationManager.AppSettings[this.AuthorisedRoles]).Split(','));
if (!authorizedRoles.Contains(userRole))
{
Response.Redirect("UnauthorizedPage.aspx");
}
}
}
public class LogsPage : BasePage
{
protected override string AuthorisedRoles
{
get { return "LogsScreenRoles"; }
}
}
public class AdminPagePage : BasePage
{
protected override string AuthorisedRoles
{
get { return "AdminScreenRoles"; }
}
}
But seriously, look into forms authentication if you want to do it properly - it's not as complicated as it first looks.
public abstract class MyControllerBase : Controller
{
protected override void OnActionExecuting(ActionExecutingContext context)
{
// do some magic
}
}
All of my controllers inherit from MyControllerBase. The problem is that now I can't unit test certain methods because the filter sets some authorisation/logic flags which influence code path.
Is there any way to manually trigger OnActionExecuting? How does the pipeline trigger these events?
EDIT: to show a little more the idea behind this design in response to comments. I basically have something like this:
public abstract class MyControllerBase : Controller
{
protected override void OnActionExecuting(ActionExecutingContext context)
{
UserProperties =
_userService
.GetUserProperties(filterContext.HttpContext.User.Identity.Name);
ViewBag.UserProperties = UserProperties;
}
public UserProperties { get; private set; }
public bool CheckSomethingAboutUser()
{
return UserProperties != null
&& UserProperties.IsAuthorisedToPerformThisAction;
}
// ... etc, other methods for querying UserProperties
}
So now anywhere in View or Controller I can get details of the current user, what is their email, what authorisation they have, which department they work for etc.
Example:
public class PurchasingController : MyControllerBase
{
public ActionResult RaisePurchaseOrder(Item item)
{
// can use UserProperties from base class to determine correct action...
if (UserProperties.CanRaiseOrders)
if (UserProperties.Department == item.AllocatedDepartment)
}
}
So this design works really nice, but as you can see testing the above action is difficult as I can't directly manipulate the UserProperties in the test set up.
I'm not sure you're suppose to override OnActionExecuting like that in MCV, normally I make an ActionFilterAttribute
public class SomeMagicAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
}
}
Then your class:
[SomeMagic]
public abstract class MyControllerBase : Controller
{
}
Then in your unit test you can just do
var magic = new SomeMagicAttribute();
var simulatedContext = new ActionExecutingContext();
magic.OnActionExecuting(simulatedContext);