Mac OS X - Работа Cocoa Bindings.

Программирование (и работа) всего пользовательского интерфейса в Mac OS X основана на модели (петтерне) Model-View-Controller (MVC).

Этот паттерн четко разделяет данные, с которыми идет работа (Model), визуальное представление, служащее для показа и/или редактирования этих данных (View) и связующих их код (Controller).

MVC

Рис 1. Схема паттерна MVC.

Все взаимодействие между моедлью и ее визуальным представлением идет только через Controller. При этом можно использовать сразу несоклько различных компонент View (одни и те же данные могут быть представлены как текстовы поля, набор слайдер, диаграмма, график и т.п) и Controller обеспечивает их синхронизацию.

MVC

Рис 2. Работа с несколькими View.

При этом Cocoa предоставляет большой набор готовых классов, служащих как для показа (редактирвоания) данных (объекты, унаследованные от NSView) и большой набор классов, облегчающих написание модели.

Типичная задача Controller'а - передача значения от модели к ее визуальному представлению (представлениям) и при изменении значения в визаульном представление - передача изменений в модель (и остальные визуальные представления).

И неудивительно, что основная часть кода Controller'а очень однообразна и проста, хотя и требует написания кода.

Начниная с Mac OS X 10.3 Panther в Cocoa входит очень удобный и мощный механзи, получивший название Cocoa Bindings (инаогда используется название Controller Layer). Его использование позволяет заметно сокраитить объек кода, переложив основные функции Controller'а на предоставленные готовые объекты.

В основе Cocoa Bindings лежат два понятия Key-Value Coding и Key-Value Observing и основанные на них готовые контрорллеры - NSController, NSObjectController, NSArrayController, NSUserDefaultsController, NSTreeController.

Key-Value Coding (KVC)

Key-Value Coding это протокол, поддерживаемый практически всеми объектами Cocoa. Основная его задача - дать унифицированный механизм доступа п полям объектов по именам этих полей.

Пусть у нас есть следующий класс:

@interface Employee : NSObject
{
    NSString * name;
	NSString * department;
	float      salary;
	Employee * boss;
}

- (NSString *) name;
- (NSString *) department;
- (float)      salary;
- (Employee *) boss;
- (void) setName: (NSString *) newName;
- (void) setDepartment: (NSString *) newDepartment;
- (void) setSalary: (float) newSalary;
- (void) setBoss: (Employee *) newBoss;
- (void) dealloc;
@end

Тогда используя KVC можно получить имя при помощи следующего вызова:

id name = [person valueForKey: @"name"];

Этот вызов приведет к посылке сообщения name, в результате чего будет получен указатель на строку, который и будет возвращен. Мы также можем использовать метод valueForKey: для получения значения поля salary, но результат мы получим не в виде числа типа float, а в вие указателя на объект класса NSNumber.

Это связано с тем, что для обеспечения единообразного доступа ко всем полям, значения должны возвращаться как указатели на объект, унаследованный от NSObject.

Аналогично можно использовать метод setValue:forKey: для записи значения в поле:

[person setValue: [NSNumber numberWithFloat: 100000.0f] forKey: @"salary"];
[person setValue: @"IT" forKey: @"department"];

Таким образом KVC предоставляет унимицированный способ доступа к полям произвольных объектов по именам этих полей на основе следующих двух методов:

- (id) valueForKey: (NSString *) key;
- (void) setValue: (id) value forKey: (NSString *) key;

При этом нет необхожимости самому реализовывать эти методы для каждого класса - есть стандартная реализация, используя информацию о классе объекта для получения доступа к его полям. Так если нужно прочесть значение поля boss, то сперва будет проверено наличие метода чтения boss или boss_, а при их отсутствии будет проверено наличие поля с именем boss или boss_. Если этого поля не будет найдено, то произойдет исключение (exception).

Для записи поля boss сперва проверяется наличие методов setBoss или setBoss_, а при неудаче - проверено наличие поля с именем boss или boss_. При неудаче выбрасывается исключение.

Таким образом для нормальной поддержки KVC достаточно просто для каждого поля дать методы доступа (accessor'ы для чтения и записи) следуя указанной выше схеме наименования. Хотя этого можно и не делать (тогда просто будет прямой доступ к полю), это не желательно, так наличие этих методов понадобится нам далее для Key-Value Observing.

Все преобразования типов (например между float и NSNumber *) происходят автоматически.

Используемый в Mac OS X 10.5 Leopard язык Objective-C 2.0 поддерживает автоматическое создание accessor'ов.

Однако KVC дает больше, чем просто единообразный доступ к полям данного объекта. Довольно часто поле объекта является указателем на другой объект, поля которго также могут быть указателями и т.д. KVC позволяет сразу обратиться к полю сквозь всю цепочку названий промежуточных полей - достаточно их соединить между собой при помощи символа ".".

Так для получения имени начальника можно использовать следующий вызов:

id name = [person valueForKey: @"boss.name"];

Чтобы узнать имя начальника начальника можно использовать имя (путь) "boss.boss.name".

Кроме того Objective-C 2.0 позволяет использовать гораздо более привычныую форму доступа к полм по имени:

id name = person.boss.name;
person.department = @"Marketing";

Естественно, что все эти вызовы переводятся в стандантные вызовы valueForKey: и setValue:forKey:, но это автоматически делается комплятором, а разработчики получают возможность ис пользовать более привычную форму работы с полями.

Для класса NSDictionary (NSMutableDictionary) используется расширенная реализация KVC, позволяющая использовать ключи в словаре в качестве имен полей (если эти ключи являются строками).

Key-Value Observing

Key-Value Observing (KVO) - это механизм, позволяющий одним объектам отслеживать изменения полей других объектов. Поддержка KVO встроена в базовый класс NSObject и для ее использования служат следующие два метода:

- (void) addObserver: (id) observer forKeyPath: (NSString *) keyPath options: (NSKeyValueObservingOptions) options context: (void *) context;
- (void) removeObserver: (NSObject *) observer forKeyPath: (NSString *) keyPath;

Посылка первого из этих сообщений регистрирует переданный объект observer как наблюдателся за полем с именем keyPath. При этом в параметре options передаются дополнительные опции для наблюдения (просто набор битов) и context - это просто некоторый указатель, передаваемый наблюдателю.

При помощи сообщения можно прекратить наблюдения за заднным полем.

Фактически KVO реализовано таким образом, что не требует почти никаких усилий от программиста - когда поступает запрос на наблюдения за данным объектом, то у данного объекта (именного у него, а не у всего класса) происходит подмена setter'а на метод, который кроме вызова этого setter'а также пошлет сообщение observeValueForKeyPath:ofObject:change:context: наблюдателю.

- (void) observeValueForKeyPath: (NSString *) keyPath ofObject: (id) object change: (NSDictionary *) change context: (void *) context;

Параметры object и keyPath содержат адрес наблюдаемого объекта и имя поля, значение которго изменилось. Поле context содержит переданный при запросе на наблюдение контекст, а словарь change содержит дополнительную информацию о происшедшем изменении.

При этом важно, что все что нужно для того, чтобы за заданным полем объекта можно было наблюдать - это наличие setter'а для этого поля. Аналог Cocoa Bindings есть и в M$ WPF (правда под названием Data Binding, вообще в WPF моожно найти массу заимствований из Cocoa), но там для того, чтобы за полем можно было наблюдатеь необходимо дабавелние специального кода в setter. KVO этого не требует - все происходит полностью автоматически.

Key-Value Binding (Cocoa Bindings)

За счет использования KVC и KVO можно легко реализовать несколько почти универсальных Controller'ов. Ведь фактически задача каждого Controller'а это отслеживание изменений в модели и передача новых значений всем используемым View и отслеживание изменения значнеий в View и передача измененных значений всем остальным.

За счет использования Key-Value Observing можно легко реализовать остлеживание изменения в полях модели, а использование Key-Value Coding предоставляет универсальный способ доступа к значениям (отслеживание изменений занчений в визуальной компоненте намного проще - обычно это и так ведет к псоылке сообщения).

Таким образом можно просто связать поля модели с визуальными компонентами интерфейса так, что изменения в одном месте будет автоматически передваться во все остальные места.

Важным моментом является то, что все эти связи можно задать прямо в Interface Builder'е и они не требуют написания вообще никакого дополнительного кода - всю работу делают специальные объекты-контроллеры.

Далее мы рассмотрим несколько простых примеров, иллюстрирующих использование Cocoa Bindings.

Запустим XCode и сосздадим новый проект типа Cocoa Application.

Рис 3. Создаем новый проект

Далее добавим новый класс (Модель) в наш проект при помощи правого (???) клика мышью на элемент Classes в окне проекта и выбрать Add|New File ... (рис. 4).

Рис 4. Добавляем новый класс.

В качестве добавляемого класса выберем подкласс NSObject и зададим в качестве имени класса Model (рис 5 и 6).

Рис. 5. Задание типа добавляемого класса.

Рис 6. Задание имени добавляемого класса.

Далее откроем созданные файлы Model.h и Model.m и заполним поисание и реализацию класса. Поскольку в данном примере Model будет просто хранить числовое значение, отображаемое и изменяемое при помощи нескольких визуальных компонент, то нам хватит одной instance-переменной, содержащей значнеие типа NSNumber * и соответствующих accessor-методов.

#import <Cocoa/Cocoa.h>

@interface Model : NSObject 
{
    IBOutlet NSNumber * value;
}

@end

#import "Model.h"


@implementation Model

- (void) dealloc
{
    [value release];
    [super dealloc];
}

@end

Вся остальная работа будет проводиться в Interface Builder'е поэтому запустим его двойным щелчком мыши по файлу Main Menu.nib в окне проекта.

Далее выполним операцию синхронизации с XCode - при этом вся информация о созданных в XCode классах станет доступна из Interface Builder'а. Для этого служит команда меню File|Synchronize.

Рис 7. Синхронизация с XCode.

Далее перейдем в окно Library и перетащим объект NSObjectв окно Main Menu.nibю Для быстрого поиска нужного объекта (компоненты) в окне Library можно воспользоваться полем поиска в нижней части окна.

Рис 8. Выбор NSObject'а в окне компонент.

Рис 9. Перетаскивание объекта в окно Main Menu.nib.

Далее выделим перетащенный объект и в окне свойств выберем закладку Object Identity (рис. 10), в поле Class выберем Model.

Рис 10. Задание класса для объекта.

Обратите внимание, что после этого в разделе Class Outlets появится поле value класса Model.

Рис 11. Свойства модели в Interface Builder'е.

Далее приступим к работе с окном. Сначала изменим его заголовок на "Cocoa Bindings 1". Затем добавим поле NSTextFieldдля редактирования числового значения. Чтобы быстро найти эту компоненту в окне Library достаточно набрать "text" в поле поиска, после чего останутся только те компоненты, которые связаны с работой с текстом.

Рис 12. Поиск компоненты NSTextField.

После этого просто перетащим компоненту на окно и настроим ее свойства.

Рис 13. Добавляем текстовое поле в окно.

Далее добавляем объекты NSStepper справа от поля ввода и NSSlider внизу. Также сбоку поместим кномпу "Quit" и отделяющую ее вертикальную линию.

Рис 14. Окончательный вид окна.

Далее привяжем кнопку к сообщению terminate: у объекта First Responder (???), для этого достаточно удерживая нажатой клавишу Ctrl "протянуть" соединение от кнопки "Quit" к объекту First Responder. После этого в появившемся окне из всего списка методов выбираем terminate:.

Рис 15. Привязка кнопки "Quit".

После этого все, что нам осталось сделать это подключиьт (bind) компоненты, позвоялющие показывать и изменять значение к полю vbalue нашей модели (объекту Model в окне Main Menu.nib).

Рис 16. Настройка привязывания для текстового поля.

Для этого для каждого из этих объектов выедлим его и в окне свойств выберем закладку Bindings. Далее в разделе Value "включим" поле "Bind To" и выберем какому объекту мы хотим "привязать" - это объект Model.

Рис 17. Параметры приязывания для текстового поля.

После этого сохраним порлучившийся nib-файл. Обратите внимание, что при попытке сохранения файла мы получаем окно со свойствами nib-файла, в котором можно выбрать минимальную версию Mac OS X, на которую расчитано приложение, и для выбранной версии обозначены все возможные несовместимости.

Выберем в качестве минимальной версии Mac OS X 10.4.

Рис 18. Окно со свойствами nib-файлами и предупреждениями о возможных непереносимостях.

После этого вернемся обратно в XCode и запустим наш пример.

Рис 19. Работающее приложение.

Получившееся приложенние позволяет отображать и изменять одно и то же значение при помощи трех разных визуальных компонент и за счет использования Cocoa Bindings все значения автоматически синхронизируются, какмим бы способом мы его не меняли.

Поскольку все стандартные компоненты поддерживают KVC, то можно устанавливать Bindings на любые свойства стандартных компонент. В следующем примере мы привяжем основные свойства компоненты NSSlider к набору отдельных компонент и получим возможность на ходу изменять параметры для данной компоненты - без написания какого-либо кода вообще.

Создадим новый проект типа Cocoa Application (рис 20).

Рис 20. Создаеми новый проект.

Далее сразу же перейдем к редактипрованию nib-файла. Первое, что нам нужно будет сделать - это перенести на окно собственно сам NSSlider, свойства которогоо мы будем "привязывать" к другим компонентам. Достаточно в поле поиска окна Library набрать "sl" как мы сразу получим все основные компоненты связанные со слайдерами.

Рис 21. Выбор слайдеров.

Аналогичным образомт перетащим остальные компоненты для получения окна, показанного на рисунке 22.

Рис 22. Оконачательный вид окна.

Компоненты, расположенные сбоку от слайдера мы "подключим" к основным параметрам слайдера и в результате получим возможность управления слайдером прямо на ходу.

Кнопку "Quit", как и ранее, подключим к методу terminate: объекта First Responder.

Рис 23. Подключение кнопки "Quit".

При помощи Ctrl-клика мышью по кнопке "Quit" мы можем вызвать панель со всеми outlet'ми и action'ми этой кнопки. Обратите внимание, что их назначение можно менять прямо из этого окна - достаточно "протащить" связь от окружности, соответствующей задаваемому элементу к назначению.

Рис 24. Настройка outlet'ов и action'ов для кнопки "Quit"

Из палитры компонент (окна Library) передащим объект NSObjectController в окно MainMenu.nib - это и будет наш конт роллер, осуществляющий всю координацию изменений.

Следующим шагом будет подключение контроллера - для этого используем Ctrl-drag от контроллера в окне MainMenu.nib к слайдеру и в появившемся списке выберем contents.

Рис 25. Подключение контроллера к слайдеру.

Далее приступим к непосредственно к гнастройке связей для свойств слайдера. Сперва выделим текстовое оле с меткой "Number Of Ticks" и в окне свойств откроем закладку "Bindings". Далее выставим связи как показано на рисунке 24 - само значение привязывается к свойству selection объекта ObjectController по ключу (пути) numberOfTickMarks.

Рис 26. Настройка связей для поля с меткой "Number Of Ticks:".

Свойство selection для ObjectController'а соответсвует "контроллируемому" объекту, т.е. в нашем случае слайдеру.

Аналогичным образом "привяжем" к тем же параметрам объект NSStepper - он также будет использоваться для изменения числа отметок на слайдере. Поля с метками "Min Value:" и "Max Value:" привяжем к путям minValue и maxValue соотвственно.

Поле с меткой "Value:" привяжем к пути floatValue, а checkbox с меткой "Ticks Only:" прияжем к ключу allowsTickMarkValuesOnly.

Рис 27. Настройка привязывания для свойств слайдера.

Далее сохраним nib-файл и вернемся в XCode, собрем и запустим приложение.

Рис 28. Работающее приложение.

Убедимся, что все компоненты корректно изменяют свойства слайдера и изменение изначения слайдера синхронизировано с полем "Value:". Таким образом мы автоматически получили полную синхронизацию свойств слайдера со значения ряда других компонент (в том числе и для случая, когда одному свойству соответствует сразу несколько компонент) ни написав при этом ни стройки кода - все было сделано на уровне ресурсов.

Следующий пример будет использовать еще один из станлдартных контроллеров Cocoa - NSArrayController. Этот контроллер позволяет рабоать не с одним объектом, а сразу с массивом объектов заданного класса, поддерживая как создание/удаление/редактирование объектов в таблице, так и в отдельном окне (подход -Master-Detail View).

Создадим новый проект и сразу добавим в него новый класс Person (аналогично тому как это делолось в первом примере).

Рис 29. Создание класса Person.

Этот класс будет преджставолять из себя описание человека и содержаить оснонвые атрибуты - имя и фамилия (firstName и lastName), номер телефона (phone), e-mail (email), дату рождения (birthDate) и фотографию (photo).

Для использования Cocoa Bindings сделаем accessor'ы ко всем этим атрибутам. Ниже приводится файл Person.h.

#import <Cocoa/Cocoa.h>


@interface Person : NSObject
{
    NSString * firstName;
    NSString * lastName;
    NSString * phone;
    NSString * email;
    NSDate   * birthDate;
    NSImage  * photo;
}

@property (retain) NSString * firstName;
@property (retain) NSString * lastName;
@property (retain) NSString * phone;
@property (retain) NSString * email;
@property (retain) NSDate   * birthDate;
@property (retain) NSImage  * photo;
 
@end

На следующем листинге приводится файл Person.m.

#import "Person.h"

@implementation Person

- (void) dealloc 
{
        [firstName autorelease];
        [lastName autorelease];
        [phone autorelease];
        [birthDate autorelease];
        [photo autorelease];
        
        [super dealloc];
}

@synthesize firstName;
@synthesize lastName;
@synthesize phone;
@synthesize birthDate;
@synthesize email;
@synthesize photo;  

@end

Далее откомпилируем файлы нашего проекта, перейдем в Interface Builder и выполним команду Synchronize.

Для начала перенесем объект NSTableView (рис 30) в наше окно.

Рис 30. Выбор компонента NSTableView.

После этого настроим свойства таблицы - нам нужно создать таблицу с двумя столбцами с именами "First Name" и "Last Name".

Рис 31. Настройка таблицы из главного окна.

Перенесем в окно MainMenu.nib объект NSArrayController и приступим к его настройке. Для этого выделим перетащенный контроллер и в окне свойств в первой закладке зададим в поле CLass Name имя нашего класса Person, массивом экземпляров данного класса и будеи управлять контроллер.

Рис 32. Своства контроллера.

После этого добавим в иаблицу Key все атрибуты класса Person. Далее добавим в окно три кнопки - Add ..., Remove и Info ....

Рис 33. Итоговый вид главного окна.

Далее подключим первые две из этих кнопок к методам контроллера add: и remove:.

Рис 34. Подключение кнопки "Add ...".

Далее несколькими щелчками мыши выделим первый столбец таблицы и в окне свойств откроем для этого столбца закладку Bindings - Cocoa позволяет задавать привязку отдельных столбцов таблицы.

Необходимо подключить весь столбец к атрибубу контроллера arrangedObjects (этот атрибут соответствует массиву управляемых контроллером объектов) и в качестве ключа (пути) выберем из списка ключей firstNameю

Рис 35. Задание привязки для первого столбца таблицы.

Аналогично второй стобец таблицы подключим к ключу lastName.

Рис 36. Настройка стоблца таблицы.

Далее "вытащим" новое окно из палитры компонент (Library) - это будет detail view, в котором можно будет редактировать все атрибуты объекта Person, выделенного в Master View.

Рис 37. Окнончательный вид detail-окна.

Разместим в этом окне компоненты для показано на рис 35 (большапя компонента справа - это объект класса NSImageView).

Далее точно также "привяжем" поля этого окна к контроллеру, оданко в качестве значения Controller Path будем использовать selection, соответствующий выделенной строке в таблице (т.е. текущему объекту Person).

Рис 38. Настройка привязывания для поля First Name detail-окна.

Кнопку Info ... подключим к метода detail-окна makeKeyAndOrderFron:.

Рис 39. Подключение кнопки "Info ...".

Подключим свойство Caption detail-окна к атрибуту lastName ключа selectionконтроллера. Далее сохраним nib-файл и соберем проект. После его запуска мы сможем добавлять новые строки (объекты) в таблицу при помощи кнопки Add ... и удалять при помощи кнопки Remove.

Кнопка Info ... делает detail-окно видимым и активным. Обратите внимание, что редактировать объекты можно как непосредственно в самой таблице, так и в detail-окне. Для добавления фотография выбранному объекту следует использовать механизм Drag-and-Drop - картинка просто перетаскивается в поле для картинке на detail-view.

Рис 40. Редактирование одной записи.

Мы получили почти готовое приложение для работы со списокм людей (в нем пока еще нет сохранения и чтения списков из файлов и поиска) с достаточно большой функциональностью. И весь код, который нужно было для этого написать это просто класс, представляющий один контакт и для него описать accessor'ы - все остальное (включая и создание этих самых accessor'ов) было получено нами автоматически.

При этом всю работу, связанную с возмоностью редактирвоания объектов (причем сразу в разных местах), взял на себя готовый объект-контроллер.

Весь исходный код к этой статье можно скачать по следующим ссылкам - код к первому примеру код ко второму примеру код к третьему примеру.

Обратите внимание, что все скриншоты соответствуют версии Interface Builder 3.

Используются технологии uCoz