我想在 opening/creating NSDocument 时要求输入密码。在哪里放置提示?

I want to require a password when opening/creating a NSDocument. Where to put the prompt?

我真的不熟悉 macOS 开发,正在努力找出正确的方法来做这件事。场景:我的应用程序使用加密文档。这些是跨平台的,所以我无法更改加密机制(例如,直接使用 OS 提供的内容)。我也想稍后创建一个 iOS 应用程序,并尽可能多地共享代码。

流程是这样的:

  1. "Open" 或 "New" 一个新文档
  2. 提示用户输入密码
  3. (如果打开Document,请验证密码是否正确,否则重复步骤2直到正确或取消)
  4. 显示文档Window

所以我有这些 classes:

这都包含在一个main.storyboard中(考虑拆分,但首先要弄清楚正确的架构):

我在 MyEncryptedDocument 中实现了 read(from data: Data, ofType typeName: String),只是将内容作为字节数组读取。现在,这里是我要显示密码提示的地方,但 NSDocument class 似乎不是正确的地方 - 对于初学者来说,我没有 WindowController,并且 windowControllers 是空的(我假设之后调用了 makeWindowControllers)。

我一直在考虑 subclassing NSWindowController 或 NSWindow,但后来我想知道密码提示的正确位置在哪里? WindowController 中的 awakeFromNib 还没有文档,尽管我可以通过 makeWindowControllers.

分配它

这给我留下了这些问题:

我可以接受 Swift 或 Objective-C,因为我更关心 "Where" 而不是确切的 "How"。

下面是我现在的实现方式:

  • 创建 NSDocumentController
  • 的子class
  • 在 AppDelegate 中,实例化 class - 这足以将其设置为应用程序的 DocumentController(只能有一个)
  • 在子class中,为makeUntitledDocumentOfType:error:makeDocumentWithContentsOfURL:ofType:error:
  • 设置处理程序
  • 在那里,我现在可以创建对话框来询问密码并创建(解密的)文档,或者 return 一个错误。
  • MyEncryptedDocument(NSDocument 的子class)需要在其 init/constructor 中输入密码。这用于覆盖 readFromData:ofType:error:dataOfType:error: 到 load/decrypt 和 save/encrypt 数据

在我看来,DocumentController 确实是应该处理这个问题的地方,因为 password/encryption 比实际文档或任何 UI 更多的是管道问题].总的来说,作为一个没有经验的 macOS 开发者,这 "feels" 适合我。我不确定 NSAlert 是否是对话框的正确 class;看着 Apple's Guidelines 我想我应该创建自己的 NSPanel 或 NSWindow。但这是以后的问题。

在 Xamarin C# 代码中,class 如下所示:

public class MyEncryptedDocumentController : NSDocumentController
{
    public MyEncryptedDocumentController()
    {
    }

    // makeUntitledDocumentOfType:error:
    public override NSObject MakeUntitledDocument(string typeName, out NSError error)
    {
        return LoadOrCreateDocument(typeName, null, out error);
    }

    // makeDocumentWithContentsOfURL:ofType:error:
    public override NSObject MakeDocument(NSUrl url, string typeName, out NSError outError)
    {
        return LoadOrCreateDocument(typeName, url, out outError);
    }

    private MyEncryptedDocument LoadOrCreateDocument(string typeName, NSUrl url, out NSError error)
    {
        error = null;
        using (var sb = NSStoryboard.FromName("PasswordView", null))
        using (var ctrl = sb.InstantiateControllerWithIdentifier("Password View Controller") as PasswordViewController)
        using (var win = new NSAlert())
        {
            win.MessageText = "Please enter the Password:";
            //win.InformativeText = "Error message goes here.";
            win.AlertStyle = NSAlertStyle.Informational;
            win.AccessoryView = ctrl.View;

            var btnOK = win.AddButton("OK");
            var btnCancel = win.AddButton("Cancel");

            var res = win.RunModal();
            var pw = ctrl.Password;

            if (res == (int)NSAlertButtonReturn.First)
            {
                var doc = new MyEncryptedDocument(pw);
                if (url != null)
                {
                    if (!doc.ReadFromUrl(url, typeName, out error))
                    {
                        // Could check if error is a custom "Wrong Password"
                        // and then re-open the Alert, setting the Informational Text
                        // to something like "wrong password"
                        return null;
                    }
                }

                return doc;
            }

            // MyEncryptedDocument.Domain is a NSString("com.mycompany.myapplication");
            // MyErrorCodes is just a custom c# enum
            error = new NSError(MyEncryptedDocument.Domain, (int)MyErrorCodes.PasswordDialogCancel);
            return null;
        }
    }
}

PasswordViewController 是 NSViewController 的一个非常简单的子class:

public partial class PasswordViewController : NSViewController
{
    public string Password { get => tbPassphrase?.StringValue ?? ""; }

    public PasswordViewController(IntPtr handle) : base(handle)
    {
    }
}

tbPassphrase 是视图中文本框的出口(.h 文件中的 @synthesize tbPassphrase = _tbPassphrase;)。故事板是一个简单的场景 viewController:

<viewController storyboardIdentifier="Password View Controller" id="5LL-3u-LyJ" customClass="PasswordViewController" sceneMemberID="viewController">
    <view key="view" id="yoi-7p-9v6">
        <rect key="frame" x="0.0" y="0.0" width="315" height="22"/>
        <autoresizingMask key="autoresizingMask"/>
        <subviews>
            <secureTextField identifier="tfPassphrase" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YmM-nK-9Hb">
                <rect key="frame" x="0.0" y="0.0" width="315" height="22"/>
                <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
                <secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="ChX-i5-luo">
                    <font key="font" metaFont="system"/>
                    <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
                    <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
                    <allowedInputSourceLocales>
                        <string>NSAllRomanInputSourcesLocaleIdentifier</string>
                    </allowedInputSourceLocales>
                </secureTextFieldCell>
            </secureTextField>
        </subviews>
    </view>
    <connections>
        <outlet property="tbPassphrase" destination="YmM-nK-9Hb" id="sCC-Ve-8FO"/>
    </connections>
</viewController>