微软的官方教程.storyboard/.xib-less user interface design in Xamarin.Mac介绍了如何在Xamarin.Mac中不使用Storyboard和XIB。本文记录练习这个教程中遇到的一些问题和思考。

题外话

什么是Xamarin.Mac

在苹果的macOS上编写APP,官方支持两种语言:Swift和Objective-C。微软为了证明自己是跨平台APP的开发工具和服务提供商,收购了Xamarin并将其开源,使得开发者可以用C#来跨平台开发APP。

Xamarin的基础是Mono,一个开源的C#实现。Xamarin打包了不同平台原生的API供C#使用。比如,Xamarin.Mac打包了macOS上APP开发所需要的API,Xamarin.iOS打包了iOS上API开发所需的API,等等。如果使用这些Xamarin库开发的应用,基本上可以看出是原生的应用,只不过开发语言是C#,并且APP中需要打包Mono的Runtime。

C#和JavaScript一样,都是在ECMA标准化了的语言,所以可以有Mono这种开源实现。根据Can Xamarin.Mac work with .NET Core?的说法,Xamarin目前还不能使用微软自家的跨平台开源的.Net Core来替代Mono。

除了对原生的API提供直接的封装之外,Xamarin还提供Xamarin.Forms,一个跨平台的UI抽象层。基于Xamarin.Forms的代码可以不经修改就在不同平台编译允许。适合用来编写通用的,对平台依赖性不强的UI。The Good and The Bad of Xamarin Mobile Development是了解Xamarin的一篇好文章。

正题

关于Interface Builder

微软推出的Visual Studio for Mac 2017支持Xamarin的开发,个人使用免费。微软的文档中提供了一个例子:Hello, Mac,可以帮助你快速熟悉如何用Visual Studio for Mac开发macOS APP。Visual Studio支持调用macOS自身的Xcode中的Interface Builder来设计界面,然后用C#来处理IBOutlet和IBAction的处理逻辑。

在Mac上开发UI,可以使用三种方法,一)使用XIB,二)使用Storyboard,三)直接使用代码。三比较直接,一和二需要解释下。一和二其实类似,都是在Interface Builder里面使用图形化的辅助工具拖拉生成相应的UI描述文件,然后在APP启动时加载该描述文件并自动生成相应的UI。但是有一些问题是方法一能解决,但是二解决不了的,反之亦然。

网上有好多文章,讨论XIB和Storyboard之间的差别,以及如何不使用它们:

避免使用Interface Builder

Interface Builder当然很好,即可视化,又能够方便组织UI元素直接的关系。但是我比较偏向于直接在Xamarin中直接使用代码,一是不想在Visual Studio和Interface Builder间切换,操作不太方便;二是避免Interface Builder和Visual Studio同步过程中可能存在的bug;三是从长期看,虽然写代码前期慢了一点,但是可重用性高,而且可以帮助你加深对API的理解,长期看好处不小。 文章开头介绍的.storyboard/.xib-less user interface design in Xamarin.Mac告诉你如何不用Storyboard或者XIB,并且提供了一个例子:MacXibless (sample). 但是估计写文档的和提供例子的不是一个人,根据文档的描述会出现错误,但是运行例子则OK。下面会解释。

  • 首先按照文档的描述,在Visual Studio中创建一个Cocoa APP项目,取名HelloMac。
  • 根据文档的描述,删掉Main.storyboard。除此之外,还可以删掉ViewController.cs。
  • 根据文档的描述,创建MainWindow.cs和MainWindowController.cs
    • 注意要把namespace从MacXibless替换城HelloMac
  • 根据文档的描述,修改AppDelegate.cs,为AppDelegate添加一个属性private MainWindowController mainWindowController;,并在其DidFinishLaunching方法中添加下面两行用来生成窗口:
    • mainWindowController = new MainWindowController ();
    • mainWindowController.Window.MakeKeyAndOrderFront (this);

到此,编译运行程序,发现HelloMac的APP图标出现了,但是却出错了

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Could not find a storyboard named 'Main' in bundle NSBundle </Users/mine/Projects/HelloMac/HelloMac/bin/Debug/HelloMac.app> (loaded)'
***

可惜那篇文档里面没有告诉你原因。其实原因在Xamarin remove storyboard。你需要在Info.plist中把Main Interface设空。

再编译运行,错误信息消失了,但是窗口依然没有出现。经过一番调查,发现原因是AppDelegate的DidFinishLaunching没有被调用。你可以根据StackOverflow这篇帖子NSApplicationDelegate not working without Storyboard介绍的方法来调用AppDelegate的DidFinishLaunching。但是这种Hack似乎太过了,所以我觉得还是用XIB来调用AppDelegate的DidFinishLaunching

虽然我不想使用XIB或者Storyboard来做界面,但是使用XIB来做菜单是一个不错的选择。在HelloMac中创建一个MainMenu,会自动命名成MainMenu.xib。保存以后,就可以在Info.plist中把Main Interface设为下拉菜单中的MainMenu。

但是还没有完,我们需要在XIB做一下和AppDelegate相关的设置。双击MainMenu.xib,在Interface Builder中打开,在屏幕正中会显示菜单的预览。然后在Interface Builder的菜单栏选择Editor->ShowDocumentOutline,会在预览窗口的左边展开一个DocumentOutline栏。接着选择View->Libraries->Show Library打开UI对象库,在里面找到Object,并双击添加,可以看到Object出现在左边的DocumentOutline栏中。将其选中。使用快捷键ALT + CMD + 3打开Identity Inspector,然后将其的class设置为AppDelegate。可以看到DocumentOutline栏中Object自动变成了App Delegate。最后,有一个至关重要的步骤,在DocumentOutline栏中按住CTRL并点击Placeholder中的File's Owner,拖出一根线到App Delegate。使用快捷键ALT + CMD + 6打开Connection Inspector,可以看到File's OwnerApp Delegate连接在一起了。保存并返回Visual Studio,然后重新编译并运行HelloMac,可以看到窗口出现了。

上述过程可以部分参考: How to create a macOS Project without Storyboards in Xcode 8 以及Creating and maintaining windows in Xcode。 关于File's Owner,可以参考What is the File’s Owner (in Interface builder)?的解释。

其他参考

苹果官方文档