Showing posts with label howto. Show all posts
Showing posts with label howto. Show all posts

Saturday, February 7, 2015

MonoMac #3: Creating a prompt dialog

One of my most important pieces of advice if you're creating a Xamarin codebase is to pay especially good attention to separation of concerns. In a best case, you'll want to reuse everything but the UI when you go from app to app and platform to platform.

I do this by first creating a project with all of my "business logic", and I perform testing via a minimal command line application that'll behave the same regardess of the platform you're running on. This makes it easy for me to develop an app for any platform on any platform, which is great. And by "any", I do mean "any" -- you can run Mono with MonoDevelop on Linux and develop this headless + command line app. That's kind of neat (and also forces you to separate concerns).

Console utility functions

I have a utility class that has some static methods in it that help me to this.

Quick hint: We're going to use Console.WriteLine() in this example, but when you're playing with the command line, it's often more useful to do something like this:

public static void WriteAtLine(int intLine, string strToWrite)
{
   ClearLineSetCursor(intLine);
   Console.Write(strToWrite);
}
public static void ClearLineSetCursor(int intRow)
{
  Console.SetCursorPosition(0, intRow);
  Console.Write(new String(' ', Console.BufferWidth));
  Console.SetCursorPosition(0, intRow);
}
The real "trick" is the SetCursorPosition line, where you set your cursor and clear out a line like you would waaaay back in the day, when you were writing for a dumb terminal. It's surprising how much thought goes into making a good command line client, even if you're the only one using it.

Here's one of my most active ones, as it's what prompts for and grabs data from the user:

public static string PromptAndResponse(string strPrompt, 
    bool isPassword = false)
{
    string strReturn = string.Empty;
    Console.Write(strPrompt);

    if (isPassword)
    {
        ConsoleKeyInfo conkeyInfo;
        do
        {
            conkeyInfo = Console.ReadKey(true);
            strReturn += conkeyInfo.KeyChar;
            Console.Write("X");
        } while (ConsoleKey.Enter != conkeyInfo.Key);
    }
    else
    {
        strReturn = Console.ReadLine();
    }

    return strReturn.TrimEnd();
}

Porting your console app to a Mac GUI

So this is my first port to Mac. With my simpler mobile apps, I just started from scratch, and hooked up my business logic without any real porting. But on one of my larger projects -- an email client -- I figured it was worth giving a true porting of my command line client a shot.

But what do you do to stub out these utility functions while you're mid port? You certainly don't to take in input from the command line in a serious Frankenstein's App interface, do you?

This isn't much better, but after looking through this answer on StackOverflow, I made a javascript prompt style utility function to replace PromptAndRepsonse on the command line.

Now this said, I do completely agree with this comment on that StackOverflow question, which says, "There isn't a predefined method for that because that's bad UI. That should just be a field." This is a simple stand-in while you're really coding your UI, okay?! Promise?!?

Here we go. I'm going to just slap it into MainWindow.cs as a static method to make things easy to run, but you'd want to factor it somewhere else. This is nothing but a port of the code from the above StackOverflow answer:

public static string PromptAndResponse(string msg)
{
    string strReturn = string.Empty;

    NSAlert alert = new NSAlert ();
    alert.MessageText = msg;
    alert.AddButton ("Ok");
    alert.AddButton ("Cancel");

    NSTextField input = new NSTextField (new Rectangle (0, 0, 200, 24));
    input.StringValue = string.Empty;
    alert.AccessoryView = input;

    int intSelectedButton = alert.RunModal ();
    if (intSelectedButton.Equals((int)NSAlertButtonReturn.First))
    {
        strReturn = input.StringValue;
    }

    return strReturn;
}

Only one thing really bugged me there. Check out the description of the NSAlertButtonReturn enum from Apple's docs.

The enum is wacky. It goes to three (1000, 1001, 1002), then you're on your own. Cocoa counting cultural level: One, two, three, many. Why use an enum at all? Anyhow, let's keep up with the sample code and use it.

I guess I also feel there's got to be an cleaner way to get to 1000 from ~First than a cast. When I have it paused, I can use .Value, but that doesn't exist (?) at compile time.

Example usage

To use this code in an example, simply add it somewhere in your MainWindow.cs file, and then insert this AwakeFromNib function to that same file:

public override void AwakeFromNib ()
{
    int intButtonHeight = 25;
    int intButtonWidth = 120;

    int intFullHeight = (int)this.ContentView.Frame.Size.Height;

    NSButton cmdGo =  new NSButton(new Rectangle (
        20, intFullHeight - 50,
        intButtonWidth, intButtonHeight
    ));
    cmdGo.Title = "Send/Receive";
    cmdGo.BezelStyle = MonoMac.AppKit.NSBezelStyle.Rounded;
    cmdGo.Activated += (object sender, EventArgs e) => {
        string strReply = MainWindow.PromptAndResponse(
            "Enter some text, please."
        );
        Console.WriteLine(strReply);
    };
    this.ContentView.AddSubview (cmdGo);
}

This is the most aggravating part of Apple's "(0,0) is at the bottom left corner" coordinate system we mentioned last time, and will mention again in the next step. If I want to place a button at the top left, I have to start calculating against the NSWindow's height. And, extra annoyingly, I don't know how to get the Window's height with the titlebar yet (I'm just grabbing the ContentView's height here). /sigh So I'm pretending it's about 30 until I stumble over a better source for the height.

And run it with Command-Return. Voila. Pretty exciting.

Now to really mess with yourself, resize the window. Make it smaller. Now quit. Now reopen. Huh? We'll start dealing with adding UI widgets more deliberately next time. But for now, we have an easy stand-in for our command line apps while we're porting each function over.

Wednesday, January 21, 2015

MonoMac #1: Changing window size editing the xib in MonoMac (updated)

EDIT: I'm afraid I initially did this one all wrong. Before, I edited the xib directly, which is apparently a risky proposition. So let's try again, the safe way. If you read this before, you'll notice one or two changes. ;^)


I believe we'll start with MonoMac. MonoMac itself is a strange beast. Even Xamarin employees on their official forums have a hard time explaining the difference.

Here's the bottom line: MonoMac lets you make apps of nearly limitless size that you can't sign easily or release on the app store. Apps made by MonoMac are also intended to be run by someone who has installed the Mono runtime separately. Xamarin.Mac will crunch out what parts of Mono your app uses, and package that up with your app's code before creating an executable for the App Store (or however else you'd like to distriute it). You have options for doing something similar yourself, like using BockBuild, the package Banshee uses to be a self-sufficient app bundle, but it's going to be more work.

Xamarin.mac also doesn't [yet] let you pay for a Xamarin license on the month-by-month plan. Which stinks. Makes some sense, since I don't believe MonoMac has the size limitation that Xamarin.iOS or .Android have, and you could concievably build your entire app in MonoMac, pay $25, compile and sign, and slap something on the Mac App Store. /wink wink

Resizing your window


But let's get to the code. It's going to be horribly quick today. You've got Xamarin Studio installed. Open it. Select "New Solution" on the left. Under "Unsupported", select "MonoMac Project". Enter an incredibly awesome name, like MyMonoMacUI. Hit okay.

Pow. Just for fun, go ahead and hit Command-Return. Lookit there! A window! That's cookin' with gas. Go ahead and hit Command-Shift-Return or click the stop button at the upper left of Xamarin Studio.

So let's change that window's size. Usually, what we'd do would be to double-click MainWindow.xib, open XCode, and set up our UI there, just like Xamarin's Hello, Mac tell us to. But in my limited exposure to Xamarin, that's not the best route for quick, pixel-perfect designs.

I am going to build our first app with what's called "xibless" code. We still have a xib -- MainWindow.xib -- but we're going to use the Initialize and AwakeFromNib methods in MainWindow.cs to set up our UI in code instead. This lets us, I think, much more efficiently position our widgets pixel-perfectly. It also means we have less to learn; if you can C# your way around the UI, you'll never have to open XCode at all.

So resizing is actually pretty easy. Open up MainWindow.cs in Xamarin Studio, and look for the Initialize method. Insert this line to change the frame's size and position:

this.SetFrame(new RectangleF (10, 11, 800, 800), true);

If you just pasted that in, you'll note that RectangleF is red. I'm going to assume you know how to use using in C# and simply say we're missing the reference to RectangleF's namespace. Easy enough to add. Right click RectangleF in Xamarin Studio, select Resolve to let Xamarin Studio give us a list of possible references, and then select using System.Drawing from our options.

Go ahead and hit command+return and watch the form open up. Wow.

And there you go. A square window. We're using a RectangleF to position the window on the screen. There are two types of rectangles I've run into so far -- the RectangleF and Rectangle. The only difference is that RectangleF expects float values and the Rectangle wants ints. The first value, a 10, is how far we'll position the window on the screen from the left. The second number, the 11, is how far from the top we'll be. Then we have the window's width and height, both 800 for now.

Man, we're rolling now.

One freebee "power user" tip, not that I claim to be one: To open more than one instance of Xamarin Studio, use this line in your Terminal (assuming you've got Xamarin Studio in the default location):

open -n /Applications/Xamarin\ Studio.app/

I know, not a lot yet, but we're starting. Happy hacking!