Xamarin Native Projects 中的 Xamarin.Forms

通常,Xamarin.Forms 应用程序包括一个或多个派生自 ContentPage 的页面,这些页面由 .NET Standard 库项目或共享项目中的所有平台共享。 但是,Native Forms 允许将 ContentPage 派生的页面直接添加到本机 Xamarin.iOS、Xamarin.Android 和 UWP 应用程序。 相较于让本机项目使用来自 .NET Standard 库项目或共享项目的 ContentPage 派生页,直接将页面添加到本机项目的优点是页面可以使用本机视图进行扩展。 然后,可以通过 x:Name 在 XAML 中命名本机视图,并从代码隐藏中引用。 有关本机视图的详细信息,请参阅本机视图

在本机项目中使用 Xamarin.FormsContentPage 派生页的过程如下所示:

  1. 将 Xamarin.Forms NuGet 包添加到本机项目。
  2. ContentPage 派生页和任何依赖项添加到本机项目。
  3. 调用 Forms.Init 方法。
  4. 构造 ContentPage 派生页的实例,并使用以下扩展方法之一将其转换为适当的本机类型:适用于 iOS 的 CreateViewController、适用于 Android 的 CreateSupportFragment 或适用于 UWP 的 CreateFrameworkElement
  5. 使用本机导航 API 导航到 ContentPage 派生页的本机类型表示形式。

必须先通过调用 Forms.Init 方法来初始化 Xamarin.Forms,然后本机项目才能构造 ContentPage派生页。 选择何时执行此操作主要取决于应用程序流中最方便的时机,可以在应用程序启动时执行,也可以在快要构造 ContentPage 派生页之前执行。 本文和随附的示例应用程序会在应用程序启动时调用 Forms.Init 方法。

注意

NativeForms 示例应用程序解决方案不包含任何 Xamarin.Forms 项目。 它是由 Xamarin.iOS 项目、Xamarin.Android 项目和 UWP 项目组成。 每个项目都是使用 Native Forms 来使用 ContentPage 派生页的本机项目。 但是,本机项目应该也可以使用 .NET Standard 库项目或共享项目的 ContentPage 派生页。

使用本机窗体时,Xamarin.Forms 功能(例如 DependencyServiceMessagingCenter 和数据绑定引擎)仍然有效。 但是,必须使用本机导航 API 执行页面导航。

iOS

在 iOS 上,AppDelegate 类中的 FinishedLaunching 重写通常是执行应用程序启动相关任务的位置。 应用程序启动后即会调用它,而且它通常被重写以配置主窗口和视图控制器。 以下代码示例演示示例应用程序中的 AppDelegate 类:

[Register("AppDelegate")]
public class AppDelegate : UIApplicationDelegate
{
    public static AppDelegate Instance;
    UIWindow _window;
    AppNavigationController _navigation;

    public static string FolderPath { get; private set; }

    public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
    {
        Forms.Init();

        // Create app-level resource dictionary.
        Xamarin.Forms.Application.Current = new Xamarin.Forms.Application();
        Xamarin.Forms.Application.Current.Resources = new MyDictionary();

        Instance = this;
        _window = new UIWindow(UIScreen.MainScreen.Bounds);

        UINavigationBar.Appearance.SetTitleTextAttributes(new UITextAttributes
        {
            TextColor = UIColor.Black
        });

        FolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));

        NotesPage notesPage = new NotesPage()
        {
            // Set the parent so that the app-level resource dictionary can be located.
            Parent = Xamarin.Forms.Application.Current
        };

        UIViewController notesPageController = notesPage.CreateViewController();
        notesPageController.Title = "Notes";

        _navigation = new AppNavigationController(notesPageController);

        _window.RootViewController = _navigation;
        _window.MakeKeyAndVisible();

        notesPage.Parent = null;
        return true;
    }
    // ...
}

FinishedLaunching 方法执行以下任务:

  • 通过调用 Forms.Init 方法初始化 Xamarin.Forms。
  • 将创建一个新 Xamarin.Forms.Application 对象,其应用程序级资源字典设置为XAML 中定义的 ResourceDictionary
  • AppDelegate类的引用存储在staticInstance字段中。 这是为其他类提供一种机制来调用 AppDelegate 类中定义的方法。
  • 将创建 UIWindow,也就是本机 iOS 应用程序中视图的主容器。
  • FolderPath 属性初始化为将存储笔记数据的设备上的路径。
  • 创建一个 NotesPage 对象,该对象是在 XAML 中定义的 Xamarin.FormsContentPage 派生页,其父对象设置为以前创建的 Xamarin.Forms.Application 对象。
  • NotesPage 对象将使用 CreateViewController 扩展方法转换为 UIViewController
  • 设置 UIViewController 的属性 Title,该属性将显示在 UINavigationBar 上。
  • 创建用于管理分层导航的 AppNavigationController。 这是派生自 UINavigationController 的自定义导航控制器类。 AppNavigationController 对象管理视图控制器的堆栈,加载 AppNavigationController 时,将最初呈现传入构造函数的 UIViewController
  • AppNavigationController 对象设置为 UIWindow 的顶级 UIViewControllerUIWindow 设置为应用程序的关键窗口,并可见。
  • NotesPage 对象的 Parent 属性设置为 null,以防止内存泄漏。

执行 FinishedLaunching 方法后,将显示 Xamarin.FormsNotesPage 类中定义的 UI,如以下屏幕截图所示:

屏幕截图显示了移动设备上的“备注”屏幕。

重要

所有 ContentPage 派生页都可以使用应用程序级别 ResourceDictionary 中定义的资源,前提是页面的 Parent 属性设置为 Application 对象。

例如,通过点击 + ButtonUI 与 UI 交互将导致代码隐藏中 NotesPage 执行以下事件处理程序:

void OnNoteAddedClicked(object sender, EventArgs e)
{
    AppDelegate.Instance.NavigateToNoteEntryPage(new Note());
}

staticAppDelegate.Instance字段允许AppDelegate.NavigateToNoteEntryPage调用方法,如以下代码示例所示:

public void NavigateToNoteEntryPage(Note note)
{
    NoteEntryPage noteEntryPage = new NoteEntryPage
    {
        BindingContext = note,
        // Set the parent so that the app-level resource dictionary can be located.
        Parent = Xamarin.Forms.Application.Current
    };

    var noteEntryViewController = noteEntryPage.CreateViewController();
    noteEntryViewController.Title = "Note Entry";

    _navigation.PushViewController(noteEntryViewController, true);
    noteEntryPage.Parent = null;
}

NavigateToNoteEntryPage 方法使用 CreateViewController 扩展方法将 Xamarin.FormsContentPage 派生页转换为 UIViewController,并设置 UIViewControllerTitle 属性。 然后,PushViewController 方法将 UIViewController 推送到 AppNavigationController。 因此,将显示 Xamarin.FormsNoteEntryPage 类中定义的 UI,如以下屏幕截图所示:

屏幕截图显示了移动设备上的备注条目。

显示 NoteEntryPage 时,返回导航将从 AppNavigationController 弹出 NoteEntryPage 类的 UIViewController,并将用户返回到 NotesPage 类的 UIViewController。 但是,从 iOS 本机导航堆栈弹出 UIViewController 不会自动释放 UIViewController 和附加的 Page 对象。 因此,AppNavigationController 类重写 PopViewController 方法,以在向后导航上释放视图控制器:

public class AppNavigationController : UINavigationController
{
    //...
    public override UIViewController PopViewController(bool animated)
    {
        UIViewController topView = TopViewController;
        if (topView != null)
        {
            // Dispose of ViewController on back navigation.
            topView.Dispose();
        }
        return base.PopViewController(animated);
    }
}

PopViewController 重写在从 iOS 本机导航堆栈弹出的 UIViewController 对象上调用 Dispose 方法。 不执行此操作将导致 UIViewController 和附加的 Page 对象被孤立。

重要

孤立对象无法进行垃圾回收,因此会导致内存泄漏。

Android

在 Android 上,MainActivity 类中的 OnCreate 替代通常是执行应用程序启动相关任务的位置。 以下代码示例演示示例应用程序中的 MainActivity 类:

public class MainActivity : AppCompatActivity
{
    public static string FolderPath { get; private set; }

    public static MainActivity Instance;

    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);

        Forms.Init(this, bundle);

        // Create app-level resource dictionary.
        Xamarin.Forms.Application.Current = new Xamarin.Forms.Application();
        Xamarin.Forms.Application.Current.Resources = new MyDictionary();

        Instance = this;

        SetContentView(Resource.Layout.Main);
        var toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
        SetSupportActionBar(toolbar);
        SupportActionBar.Title = "Notes";

        FolderPath = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData));

        NotesPage notesPage = new NotesPage()
        {
            // Set the parent so that the app-level resource dictionary can be located.
            Parent = Xamarin.Forms.Application.Current
        };
        AndroidX.Fragment.App.Fragment notesPageFragment = notesPage.CreateSupportFragment(this);

        SupportFragmentManager
            .BeginTransaction()
            .Replace(Resource.Id.fragment_frame_layout, mainPage)
            .Commit();
        //...

        notesPage.Parent = null;
    }
    ...
}

OnCreate 方法执行以下任务:

  • 通过调用 Forms.Init 方法初始化 Xamarin.Forms。
  • 将创建一个新 Xamarin.Forms.Application 对象,其应用程序级资源字典设置为XAML 中定义的 ResourceDictionary
  • MainActivity类的引用存储在staticInstance字段中。 这为其他类提供来一种机制来调用在 MainActivity 类中定义的方法。
  • Activity 内容是从布局资源设置的。 在示例应用程序中,布局由包含 ToolbarLinearLayout 和充当片段容器的 FrameLayout 组成。
  • 检索 Toolbar 并将其设置为 Activity 的操作栏,并设置操作栏标题。
  • FolderPath 属性初始化为将存储笔记数据的设备上的路径。
  • 创建一个 NotesPage 对象,该对象是在 XAML 中定义的 Xamarin.FormsContentPage 派生页,其父对象设置为以前创建的 Xamarin.Forms.Application 对象。
  • 使用 CreateSupportFragment 扩展方法将 NotesPage 对象转换为 Fragment
  • SupportFragmentManager 类创建并提交一个事务,该事务将 FrameLayout 实例替换为 NotesPage 类的 Fragment
  • NotesPage 对象的 Parent 属性设置为 null,以防止内存泄漏。

有关片段的详细信息,请参阅片段

执行 OnCreate 方法后,将显示 Xamarin.FormsNotesPage 类中定义的 UI,如以下屏幕截图所示:

屏幕截图显示了移动设备上带有蓝色横幅和彩色备注文本的“备注”屏幕。

重要

所有 ContentPage 派生页都可以使用应用程序级别 ResourceDictionary 中定义的资源,前提是页面的 Parent 属性设置为 Application 对象。

例如,通过点击 + ButtonUI 与 UI 交互将导致代码隐藏中 NotesPage 执行以下事件处理程序:

void OnNoteAddedClicked(object sender, EventArgs e)
{
    MainActivity.Instance.NavigateToNoteEntryPage(new Note());
}

staticMainActivity.Instance字段允许MainActivity.NavigateToNoteEntryPage调用方法,如以下代码示例所示:

public void NavigateToNoteEntryPage(Note note)
{
    NoteEntryPage noteEntryPage = new NoteEntryPage
    {
        BindingContext = note,
        // Set the parent so that the app-level resource dictionary can be located.
        Parent = Xamarin.Forms.Application.Current
    };

    AndroidX.Fragment.App.Fragment noteEntryFragment = noteEntryPage.CreateSupportFragment(this);
    SupportFragmentManager
        .BeginTransaction()
        .AddToBackStack(null)
        .Replace(Resource.Id.fragment_frame_layout, noteEntryFragment)
        .Commit();

    noteEntryPage.Parent = null;
}

NavigateToNoteEntryPage 方法使用 CreateSupportFragment 扩展方法将 Xamarin.FormsContentPage 派生页转换为 Fragment,并将 Fragment 添加到片段后堆栈。 因此,将显示 Xamarin.FormsNoteEntryPage 中定义的 UI,如以下屏幕截图所示:

屏幕截图显示了移动设备上带有蓝色横幅的备注条目。

显示 NoteEntryPage 时,点击后退箭头将从片段后堆栈弹出 NoteEntryPageFragment,并将用户返回到 NotesPage 类的 Fragment

启用后退导航支持

SupportFragmentManager 类具有一个 BackStackChanged 事件,每当片段后堆栈的内容发生更改时触发。 MainActivity 类中的 OnCreate 方法包含此事件的匿名事件处理程序:

SupportFragmentManager.BackStackChanged += (sender, e) =>
{
    bool hasBack = SupportFragmentManager.BackStackEntryCount > 0;
    SupportActionBar.SetHomeButtonEnabled(hasBack);
    SupportActionBar.SetDisplayHomeAsUpEnabled(hasBack);
    SupportActionBar.Title = hasBack ? "Note Entry" : "Notes";
};

此事件处理程序在操作栏上显示一个后退按钮,前提是片段后堆栈上有一个或多个 Fragment 实例。 对点击后退按钮的响应由 OnOptionsItemSelected 替代处理:

public override bool OnOptionsItemSelected(Android.Views.IMenuItem item)
{
    if (item.ItemId == global::Android.Resource.Id.Home && SupportFragmentManager.BackStackEntryCount > 0)
    {
        SupportFragmentManager.PopBackStack();
        return true;
    }
    return base.OnOptionsItemSelected(item);
}

每当选择选项菜单中的项时,将调用 OnOptionsItemSelected 替代。 此实现从片段后退堆栈弹出当前片段,前提是已选择后退按钮,并且片段后退堆栈上有一个或多个 Fragment 实例。

多个活动

当应用程序由多个活动组成时,可以将 ContentPage 派生页嵌入到每个活动中。 在此方案中,只需在嵌入 Xamarin.FormsContentPage 的第一个 ActivityOnCreate 重写中调用 Forms.Init 方法。 但是,这具有以下影响:

  • Xamarin.Forms.Color.Accent 的值将从调用 Forms.Init 方法的 Activity 中获取。
  • Xamarin.Forms.Application.Current 的值将与调用 Forms.Init 方法的 Activity 相关联。

选择文件

嵌入使用需要支持 HTML“选择文件”按钮的 WebViewContentPage 派生页时,Activity 需要重写 OnActivityResult 方法:

protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
    base.OnActivityResult(requestCode, resultCode, data);
    ActivityResultCallbackRegistry.InvokeCallback(requestCode, resultCode, data);
}

UWP

在 UWP 上,本机 App 类通常是执行应用程序启动相关任务的位置。 通常在 Xamarin.Forms UWP 应用程序中,在本机 App 类的 OnLaunched 重写中初始化 Xamarin.Forms,以将 LaunchActivatedEventArgs 参数传递给 Forms.Init 方法。 因此,使用 Xamarin.FormsContentPage 派生页的本机 UWP 应用程序可以轻松地从 App.OnLaunched 方法调用 Forms.Init 方法:

protected override void OnLaunched(LaunchActivatedEventArgs e)
{
    // ...
    Xamarin.Forms.Forms.Init(e);

    // Create app-level resource dictionary.
    Xamarin.Forms.Application.Current = new Xamarin.Forms.Application();
    Xamarin.Forms.Application.Current.Resources = new MyDictionary();

    // ...
}

此外,OnLaunched 方法还可以创建应用程序所需的任何应用程序级资源字典。

默认情况下,本机 App 类将 MainPage 类作为应用程序的第一页启动。 以下代码示例演示示例应用程序中的 MainPage 类:

public sealed partial class MainPage : Page
{
    NotesPage notesPage;
    NoteEntryPage noteEntryPage;

    public static MainPage Instance;
    public static string FolderPath { get; private set; }

    public MainPage()
    {
        this.NavigationCacheMode = NavigationCacheMode.Enabled;
        Instance = this;
        FolderPath = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData));

        notesPage = new Notes.UWP.Views.NotesPage
        {
            // Set the parent so that the app-level resource dictionary can be located.
            Parent = Xamarin.Forms.Application.Current
        };
        this.Content = notesPage.CreateFrameworkElement();
        // ...
        notesPage.Parent = null;    
    }
    // ...
}

MainPage 构造函数执行以下任务:

  • 为页面启用缓存,以便在用户导航回页面时不会构造新的 MainPage
  • MainPage类的引用存储在staticInstance字段中。 这为其他类提供了一种机制,可以调用在 MainPage 类中定义的方法。
  • FolderPath 属性初始化为将存储笔记数据的设备上的路径。
  • 创建一个 NotesPage 对象,该对象是在 XAML 中定义的 Xamarin.FormsContentPage 派生页,其父对象设置为以前创建的 Xamarin.Forms.Application 对象。
  • 使用 CreateFrameworkElement 扩展方法将 NotesPage 对象转换为 FrameworkElement,然后将其设置为 MainPage 类的内容。
  • NotesPage 对象的 Parent 属性设置为 null,以防止内存泄漏。

执行 MainPage 构造函数后,将显示 Xamarin.FormsNotesPage 类中定义的 UI,如以下屏幕截图所示:

屏幕截图显示了包含备注和日期/时间的“备注”页面。

重要

所有 ContentPage 派生页都可以使用应用程序级别 ResourceDictionary 中定义的资源,前提是页面的 Parent 属性设置为 Application 对象。

例如,通过点击 + ButtonUI 与 UI 交互将导致代码隐藏中 NotesPage 执行以下事件处理程序:

void OnNoteAddedClicked(object sender, EventArgs e)
{
    MainPage.Instance.NavigateToNoteEntryPage(new Note());
}

staticMainPage.Instance字段允许MainPage.NavigateToNoteEntryPage调用方法,如以下代码示例所示:

public void NavigateToNoteEntryPage(Note note)
{
    noteEntryPage = new Notes.UWP.Views.NoteEntryPage
    {
        BindingContext = note,
        // Set the parent so that the app-level resource dictionary can be located.
        Parent = Xamarin.Forms.Application.Current
    };
    this.Frame.Navigate(noteEntryPage);
    noteEntryPage.Parent = null;
}

UWP 中的导航通常使用采用 Page 参数的 Frame.Navigate 方法执行。 Xamarin.Forms 定义采用 ContentPage 派生页实例的 Frame.Navigate 扩展方法。 因此,执行 NavigateToNoteEntryPage 方法时,将显示 Xamarin.FormsNoteEntryPage 中定义的 UI,如以下屏幕截图所示:

屏幕截图显示了一个“备注”页面,其中包含一个输入了备注的文本框。

显示 NoteEntryPage 时,点击后退箭头将从应用内后退堆栈弹出 NoteEntryPageFrameworkElement,并将用户返回到 NotesPage 类的 FrameworkElement

启用页面大小调整支持

调整 UWP 应用程序窗口的大小时,还应调整 Xamarin.Forms 内容的大小。 这是通过在 MainPage 构造函数中注册 Loaded 事件的事件处理程序来实现的:

public MainPage()
{
    // ...
    this.Loaded += OnMainPageLoaded;
    // ...
}

当页面已布局并呈现,且准备好交互时,Loaded 事件触发,并在响应中执行 OnMainPageLoaded 方法:

void OnMainPageLoaded(object sender, RoutedEventArgs e)
{
    this.Frame.SizeChanged += (o, args) =>
    {
        if (noteEntryPage != null)
            noteEntryPage.Layout(new Xamarin.Forms.Rectangle(0, 0, args.NewSize.Width, args.NewSize.Height));
        else
            notesPage.Layout(new Xamarin.Forms.Rectangle(0, 0, args.NewSize.Width, args.NewSize.Height));
    };
}

OnMainPageLoaded 方法为 Frame.SizeChanged 事件注册匿名事件处理程序,该事件处理程序会在 ActualHeightActualWidth 属性在 Frame 上更改时引发。 作为响应,通过调用 Layout 方法调整活动页面 Xamarin.Forms 内容的大小。

启用后退导航支持

在 UWP 上,应用程序必须跨不同的设备外形规格为所有硬件和软件后退按钮启用后退导航。 为此,可以注册 BackRequested 事件的事件处理程序,该事件处理程序可在 MainPage 构造函数中执行:

public MainPage()
{
    // ...
    SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested;
}

启动应用程序时,GetForCurrentView 方法检索与当前视图关联的 SystemNavigationManager 对象,然后注册 BackRequested 事件的事件处理程序。 只有当应用程序是前台应用程序时,它才会收到该事件,且会调用 OnBackRequested 事件处理程序作为响应:

void OnBackRequested(object sender, BackRequestedEventArgs e)
{
    Frame rootFrame = Window.Current.Content as Frame;
    if (rootFrame.CanGoBack)
    {
        e.Handled = true;
        rootFrame.GoBack();
        noteEntryPage = null;
    }
}

OnBackRequested 事件处理程序在应用程序的根帧上调用 GoBack 方法,并将 BackRequestedEventArgs.Handled 属性设置为 true 以将事件标记为已处理。 未能将事件标记为已处理可能会导致事件被忽略。

应用程序选择是否在标题栏上显示后退按钮。 通过将 AppViewBackButtonVisibility 属性设置为 App 类中的 AppViewBackButtonVisibility 枚举值之一来实现此目的:

void OnNavigated(object sender, NavigationEventArgs e)
{
    SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
        ((Frame)sender).CanGoBack ? AppViewBackButtonVisibility.Visible : AppViewBackButtonVisibility.Collapsed;
}

响应 Navigated 事件触发时执行的 OnNavigated 事件处理程序更新页面导航时标题栏后退按钮的可见性。 这可确保在应用内后退堆栈不为空的情况下,标题栏后退按钮是可见的;或者在应用内后退堆栈为空的情况下,标题栏中不显示后退按钮。

有关 UWP 上的后退导航支持的详细信息,请参阅 UWP 应用的导航历史记录和向后导航