如何在Xamarin.Forms中切换页面?

117
如何在Xamarin Forms中切换页面?我的主页面是ContentPage,我不想切换到像Tabbed Page这样的东西。我已经通过查找应触发新页面的控件的父级,直到找到ContentPage并使用新页面的控件替换内容来伪实现了它。但这似乎非常笨拙。

1
这个问题已经有很多答案了,如果想看如何使用MVVM结构模式来实现它,请参考这个链接:https://stackoverflow.com/a/37142513/9403963 - Alireza Sattari
如果这个问题已经有了很多答案... 那些答案在哪里?你提供的链接只有一个没有得票甚至没有包含评论的答案,而这个页面确实有帮助的内容,不是你提供的那个链接的页面。 - Windgate
13个回答

74
在 App 类中,您可以将 MainPage 设置为 Navigation Page,并将根页面设置为您的 ContentPage:
public App ()
{
    // The root page of your application
    MainPage = new NavigationPage( new FirstContentPage() );
}

然后在你的第一个 ContentPage 调用中:

Navigation.PushAsync (new SecondContentPage ());

我已经这样做了,但是主页面仍然是默认打开的页面。任何我设置为主页的页面都没有效果。我只能打开第一个设置的页面。问题出在哪里? - Behzad
1
Visual Studio建议导入Android.Content.Res进行导航。这似乎不正确,我应该从哪里导入它? - Christian
如果您想更改默认的主页面,则将“new FirstContentPage”更改为您想要的新主页面。我刚试过了,对我有效。 - thejdah

73

Xamarin.Forms内置支持多个导航宿主:

  • NavigationPage,下一页从侧面滑入,
  • TabbedPage,这是你不喜欢的那个,
  • CarouselPage,可以左右切换到下一页/上一页。

此外,所有页面还支持 PushModalAsync() 方法,该方法只是在现有页面之上推送新页面。

最后,如果您想确保用户无法返回到先前的页面(使用手势或后退硬件按钮),可以保持相同的 Page 显示并替换其 Content

替换根页面的建议选项同样有效,但您必须针对每个平台进行不同的处理。


PushModalAsync似乎是导航的一部分,对吗?我无法弄清如何访问导航对象/类。我假设我需要访问实现INavigation的某些内容,但是是什么呢? - Eric
如果您的页面包含在 NavigationPage 中,则应该能够从页面内访问 Navigation 属性。 - Jason
1
一旦我开始使用NavigationPage,一切都变得顺畅了。谢谢。 - Eric
1
@stephane,请问如果我的第一页是CarouselPage,而第二页是MasterDetailPage,那么我该如何切换页面呢?http://stackoverflow.com/questions/31129845/how-to-navigate-from-carouselpage-to-masterdetailpage - Atul Dhanuka

44

如果您的项目已经设置为PCL表单项目(非常可能也是Shared Forms,但我没有尝试过),那么会有一个名为App.cs的类,其代码如下:

如果您的项目被设置为PCL表单项目(很可能也是共享表单,但我没有尝试过),那么会有一个名为App.cs的类,其代码如下:

public class App
{
    public static Page GetMainPage ()
    {     
        AuditorDB.Model.Extensions.AutoTimestamp = true;
        return new NavigationPage (new LoginPage ());
    }
}

您可以修改GetMainPage方法,以返回在项目中定义的新的选项卡页面或其他页面。

从那里开始,您可以添加命令或事件处理程序来执行代码和操作。

// to show OtherPage and be able to go back
Navigation.PushAsync(new OtherPage());

// to show AnotherPage and not have a Back button
Navigation.PushModalAsync(new AnotherPage()); 

// to go back one step on the navigation stack
Navigation.PopAsync();

3
这不会在页面间切换,它只会改变最初加载的页面。 - dakamojo
你的问题涉及到一个主页面。请查看更新后的答案以获取导航示例。 - Sten Petrov
1
在这个例子中,“Navigation”是什么? - 它是您在某个地方创建的对象吗? - 我在这个代码示例中没有看到它。 - BrainSlugs83
感谢;在我的情况下,FTR中的PushAsync()无法正常工作,而PushModalAsync()可以。 - knocte
无论如何,我不需要回去,所以对我来说没问题。 - knocte
显示剩余2条评论

24

将新页面推送到堆栈中,然后移除当前页面。这将导致切换。

item.Tapped += async (sender, e) => {
    await Navigation.PushAsync (new SecondPage ());
    Navigation.RemovePage(this);
};

您需要先进入导航页面:

MainPage = NavigationPage(new FirstPage());

切换内容并不理想,因为您只有一个大页面和一个页面事件集,例如 OnAppearing 等。


在Android上不支持Navigation.RemovePage();。 - Rohit Vipin Mathews
1
Navigation.RemovePage(page); 在 Android 中可用,但需要先在导航页面中。 - Daniel Roberts
我在我的Forms 1.4.2项目中广泛使用它。也许他们已经修复了这个bug,或者我只是幸运地没有遇到它。 - Daniel Roberts
我正在使用最新版本,而且我能够复制它。所以我认为你也很幸运。 - Rohit Vipin Mathews
2
小提示 - 在更改页面时取消转场效果,只需添加false作为第二个参数:await Navigation.PushAsync(new SecondPage(), false); - Damian Green
来自iOS本地开发人员的内容:将新页面推入堆栈意味着您无需从堆栈中删除任何内容,这就是为什么它被称为堆栈。有时需要能够返回(弹出)-您在堆栈中保留所有视图。从堆栈中删除可能会导致巨大问题。 - Async-

8
如果您不想返回上一页,也就是不让用户在授权完成后返回登录界面,则可以使用以下代码:
 App.Current.MainPage = new HomePage();

如果您想启用返回功能,请使用以下代码:
Navigation.PushModalAsync(new HomePage())

7
似乎这个主题非常受欢迎,如果不提到另一种方法——“ViewModel First Navigation”,就会感到遗憾。大多数MVVM框架都在使用它,但是如果您想了解它的含义,请继续阅读。
所有官方的Xamarin.Forms文档都演示了一个简单但略微不纯的MVVM解决方案。这是因为Page(视图)不应该知道任何关于ViewModel的信息,反之亦然。下面是一个很好的违规示例:
// C# version
public partial class MyPage : ContentPage
{
    public MyPage()
    {
        InitializeComponent();
        // Violation
        this.BindingContext = new MyViewModel();
    }
}

// XAML version
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:viewmodels="clr-namespace:MyApp.ViewModel"
    x:Class="MyApp.Views.MyPage">
    <ContentPage.BindingContext>
        <!-- Violation -->
        <viewmodels:MyViewModel />
    </ContentPage.BindingContext>
</ContentPage>

如果您有一个由两个页面组成的应用程序,那么这种方法可能适合您。但是,如果您正在开发一个大型企业解决方案,则最好采用“ViewModel First Navigation”的方法。它略微复杂,但更加清晰,允许您在ViewModels之间导航,而不是在Pages(Views)之间导航。除了明确分离关注点之外,其中一个优点是您可以轻松地将参数传递给下一个ViewModel或在导航后立即执行异步初始化代码。现在进入细节。
首先,我们需要一个地方来注册所有对象,并可选地定义它们的生命周期。为此,我们可以使用IOC容器,您可以自行选择一个。在本例中,我将使用Autofac(它是可用性最快的之一)。我们可以在App中保留对它的引用,以便全局可用(这不是一个好主意,但为了简化需要):
public class DependencyResolver
{
    static IContainer container;

    public DependencyResolver(params Module[] modules)
    {
        var builder = new ContainerBuilder();

        if (modules != null)
            foreach (var module in modules)
                builder.RegisterModule(module);

        container = builder.Build();
    }

    public T Resolve<T>() => container.Resolve<T>();
    public object Resolve(Type type) => container.Resolve(type);
}

public partial class App : Application
{
    public DependencyResolver DependencyResolver { get; }

    // Pass here platform specific dependencies
    public App(Module platformIocModule)
    {
        InitializeComponent();
        DependencyResolver = new DependencyResolver(platformIocModule, new IocModule());
        MainPage = new WelcomeView();
    }

    /* The rest of the code ... */
}

2. 我们需要一个对象负责检索特定ViewModelPage(视图),反之亦然。在设置应用程序的根/主页面时,第二种情况可能很有用。为此,我们应该约定所有ViewModels都应该在ViewModels目录中,而Pages(Views)应该在Views目录中。换句话说,ViewModels应该位于[MyApp].ViewModels命名空间中,而Pages(Views)应该位于[MyApp].Views命名空间中。此外,我们应该约定WelcomeView(页面)应该有一个WelcomeViewModel等。以下是一个映射器的代码示例:

public class TypeMapperService
{
    public Type MapViewModelToView(Type viewModelType)
    {
        var viewName = viewModelType.FullName.Replace("Model", string.Empty);
        var viewAssemblyName = GetTypeAssemblyName(viewModelType);
        var viewTypeName = GenerateTypeName("{0}, {1}", viewName, viewAssemblyName);
        return Type.GetType(viewTypeName);
    }

    public Type MapViewToViewModel(Type viewType)
    {
        var viewModelName = viewType.FullName.Replace(".Views.", ".ViewModels.");
        var viewModelAssemblyName = GetTypeAssemblyName(viewType);
        var viewTypeModelName = GenerateTypeName("{0}Model, {1}", viewModelName, viewModelAssemblyName);
        return Type.GetType(viewTypeModelName);
    }

    string GetTypeAssemblyName(Type type) => type.GetTypeInfo().Assembly.FullName;
    string GenerateTypeName(string format, string typeName, string assemblyName) =>
        string.Format(CultureInfo.InvariantCulture, format, typeName, assemblyName);
}

3. 对于设置根页面的情况,我们需要一种类似于ViewModelLocator的东西来自动设置BindingContext

public static class ViewModelLocator
{
    public static readonly BindableProperty AutoWireViewModelProperty =
        BindableProperty.CreateAttached("AutoWireViewModel", typeof(bool), typeof(ViewModelLocator), default(bool), propertyChanged: OnAutoWireViewModelChanged);

    public static bool GetAutoWireViewModel(BindableObject bindable) =>
        (bool)bindable.GetValue(AutoWireViewModelProperty);

    public static void SetAutoWireViewModel(BindableObject bindable, bool value) =>
        bindable.SetValue(AutoWireViewModelProperty, value);

    static ITypeMapperService mapper = (Application.Current as App).DependencyResolver.Resolve<ITypeMapperService>();

    static void OnAutoWireViewModelChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var view = bindable as Element;
        var viewType = view.GetType();
        var viewModelType = mapper.MapViewToViewModel(viewType);
        var viewModel =  (Application.Current as App).DependencyResolver.Resolve(viewModelType);
        view.BindingContext = viewModel;
    }
}

// Usage example
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:viewmodels="clr-namespace:MyApp.ViewModel"
    viewmodels:ViewModelLocator.AutoWireViewModel="true"
    x:Class="MyApp.Views.MyPage">
</ContentPage>

4.最后,我们需要一个NavigationService来支持ViewModel First Navigation方法:

public class NavigationService
{
    TypeMapperService mapperService { get; }

    public NavigationService(TypeMapperService mapperService)
    {
        this.mapperService = mapperService;
    }

    protected Page CreatePage(Type viewModelType)
    {
        Type pageType = mapperService.MapViewModelToView(viewModelType);
        if (pageType == null)
        {
            throw new Exception($"Cannot locate page type for {viewModelType}");
        }

        return Activator.CreateInstance(pageType) as Page;
    }

    protected Page GetCurrentPage()
    {
        var mainPage = Application.Current.MainPage;

        if (mainPage is MasterDetailPage)
        {
            return ((MasterDetailPage)mainPage).Detail;
        }

        // TabbedPage : MultiPage<Page>
        // CarouselPage : MultiPage<ContentPage>
        if (mainPage is TabbedPage || mainPage is CarouselPage)
        {
            return ((MultiPage<Page>)mainPage).CurrentPage;
        }

        return mainPage;
    }

    public Task PushAsync(Page page, bool animated = true)
    {
        var navigationPage = Application.Current.MainPage as NavigationPage;
        return navigationPage.PushAsync(page, animated);
    }

    public Task PopAsync(bool animated = true)
    {
        var mainPage = Application.Current.MainPage as NavigationPage;
        return mainPage.Navigation.PopAsync(animated);
    }

    public Task PushModalAsync<TViewModel>(object parameter = null, bool animated = true) where TViewModel : BaseViewModel =>
        InternalPushModalAsync(typeof(TViewModel), animated, parameter);

    public Task PopModalAsync(bool animated = true)
    {
        var mainPage = GetCurrentPage();
        if (mainPage != null)
            return mainPage.Navigation.PopModalAsync(animated);

        throw new Exception("Current page is null.");
    }

    async Task InternalPushModalAsync(Type viewModelType, bool animated, object parameter)
    {
        var page = CreatePage(viewModelType);
        var currentNavigationPage = GetCurrentPage();

        if (currentNavigationPage != null)
        {
            await currentNavigationPage.Navigation.PushModalAsync(page, animated);
        }
        else
        {
            throw new Exception("Current page is null.");
        }

        await (page.BindingContext as BaseViewModel).InitializeAsync(parameter);
    }
}

正如您所看到的,这里有一个BaseViewModel——所有ViewModels的抽象基类,您可以在其中定义像InitializeAsync这样的方法,在导航后立即执行。下面是一个导航示例:

public class WelcomeViewModel : BaseViewModel
{
    public ICommand NewGameCmd { get; }
    public ICommand TopScoreCmd { get; }
    public ICommand AboutCmd { get; }

    public WelcomeViewModel(INavigationService navigation) : base(navigation)
    {
        NewGameCmd = new Command(async () => await Navigation.PushModalAsync<GameViewModel>());
        TopScoreCmd = new Command(async () => await navigation.PushModalAsync<TopScoreViewModel>());
        AboutCmd = new Command(async () => await navigation.PushModalAsync<AboutViewModel>());
    }
}

正如您所了解的,这种方法更加复杂,难以调试,可能会令人困惑。然而,它有许多优点,实际上您不必自己实现它,因为大多数MVVM框架都支持它。此处演示的代码示例可在github上找到。

关于ViewModel First Navigation方法有很多好的文章,还有一本免费的使用Xamarin.Forms的企业应用程序模式电子书,详细介绍了这个和其他许多有趣的主题。

4
In App.Xaml.Cs:

MainPage = new NavigationPage( new YourPage());

当您想从YourPage导航到下一页时,您需要执行以下操作:
await Navigation.PushAsync(new YourSecondPage());

您可以在此处了解更多关于Xamarin Forms导航的信息: https://learn.microsoft.com/zh-cn/xamarin/xamarin-forms/app-fundamentals/navigation/hierarchical

Microsoft文档非常好。

还有一个更新的概念是Shell。它允许以一种新的方式构建应用程序并在某些情况下简化导航。

介绍:https://devblogs.microsoft.com/xamarin/shell-xamarin-forms-4-0-getting-started/

基本Shell视频教程:https://www.youtube.com/watch?v=0y1bUAcOjZY&t=3112s

文档:https://learn.microsoft.com/zh-cn/xamarin/xamarin-forms/app-fundamentals/shell/


4
通过使用PushAsync()方法,您可以将页面推送到导航堆栈中,而使用PopModalAsync()方法,则可以从导航堆栈中弹出页面。在下面的代码示例中,我有一个导航页(根页),从这个页面我推送一个内容页,即登录页面,一旦我完成登录页面后,就会弹回到根页。
导航可以被看作是Page对象的后进先出(LIFO)堆栈。应用程序要移动到另一页,需要将新页面推入此堆栈。要返回到上一页,应用程序将从堆栈中弹出当前页面。Xamarin.Forms中的导航由INavigation接口处理。
Xamarin.Forms有一个NavigationPage类,实现了这个接口,并管理Pages的堆栈。NavigationPage类还会在屏幕顶部添加一个导航栏,显示标题,并具有适用于平台的“返回”按钮,可返回到上一页。以下代码显示如何在应用程序的第一页周围包装一个NavigationPage:
参考上面列出的内容和您应该查看的Xamarin Forms链接,了解更多关于导航部分的信息:

http://developer.xamarin.com/guides/cross-platform/xamarin-forms/introduction-to-xamarin-forms/

~~~

public class MainActivity : AndroidActivity
{
    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);

        Xamarin.Forms.Forms.Init(this, bundle);
        // Set our view from the "main" layout resource
        SetPage(BuildView());
    }

    static Page BuildView()
    {
        var mainNav = new NavigationPage(new RootPage());
        return mainNav;
    }
}


public class RootPage : ContentPage
{
    async void ShowLoginDialog()
    {
        var page = new LoginPage();

        await Navigation.PushModalAsync(page);
    }
}

//为简化代码已删除,仅显示弹出窗口

private async void AuthenticationResult(bool isValid)
{
    await navigation.PopModalAsync();
}

3

呼叫:

((App)App.Current).ChangeScreen(new Map());

在 App.xaml.cs 中创建此方法:
public void ChangeScreen(Page page)
{
     MainPage = page;
}

3
在 Xamarin 中,我们有一个名为 NavigationPage 的页面,它包含一堆 ContentPagesNavigationPage 有像 PushAsync()PopAsync() 这样的方法。PushAsync 在堆栈顶部添加页面,此时该页面将成为当前活动页面。 PopAsync() 方法从堆栈顶部删除页面。
App.Xaml.Cs 中进行设置。
MainPage = new NavigationPage( new YourPage());

YourPage页面开始,您可以使用await Navigation.PushAsync(new newPage());方法将newPage添加到堆栈的顶部。此时newPage将成为当前活动页面。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接