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
派生页的过程如下所示:
- 将 Xamarin.Forms NuGet 包添加到本机项目。
- 将
ContentPage
派生页和任何依赖项添加到本机项目。 - 调用
Forms.Init
方法。 - 构造
ContentPage
派生页的实例,并使用以下扩展方法之一将其转换为适当的本机类型:适用于 iOS 的CreateViewController
、适用于 Android 的CreateSupportFragment
或适用于 UWP 的CreateFrameworkElement
。 - 使用本机导航 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 功能(例如 DependencyService
、MessagingCenter
和数据绑定引擎)仍然有效。 但是,必须使用本机导航 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
类的引用存储在static
Instance
字段中。 这是为其他类提供一种机制来调用AppDelegate
类中定义的方法。 - 将创建
UIWindow
,也就是本机 iOS 应用程序中视图的主容器。 FolderPath
属性初始化为将存储笔记数据的设备上的路径。- 创建一个
NotesPage
对象,该对象是在 XAML 中定义的 Xamarin.FormsContentPage
派生页,其父对象设置为以前创建的Xamarin.Forms.Application
对象。 NotesPage
对象将使用CreateViewController
扩展方法转换为UIViewController
。- 设置
UIViewController
的属性Title
,该属性将显示在UINavigationBar
上。 - 创建用于管理分层导航的
AppNavigationController
。 这是派生自UINavigationController
的自定义导航控制器类。AppNavigationController
对象管理视图控制器的堆栈,加载AppNavigationController
时,将最初呈现传入构造函数的UIViewController
。 AppNavigationController
对象设置为UIWindow
的顶级UIViewController
,UIWindow
设置为应用程序的关键窗口,并可见。NotesPage
对象的Parent
属性设置为null
,以防止内存泄漏。
执行 FinishedLaunching
方法后,将显示 Xamarin.FormsNotesPage
类中定义的 UI,如以下屏幕截图所示:
重要
所有 ContentPage
派生页都可以使用应用程序级别 ResourceDictionary
中定义的资源,前提是页面的 Parent
属性设置为 Application
对象。
例如,通过点击 + Button
UI 与 UI 交互将导致代码隐藏中 NotesPage
执行以下事件处理程序:
void OnNoteAddedClicked(object sender, EventArgs e)
{
AppDelegate.Instance.NavigateToNoteEntryPage(new Note());
}
该static
AppDelegate.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
,并设置 UIViewController
的 Title
属性。 然后,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
类的引用存储在static
Instance
字段中。 这为其他类提供来一种机制来调用在MainActivity
类中定义的方法。 Activity
内容是从布局资源设置的。 在示例应用程序中,布局由包含Toolbar
的LinearLayout
和充当片段容器的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
对象。
例如,通过点击 + Button
UI 与 UI 交互将导致代码隐藏中 NotesPage
执行以下事件处理程序:
void OnNoteAddedClicked(object sender, EventArgs e)
{
MainActivity.Instance.NavigateToNoteEntryPage(new Note());
}
该static
MainActivity.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
时,点击后退箭头将从片段后堆栈弹出 NoteEntryPage
的 Fragment
,并将用户返回到 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
的第一个 Activity
的 OnCreate
重写中调用 Forms.Init
方法。 但是,这具有以下影响:
Xamarin.Forms.Color.Accent
的值将从调用Forms.Init
方法的Activity
中获取。Xamarin.Forms.Application.Current
的值将与调用Forms.Init
方法的Activity
相关联。
选择文件
嵌入使用需要支持 HTML“选择文件”按钮的 WebView
的 ContentPage
派生页时,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
类的引用存储在static
Instance
字段中。 这为其他类提供了一种机制,可以调用在MainPage
类中定义的方法。 FolderPath
属性初始化为将存储笔记数据的设备上的路径。- 创建一个
NotesPage
对象,该对象是在 XAML 中定义的 Xamarin.FormsContentPage
派生页,其父对象设置为以前创建的Xamarin.Forms.Application
对象。 - 使用
CreateFrameworkElement
扩展方法将NotesPage
对象转换为FrameworkElement
,然后将其设置为MainPage
类的内容。 NotesPage
对象的Parent
属性设置为null
,以防止内存泄漏。
执行 MainPage
构造函数后,将显示 Xamarin.FormsNotesPage
类中定义的 UI,如以下屏幕截图所示:
重要
所有 ContentPage
派生页都可以使用应用程序级别 ResourceDictionary
中定义的资源,前提是页面的 Parent
属性设置为 Application
对象。
例如,通过点击 + Button
UI 与 UI 交互将导致代码隐藏中 NotesPage
执行以下事件处理程序:
void OnNoteAddedClicked(object sender, EventArgs e)
{
MainPage.Instance.NavigateToNoteEntryPage(new Note());
}
该static
MainPage.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
时,点击后退箭头将从应用内后退堆栈弹出 NoteEntryPage
的 FrameworkElement
,并将用户返回到 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
事件注册匿名事件处理程序,该事件处理程序会在 ActualHeight
或 ActualWidth
属性在 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 应用的导航历史记录和向后导航。