Creating a Custom Control/View - Xamarin.Mac

I'm helping build a Xamarin.Mac based app, and I'm having trouble getting a custom control to work.

All of our screens have the same look and feel: header (title, subtitle), content area, footer with buttons.

After making the first screen, I thought I'd refactor, and turn the header and footer into controls, so I could re-use them, and just swap out the handlers for the button clicks, and expose the StringValue properties from the NSLabel's on the title and subtitle on the header.

So, I created a new Cocoa View via the new file menu in Xamarin.Mac. I laid out the two NSLabel's. Dropped a CustomView from the little toolbox area onto my screen. Set the class on it to DialogHeader (the name of my header view).

Fire up the app, and nada. It appears that the nib/xib for the DialogHeader view isn't associated with the class, because the class gets instantiated, and the AwakeFromNib gets called, but the two NSLabel's don't appear to ever get hydrated from the xib.

Is this type of scenario supported? Does it sound like I'm just doing something wrong? Are there samples out there that do this type of thing? (I tried doing a similar thing as the Button Madness demo where I create the controls in code and drop them into their respective placeholders, but the xib doesn't appear to be loaded in that scenario either.

Thanks in advance!

-Jon

Best Answer

Answers

  • rjmrjm CAMember ✭✭✭

    This is something I've been struggling with as well. At a conceptual level, I still don't full 'get' this in Cocoa...

    For an NSView, the canonical answer seems to be that you should instantiate your view using an NSViewController, then add the view controller's view as a subview to your custom view.

    stackoverflow.com/questions/13257501/composing-nsviews-using-interface-builder-only

    stackoverflow.com/questions/15070036/reusable-nsview-from-xib

    However, as in my second question link, I'm in a scenario where I have a very simple grouping of controls that just does not require the overhead of a NSViewController.

    I know that this is not achievable using interface builder alone - you will have to load your custom view manually from the .xib in code. However I'm not sure the best way to integrate this properly into the lifecycle of the parent NSView/Controller.

    This code will load an instance of your view from a .xib:

    NSNib nib = new NSNib("SpecialView", NSBundle.MainBundle);
    
            SpecialView mySpecialView = null;
    
            NSMutableArray topLevelObjects = new NSMutableArray();
            NSDictionary externalNameTable = NSDictionary.FromObjectsAndKeys(
                new object[] { mySpecialView, topLevelObjects},
            new object[] { NSNibOwner, NSNibTopLevelObjects});
    
            var result = nib.InstantiateNib(externalNameTable);
    
            if(result)
            {
                for(int i = 0; i < topLevelObjects.Count; i++)
                {
                    var pointer = topLevelObjects.ValueAt((uint)i);
                    var value = Runtime.GetNSObject(pointer);
    
                    if(value.GetType() == typeof(SpecialView))
                    {
                        mySpecialView = (SpecialView)value;
                    }
                }
            }
    

    You will need to define NSNibOwner and NSNibTopLevelObjects, something like this:

        static NSString _NSNibTopLevelObjects;
        public static NSString NSNibTopLevelObjects {
            get {
                if (_NSNibTopLevelObjects == null) {
                    var AppKit_libraryHandle = Dlfcn.dlopen (Constants.AppKitLibrary, 0);
                    _NSNibTopLevelObjects = Dlfcn.GetStringConstant (AppKit_libraryHandle, "NSNibTopLevelObjects");
                    Dlfcn.dlclose (AppKit_libraryHandle);
                }
                return _NSNibTopLevelObjects;
            }
        }
    

    But what I can't wrap my head around, is to replace a typed 'Custom View' subview that I placed into my nib, with the instance loaded in code!

    If you ever figure it out, please share!

  • BruceMcLeodBruceMcLeod AUMember

    I am struggling with this too, and have resorted to a less desirable method of putting all my controls on the window surface and writing code to show/hide them as required. :-(

    The "hello world" tutorial hints that ... "In the next guide, we’re going to take a look at the Model, View, Controller (MVC) pattern in Mac and how it’s used to create more complex applications." ... so hopefully the next guide can cover exactly how to do this.

  • RaduSimionescuRaduSimionescu USMember
    edited April 2014

    jonfuller, I managed to solve a similar situation with no problems. Make sure you made outlets for your labels inside DialogHeader view. The labels must be encapsulated as outlets in your custom class.
    You should check if code was generated(correctly) in the DialogHeader.designer.cs file, after you set the outlets in IB. Also, you can safely make the label outlets public by adding the "public" keyword to their declarations, in this file.

  • RaduSimionescuRaduSimionescu USMember
    edited April 2014

    It's me again, I think i now know what you mean. Here are the steps to fix your situation (I blame Xamarin Studio for misleading you into it):

    (note that all these are necessary when using a custom NSView without implementing a custom controller - when creating a Cocoa View for your app from Xamarin):

    1 In the DialogHeader.xib when you made the outlets, they were set by default for the "File's Owner" placeholder, and not for the actual Dialog Header view. To set them for the view, you should have chosen the correct option when adding the outlet in the "Object" field. You can see the actual connected outlets in the outlets pane for your custom view and for the File's owner (they are two different objects in IB, even though they use the same class (your custom class).

    2 Set the class for the File's owner NSViewController, and not DialogHeader as set by default by Xamarin. Also, connect the Dialog Header view to the File's Owner "view" outlet.

    I think this is a Xamarin bug. If the File's owner would have been set by Xamarin correclty from the first place, everything would have worked flawlessly - outlets wouldn't have been set for the File's Owner in the first place. So in the future just make sure you first manually correct the File's owner class and view outlet, before adding new outlets

  • WilliamMappWilliamMapp USMember

    Hi There,

    I'm having a Hell of a hard time getting this to work.

    Does anyone have a full example including the namespace declarations demonstrating how custom views can be instantiated and added.

    Xamarin is generating code from the nib files edited in Xcode, but the outlets are all set to NULL each time.

    Also, if someone has an example demonstrating how to receive Menu click events that'll be great too, it appears the Xamarin examples don't have one.

    Thanks,
    Will

  • ChrisHamonsChrisHamons USForum Administrator, Xamarin Team Xamurai

    Added a full custom NSView tutorial to the documentation wishlist here: https://forums.xamarin.com/discussion/28582/new-xamarin-mac-documentation-live?new=1

    Here's my rough step by step:

    • New Project
    • Right click project, add, new file, Xamarin.Mac, Cocoa View.
    • Named my custom view
    • Double click MainWindow.xib
    • In search in bottom right, type in custom view. Drop it in your window
    • Click your custom view, click the 3rd tab icon (the postage stamp on paper) on the right inspector
    • Type in MyCustomView, Or whatever you have in the [Register] attribute in your designer.cs file.
    • Make sure that in the tree view to the left of the designer that your custom view is a child of View.
    • Save Xcode and quit.
    • Open MyCustomView.cs
    • Add this code:

      public override void AwakeFromNib ()
      {
          WantsLayer = true;
          Layer.BackgroundColor = new MonoMac.CoreGraphics.CGColor (1, 0, 0);
      }
      
    • Run. You should see your custom view as a Red block. If you don't, add a breakpoint to AwakeFromNib to make sure it is called. Double check your nib to make sure the view is where you think it should be.

    • Or just run this: https://www.dropbox.com/s/jdwk2f85gqvqlv2/CustomNSViewExample.zip?dl=0
  • ChrisHamonsChrisHamons USForum Administrator, Xamarin Team Xamurai

    Also, this documentation might be useful:

    http://developer.xamarin.com/guides/mac/advanced_topics/internals/

  • SumalathaSumalatha USMember
    edited April 2015

    I am trying to load a another view on my custom view similar to the code below,

    this.CustomView.AddSubview(new YourCustomViewController().View);
    

    When I had the YourCustomViewController.xib and .cs files in the same project, it works fine. When I tried moving them to a xamarin/monomac library project and reference it from my project, it failedin runtime with invalid cast exception in the code below ,

    public new YourCustomView View {
                get {
                    return (YourCustomView)base.View;
                }
            }
    

    is it not right to reference it this way? Can someone suggest what is wrong here?

  • GraGra33GraGra33 USMember

    When I tried moving them to a xamarin/monomac library project and reference it from my project, it failedin runtime with invalid cast exception in the code below

    We're seeing the same. We even tried passing in the SuperView Custom View container as a ref. We can see the Superview but when we try to .AddSubview(new ... ); we experience the same error.

    How do we load this psuedo custom layers/controls into the main app from an external class library?

    PS: emails to [email protected] for business level support are bouncing...

  • ChrisHamonsChrisHamons USForum Administrator, Xamarin Team Xamurai

    @GraGra33 We're looking into the e-mails bounding now. If you run into issues like this, feel free to contact [email protected]

  • ChrisHamonsChrisHamons USForum Administrator, Xamarin Team Xamurai

    Can one of you post an example with the full exception log?

  • DominicNDominicN USForum Administrator, Xamarin Team, University Xamurai

    @GraGra33 Chris told me about the issue you're having with emails. Please send me a private message and we'll try to rectify the email issue. :)

  • GraGra33GraGra33 USMember

    @ChrisHamons

    Can one of you post an example with the full exception log?

    I have a sample project that I was trying to send...

    @DominicNahous

    Chris told me about the issue you're having with emails. Please send me a private message and we'll try to rectify the email issue. :)

    Sending now...

  • ChrisHamonsChrisHamons USForum Administrator, Xamarin Team Xamurai

    Hey guys,

    At least for @GraGra33 's example he sent me, the problem appears to be that the sub-assemblies aren't being registered with our runtime. This means we don't have these views registered / exports scanned and thus get confused and die in that cast.

    There are two "solutions" for now:

    • Reference the type explicitly somewhere before NSApplication.Main

      Console.WriteLine (typeof(SubView1.MySubview1).Assembly);

    • Explicitly register the assembly later, say at static type constructor time:

      ObjCRuntime.Runtime.RegisterAssembly (typeof(Factory).Assembly);

    We hope to make this more awesome in the future, but that's where we are now.

  • GraGra33GraGra33 USMember

    There are two "solutions" for now:

    Big thanks to Chris for the solution - works well! ;)

  • SumalathaSumalatha USMember

    Thanks @ChrisHamons .
    this solution worked for me -
    Console.WriteLine (typeof(SubView1.MySubview1).Assembly);

  • AllanChin.6924AllanChin.6924 USUniversity ✭✭✭

    Ok, I'm not sure if anything here is addressing my problem. What I wanted to do was dynamically add a ViewController/View which was created in the IB as a SubView of a CustomView. I added a Red layer in the View's AwakeFromNib() and it's showing up red just fine. But the controls (Button and Label) I have added to the View via the IB do not show up. Am I missing something here? Attached is my simple project.

    Thanks

  • ChrisHamonsChrisHamons USForum Administrator, Xamarin Team Xamurai

    @AllanChin.6924 In the future, please consider creating a new post for an unrelated question.

    It appears there is some confusion on your end in the sample, what it currently is doing is:

    • In Main storyboard, the starting view controller just has an "empty" custom view, which has no items.
    • You are instancing up a new TestViewController (and not keeping a reference, which isn't great practice) in ViewDidAppear and dropping your view right on top of the empty view.
    • You can tell this is happening by adding a button to the "empty" view and commenting in/ out the AddSubview.
    • TestViewController is doing this:
      public TestViewController() : base("TestView", NSBundle.MainBundle)

    • This is loading the TestView.xib, which is an empty view, which you then set red.

    • This is doing nothing with the other TestView bits you have in the main storyboard.

    Now, what I think you are trying to do can be done by:

    • Deleting TestView.xib from your project
    • Deleting this code

      public TestViewController() : base("TestView", NSBundle.MainBundle)
      {
          Initialize();
      }
      
    • Opening Main.storyboard and setting the storyboard ID to something like TestViewID on the controller you defined there

    • Replacing ViewController.cs's ViewDidAppear with something like

      public override void ViewDidAppear()
      {
          base.ViewDidAppear();
      
          var controller = (TestViewController)NSStoryboard.FromName ("Main", NSBundle.MainBundle).InstantiateControllerWithIdentifier ("TestViewID");
          var subview = controller.View;
      
          // Define frame and display
          subview.Frame = new CGRect(0, 0, ViewContainer.Frame.Width, ViewContainer.Frame.Height);
          ViewContainer.AddSubview(subview);
      }
      

    You get something like:

    https://www.dropbox.com/s/e8w7mrbopqn1je5/RedViewController.png

  • AllanChin.6924AllanChin.6924 USUniversity ✭✭✭

    Brilliant again, Chris. Thanks.

    Sorry, I thought the context of the original post was related but as I said, I wasn't quite sure. But regarding my original solution, why wouldn't setting the class of the TestViewController's View to TestView (which does own those SB bits) render with the TestViewController? Or am I way off base here?

  • ChrisHamonsChrisHamons USForum Administrator, Xamarin Team Xamurai
    edited September 2016

    So, I think your a bit confused (i'm having trouble parsing your follow up question). Let me describe things a tad, and see if that answers your question.

    • All a storyboard / xib is under the hood is a cute format to serialize an object graph. You drag and drop buttons, controllers, and stuff, and can expand them back into "real" windows and stuff at runtime.
    • By default, your Info.plist will list a NSMainStoryboardFile which automagically gets loaded up and the main window controller (Set to is initial controller) gets spun up, and you get your AwakeFromNib / ViewDidAppear called.
    • Now you want to load up the bits you saved in main.storyboard, not TestView.xib.
    • By default, the ctor for the view controller does:
     public TestViewController() : base("TestView", NSBundle.MainBundle)
    
    • This says roughly, "when you create one, go find TestView.xib in my bundle, load it up, and when you read the View deserialize there.
    • However, the object graph you want isn't in TestView.xib, it's in Main.storybard. So we do:
     var sb = NSStoryboard.FromName ("Main", NSBundle.MainBundle);
     var controller = (TestViewController)sb.InstantiateControllerWithIdentifier ("TestViewID");
    
    • This gets ahold of the storyboard, asks it to locate the right subset of the graph, deserialize it, and return it in a new TestViewController.
    • (One of the) ViewController's job is to handle loading / deserialization of the thing you request.
Sign In or Register to comment.