Collection Views

Using Collection Views in a Xamarin.Mac application

PDF for offline use:
Sample Code:
Related Articles:
Related SDKs:

Let us know how you feel about this.


0/250
Thanks for the feedback!

This article covers working with Collection Views in a Xamarin.Mac application. It covers creating and maintaining Collection Views in Xcode and Interface builder, how to expose the Collection View elements to code using Outlets and Actions, populating Collection Views and finally responding to Collection Views in C# code.

Contents

This article will cover the following topics in detail:

Overview

When working with C# and .NET in a Xamarin.Mac application, you have access to the same AppKit Collection View controls that a developer working in in Objective-C and Xcode does. Because Xamarin.Mac integrates directly with Xcode, you can use Xcode's Interface Builder to create and maintain Collection Views.

A NSCollectionView displays a grid of subviews in an organized fashion. Each subview in the grid is represented by a NSCollectionViewItem which manages the loading of the view’s content from your storyboard or .xib file.

NOTE: Due to an issue in Xcode 7 (or greater) and macOS 10.11 (and greater), Collection Views are unable to be used inside of a Storyboard (.storyboard) files. As a result, you will need to continue to use .xib files to define your Collection Views for your Xamarin.Mac apps.

In this article, we'll cover the basics of working with Collection Views in a Xamarin.Mac application. It is highly suggested that you work through the Hello, Mac article first, specifically the Introduction to Xcode and Interface Builder and Outlets and Actions sections, as it covers key concepts and techniques that we'll be using in this article.

You may want to take a look at the Exposing C# classes / methods to Objective-C section of the Xamarin.Mac Internals document as well, it explains the Register and Export commands used to wire-up your C# classes to Objective-C objects and UI Elements.

About Collection Views

The main goal of a Collection View (NSCollectionView) is to visually arrange a group of objects in an organized fashion, with each individual object (NSCollectionViewItem) getting its own View in the larger collection. Collection Views work via Data Binding and Key-Value Coding techniques and as such, you should read our Data Binding and Key-Value Coding documentation before continuing with this article.

The Collection View has no standard, built-in Collection View Item (like an Outline or Table View does), so the developer is responsible for designing and implementing a Prototype View using other AppKit controls such as Image Fields, Text Fields, Labels, etc. This Prototype View will be used to display and work with each item being managed by the Collection View.

Because the developer is responsible for the look and feel of a Collection View Item, the Collection View has no built-in support for highlighting a selected item in the grid and there is also no built-in notification for the user changing the selection. Implementing both of these features will be covered in this article.

Defining your Data Model

Before you can Data Bind a Collection View in Interface Builder, you must have a Key-Value Coding (KVC)/Key-Value Observing (KVO) compliant class defined in your Xamarin.Mac application to act as the Data Model for the binding. The Data Model provides all of the data that will be displayed in the collection and receives any modifications to the data that the user makes in the UI while running the application.

For example, if you were writing an application that managed a group of employees, you could use the following class to define the Data Model:

using System;
using Foundation;
using AppKit;

namespace MacDatabinding
{
    [Register("PersonModel")]
    public class PersonModel : NSObject
    {
        #region Private Variables
        private string _name = "";
        private string _occupation = "";
        private bool _isManager = false;
        private NSMutableArray _people = new NSMutableArray();
        #endregion

        #region Computed Properties
        [Export("Name")]
        public string Name {
            get { return _name; }
            set {
                WillChangeValue ("Name");
                _name = value;
                DidChangeValue ("Name");
            }
        }

        [Export("Occupation")]
        public string Occupation {
            get { return _occupation; }
            set {
                WillChangeValue ("Occupation");
                _occupation = value;
                DidChangeValue ("Occupation");
            }
        }

        [Export("isManager")]
        public bool isManager {
            get { return _isManager; }
            set {
                WillChangeValue ("isManager");
                WillChangeValue ("Icon");
                _isManager = value;
                DidChangeValue ("isManager");
                DidChangeValue ("Icon");
            }
        }

        [Export("isEmployee")]
        public bool isEmployee {
            get { return (NumberOfEmployees == 0); }
        }

        [Export("Icon")]
        public NSImage Icon {
            get {
                if (isManager) {
                    return NSImage.ImageNamed ("group.png");
                } else {
                    return NSImage.ImageNamed ("user.png");
                }
            }
        }

        [Export("personModelArray")]
        public NSArray People {
            get { return _people; }
        }

        [Export("NumberOfEmployees")]
        public nint NumberOfEmployees {
            get { return (nint)_people.Count; }
        }
        #endregion

        #region Constructors
        public PersonModel ()
        {
        }

        public PersonModel (string name, string occupation)
        {
            // Initialize
            this.Name = name;
            this.Occupation = occupation;
        }

        public PersonModel (string name, string occupation, bool manager)
        {
            // Initialize
            this.Name = name;
            this.Occupation = occupation;
            this.isManager = manager;
        }
        #endregion

        #region Array Controller Methods
        [Export("addObject:")]
        public void AddPerson(PersonModel person) {
            WillChangeValue ("personModelArray");
            isManager = true;
            _people.Add (person);
            DidChangeValue ("personModelArray");
        }

        [Export("insertObject:inPersonModelArrayAtIndex:")]
        public void InsertPerson(PersonModel person, nint index) {
            WillChangeValue ("personModelArray");
            _people.Insert (person, index);
            DidChangeValue ("personModelArray");
        }

        [Export("removeObjectFromPersonModelArrayAtIndex:")]
        public void RemovePerson(nint index) {
            WillChangeValue ("personModelArray");
            _people.RemoveObject (index);
            DidChangeValue ("personModelArray");
        }

        [Export("setPersonModelArray:")]
        public void SetPeople(NSMutableArray array) {
            WillChangeValue ("personModelArray");
            _people = array;
            DidChangeValue ("personModelArray");
        }
        #endregion
    }
}

We'll be using the PersonModel Data Model throughout the rest of this article.

Working with a Collection View

Data Binding with a Collection View is very much like binding with a Table View, as an Array Controller is used to provide data for the collection. Since the collection view doesn't have a preset display format, more work is required to provide user interaction feedback and to track user selection.

Creating and Exposing the Collection Data

First, let's add a new View with View Controller to our Xamarin.Mac project and call it SubviewCollectionView:

Next, let's edit the SubviewCollectionViewController.cs file (that was automatically added to our project) and expose an array (NSArray) of PersonModel classes that we will be Data Binding our Collection View to. To do this add the following code:

private NSMutableArray _people = new NSMutableArray();
...

[Export("personModelArray")]
public NSArray People {
    get { return _people; }
}
...

[Export("addObject:")]
public void AddPerson(PersonModel person) {
    WillChangeValue ("personModelArray");
    _people.Add (person);
    DidChangeValue ("personModelArray");
}

[Export("insertObject:inPersonModelArrayAtIndex:")]
public void InsertPerson(PersonModel person, nint index) {
    WillChangeValue ("personModelArray");
    _people.Insert (person, index);
    DidChangeValue ("personModelArray");
}

[Export("removeObjectFromPersonModelArrayAtIndex:")]
public void RemovePerson(nint index) {
    WillChangeValue ("personModelArray");
    _people.RemoveObject (index);
    DidChangeValue ("personModelArray");
}

[Export("setPersonModelArray:")]
public void SetPeople(NSMutableArray array) {
    WillChangeValue ("personModelArray");
    _people = array;
    DidChangeValue ("personModelArray");
}

Just like we did on the PersonModel class above in the Defining your Data Model section, we've exposed four specially named public methods so that the Array Controller can read and write data from our collection of PersonModels.

Next when the View is loaded, we need to populate our array with this code:

public override void AwakeFromNib ()
{
    base.AwakeFromNib ();

    // Build list of employees
    AddPerson (new PersonModel ("Craig Dunn", "Documentation Manager", true));
    AddPerson (new PersonModel ("Amy Burns", "Technical Writer"));
    AddPerson (new PersonModel ("Joel Martinez", "Web & Infrastructure"));
    AddPerson (new PersonModel ("Kevin Mullins", "Technical Writer"));
    AddPerson (new PersonModel ("Mark McLemore", "Technical Writer"));
    AddPerson (new PersonModel ("Tom Opgenorth", "Technical Writer"));
    AddPerson (new PersonModel ("Larry O'Brien", "API Documentation Manager", true));
    AddPerson (new PersonModel ("Mike Norman", "API Documentor"));

}

Creating the Collection View in Interface Builder

Now we need to create our Collection View, double-click the SubviewCollectionView.xib file to open it for editing in Interface Builder. Drag a Collection View from the Library Inspector and place it on our View:

When you add a Collection View to a User Interface design, two extra elements are also added:

  1. Collection View Item - That manages a single instance of an item in the collection.
  2. Prototype View - A custom view that provides the visual size and appearance of each item in the collection. This view is tied to and managed by the Collection View Item.

Defining the Prototype View

As stated previously, the Collection View doesn't have a predefined, default Collection View Item so we are responsible for totally designing the look and feel of our display items. This is done by editing the Prototype View that was added to our interface design when we added the Collection View.

For an example, select the Prototype View and make it look like the following using an Image View and two Text Fields:

The size that you make the Prototype View, will control the size of each cell in the Collection View's grid.

Handling Selection Highlighting

As also stated above, the Collection View has no built-in support for highlighting a selected item in the grid so we are responsible for handling this ourselves. One easy trick is to add a Box (NSBox) behind every other element in our Prototype View, remove it's title and make it fill the content area of the view:

For example, a NSBox was added behind everything in the view with the following attributes:

We'll be hiding and showing this box to provide feedback to the user when an item is selected in the Collection View.

Adding an Array Controller

A Collection View requires an Array Controller to provide bound data and control and maintain selected items. The Array Controller is bound to our Data Model and defines which fields from the model are exposed to Interface Builder and available for data binding in our Prototype View.

Do the following:

  1. Drag an Array Controller from the Library Inspector onto the Interface Editor:
  2. Select Array Controller in the Interface Hierarchy and switch to the Attribute Inspector:
  3. Enter PersonModel for the Class Name, click the Plus button and add four Keys. Name them Icon, Name, Occupation and isManager:
  4. This tells the Array Controller what it is managing an array of, and which properties it should expose (via Keys).
  5. Switch to the Bindings Inspector and under Content Array select Bind to and File's Owner. Enter a Model Key Path of self.personModelArray:
  6. This ties the Array Controller to the array of PersonModels that we exposed on our View Controller in the Creating and Exposing the Collection Data section above.

Data Binding the Collection View

Now we need to bind our Collection View to the Array Controller, do the following:

  1. Select the Collection View and the Binding Inspector:
  2. Under the Contents turndown, select Bind to and Array Controller. Enter arrangedObjects for the Controller Key field:
  3. Under the Selection Indexes turndown, select Bind to and Array Controller. Enter selectionIndexes for the Controller Key field:
  4. Now we need to data bind the elements of our Prototype View to the instance of of PersonModel being managed by the Array Controller and the Collection View Item.
  5. In the Prototype View, select the Image View. In the Bindings Inspector under the Value turndown, select Bind to and Person (the name of our Collection View Item). Enter representedObject.Icon for the Model Key Path:

    representedObject is the current PersonModel in the array being managed by the Array Controller.
  6. Select the first Label. In the Bindings Inspector under the Value turndown, select Bind to and Person (the name of our Collection View Item). Enter representedObject.Name for the Model Key Path:
  7. Select the second Label. In the Bindings Inspector under the Value turndown, select Bind to and Person (the name of our Collection View Item). Enter representedObject.Occupation for the Model Key Path:
  8. Select the NSBox. In the Bindings Inspector under the Hidden turndown, select Bind to and Person (the name of our Collection View Item). Enter selected for the Model Key Path and NSNegateBoolean for the Value Transformer:
  9. Save your changes and return to Xamarin Studio to sync with Xcode.

Responding to Selection Changes

Unlike most other AppKit controls, Collection Views do not have any built-in events that are raised when the selected items change (either from code or by user interaction). However, we can easily create one using Key-Value Observing.

First, we need to expose our Array Controller via an Outlet (PeopleArray) on the SubviewCollectionView.h file:

Next, we need to edit the SubviewsCollectionView.cs file and make it look like the following:

using System;
using System.Collections.Generic;
using System.Linq;
using Foundation;
using AppKit;

namespace MacDatabinding
{
    public partial class SubviewCollectionView : AppKit.NSView
    {
        #region Computed Properties
        public nint SelectionIndex {
            get { return (nint)PeopleArray.SelectionIndex; }
            set { PeopleArray.SelectionIndex = (ulong)value; }
        }
        #endregion

        #region Constructors
        // Called when created from unmanaged code
        public SubviewCollectionView (IntPtr handle) : base (handle)
        {
            Initialize ();
        }

        // Called when created directly from a XIB file
        [Export ("initWithCoder:")]
        public SubviewCollectionView (NSCoder coder) : base (coder)
        {
            Initialize ();
        }

        // Shared initialization code
        void Initialize ()
        {
        }
        #endregion

        #region Override Methods
        public override void AwakeFromNib ()
        {
            base.AwakeFromNib ();

            // Watch for the selection value changing
            PeopleArray.AddObserver ("selectionIndexes", NSKeyValueObservingOptions.New, (sender) => {
                // Inform caller of selection change
                RaisePersonSelected((nint)PeopleArray.SelectionIndex);
            });
        }
        #endregion

        #region Events
        public delegate void PersonSelectedDelegate(nint index);
        public event PersonSelectedDelegate PersonSelected;

        internal void RaisePersonSelected(nint index) {
            if (this.PersonSelected != null)
                this.PersonSelected (index);
        }
        #endregion
    }
}

First, we have added a convenience property to get or set the currently selected item:

public nint SelectionIndex {
    get { return (nint)PeopleArray.SelectionIndex; }
    set { PeopleArray.SelectionIndex = (ulong)value; }
}

Next, we define an event that will be raised when the selection changes:

public delegate void PersonSelectedDelegate(nint index);
public event PersonSelectedDelegate PersonSelected;

internal void RaisePersonSelected(nint index) {
    if (this.PersonSelected != null)
        this.PersonSelected (index);
}

Finally, we add an observer to our Array Controller Outlet that will be called any time the value of the selectionIndexes property changes:

PeopleArray.AddObserver ("selectionIndexes", NSKeyValueObservingOptions.New, (sender) => {
    // Inform caller of selection change
    RaisePersonSelected((nint)PeopleArray.SelectionIndex);
});

From here we raise our PersonSelected event that we defined above.

Summary

This article has taken a detailed look at working with Collection Views in a Xamarin.Mac application. First, it looked at exposing a C# class to Objective-C by using Key-Value Coding (KVC) and Key-Value Observing (KVO). Next, it showed how to use a KVO compliant class and Data Bind it to Collection Views in Xcode's Interface Builder. Finally, it showed how to interact with Collection Views in C# code.

Xamarin Workbook

If it's not already installed, install the Xamarin Workbooks app first. The workbook file should download automatically, but if it doesn't, just click to start the workbook download manually.