With Xamarin.Forms or Without it
Xamarin.Forms is a great tool, it allows you to share UI *and* navigation code across all platforms. I emphasized the navigation part, because when speaking about Xamarin.Forms it seems like many people forget about this: the navigation system is different in each platform!
After reading this: https://xamarin.com/forms#choose-an-approach it feels to me that there are two paths: with Xamarin.Form or without it. They say: “If you follow the guidelines and you don’t care too much about the UI customization go with Forms, if not, go fully native”. That makes me wonder, why should I have these two edge cases? What happens if the requirements change and I have to do a lot of custom stuff? Do I need to create every PageRenderer by hand? If the app is large enough, it will be messy.
Imagine a world where you can have a Xamarin.Form page only when you need it, like in the MonoTouch.Dialog days. Besides, wouldn’t it be great to have full control of the navigation? Everybody seems to agree that the PageRenderer approach is a bit messy. You have to know a lot of the internals to get things working as you want them.
Xamarin.Forms App
Let’s start with a simple app, we will have only three pages. We will be able to navigate from Page1 to Page2 and from Page2 to Page3.
The Navigation Service will be simple, it will only know how to OpenAPage, so given a number it will know which page should be opened (usually it would have a navigation stack, it will be able to pop pages, etc.)
publicinterfaceINavigationService {
void OpenPage(int number);
}
publicclassFormsNavigationService : INavigationService {
NavigationPage navigationPage;
publicFormsNavigationService () {
this.navigationPage = newNavigationPage (CreatePage(1));
}
publicasyncvoid OpenPage (int number){
await navigationPage.PushAsync (CreatePage(number));
}
publicPage CreatePage (int number){
var pageType = Type.GetType ("OUR_NAMESPACE.Page" + number.ToString());
var page = (Page)Activator.CreateInstance (pageType, this);
return page;
}
}
The navigation service will be passed for every page. I prefer to have a ViewModel for handling the navigation, so it’s responsibility of the ViewModel to know to which ViewModel it should navigate, but for keeping the post as short as possible, we will delegate this responsibility to the View.
publicclassContentPageBase : ContentPage {
readonlyINavigationService navService;
publicContentPageBase (INavigationService navService){
this.navService = navService;
}
}
publicclassPage1 : ContentPageBase {
publicPage1 (INavigationService navService) : base(navService) {
BackgroundColor = Color.Red;
Content = newButton {
Text = "Go to 2",
Command = newCommand(()=> navService.OpenPage(2))
} ;
}
}
publicclassPage2 : ContentPageBase {
publicPage2 (INavigationService navService) : base(navService) {
BackgroundColor = Color.Olive;
Content = newButton {
Text = "Go to 3",
Command = newCommand(()=> navService.OpenPage(3))
} ;
}
}
publicclassPage3 : ContentPageBase {
publicPage3 (INavigationService navService) : base(navService) {
BackgroundColor = Color.Blue;
}
}
Finally, we initialised our NavigationService and we also set the MainPage in the Application class.
protectedoverridevoid OnStart () {
var navService = newFormsNavigationService ();
MainPage = navService.CreatePage (1);
}
The other way around: iOS
So, let’s say that we want the second page to be fully native. But we still need to navigate from Page1 to Page2 and from Page2 to Page3. How can we achieve it?
The strategy consists of using our Pages as if they were UIViewController.
Let’s start by creating the native view:
publicclassNativeUIViewController : UIViewController {
INavigationService navService;
publicNativeUIViewController (INavigationService navService) {
this.navService = navService;
}
publicoverridevoid ViewDidLoad () {
base.ViewDidLoad ();
this.View.BackgroundColor = UIColor.White;
var button = newUIButton (new CoreGraphics.CGRect(10,100, 200, 100));
button.TouchUpInside += (sender, e) => navService.OpenPage(3);
button.BackgroundColor = UIColor.Blue;
button.SetTitle ("To page 3", UIControlState.Normal);
Add (button);
}
}
Now we can change our NavigationService implementation.
publicclassTouchNavigationService : INavigationService {
UINavigationController navController;
publicTouchNavigationService (UINavigationController navController){
this.navController = navController;
}
publicUIViewController GetViewControllerByNumber (int number) {
if (number == 2)
returnnewNativeUIViewController(this);
var page = number == 1 ?(Page) newPage1 (this) : (Page)newPage3 (this);
return page.CreateViewController ();
}
publicvoid OpenPage (int number) {
navController.PushViewController (GetViewControllerByNumber(number), true);
}
}
As we can see, getting a native view of a Page is pretty straightforward in iOS. We just need to call the extension method page.CreateViewController.
And finally, we have to modify the AppDelegate a little bit:
[Register ("AppDelegate")]
publicpartialclassAppDelegate : UIApplicationDelegate
{
TouchNavigationService navService;
publicoverridebool FinishedLaunching (UIApplication app, NSDictionary options)
{
Xamarin.Forms.Forms.Init ();
var navController = newUINavigationController ();
navService = newTouchNavigationService (navController);
navService.OpenPage (1);
Window = newUIWindow (UIScreen.MainScreen.Bounds);
Window.RootViewController = navController;
Window.MakeKeyAndVisible ();
returntrue;
}
publicoverrideUIWindow Window { get; set; }
}
A bit hacked but not that bad: Android
Android is not that easy, we can’t just create an Activity from a Page because it uses the *same* Activity for rendering every Page! I guess that Xamarin had a good reason for doing such a terrible thing, but I don’t know which one it is.
Anyway, if we want to be able to open Activities and then be able to open Pages back, we have to use the same approach that Xamarin uses, which means to render the Page in the Xamarin’s Activity. This implies to load the form application every time (Xamarin please, could you do something here?)
[Activity (Label = "XamarinFormAsALibrary.Droid", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
publicclassMainActivity : global::Xamarin.Forms.Platform.Android.FormsApplicationActivity
{
protectedoverridevoid OnCreate (Bundle bundle) {
base.OnCreate (bundle);
var navigationService = DroidNavigationService.GetNavigationService();
if (!Forms.IsInitialized)
Forms.Init (this, bundle);
long pageNumber = 1;
if (Intent.Extras != null)
pageNumber = Intent.Extras.GetLong("number");
var pageToLoad = pageNumber == 1 ? newPage1 (navigationService) asPage
: newPage3 (navigationService);
LoadApplication (newApp { MainPage = pageToLoad });
}
}
We subclass the XamarinAppActivity as usual. But we will open a different page depending on the arguments (I’m using the number here, but you can create a page by its type).
Finally, we will create our NavigationService for Android (as a Singleton for keeping things short) which should look as follows:
publicclassDroidNavigationService : INavigationService {
staticINavigationService instance;
publicstaticINavigationService GetNavigationService(){
instance = instance ?? newDroidNavigationService ();
return instance;
}
publicvoid OpenPage (int number){
var context = Xamarin.Forms.Forms.Context;
if (number == 2) {
context.StartActivity (typeof(NativeActivity));
return;
}
var i = newIntent (context, typeof(MainActivity));
i.PutExtra ("number", number);
context.StartActivity(i);
}
}
And of course, our Native Activity:
[Activity (MainLauncher = false)]
publicclassNativeActivity : Activity {
INavigationService navService;
Android.Widget.Button button;
protectedoverridevoid OnCreate (Bundle savedInstanceState){
base.OnCreate (savedInstanceState);
SetContentView (Resource.Layout.NativePage);
button = FindViewById<Android.Widget.Button> (Resource.Id.nativePageButton);
button.Click += (sender, e) => {
navService.OpenPage(3);
} ;
navService = DroidNavigationService.GetNavigationService ();
}
}
And that's it! The main activity is the Xamarin one because we are going to open the Page1 first, but you can use any other!
If you like this approach, spread the word and, who knows, maybe Xamarin will listen to us
:-)