Interfacing Combo Boxes with Core Data
I’ve been putting together a “Learn how to program Cocoa” program, and one of the things I’ve recently struggled with is how to link up an NSComboBox with a Core Data entity so that a heretofore unseen entry in the text box part of the combo box adds an instance of the entity and sets the value of the combo box to the new entity instance.
The following is a marginally clever way to have the controlTextDidEndEditing delegate poke around in the combo box’s bindings so that you only need one version of the delegate for multiple entities and combo boxes (this assumes that the attribute you want to appear in the combo box is called “name” — there might be an even more clever way to have it figure that out from the bindings, but I can’t see how):
#import "ComboBoxDelegate.h"
@implementation comboBoxDelegate
- (void)controlTextDidEndEditing:(NSNotification *)notification
{
NSComboBox * comboBox = [notification object];
NSString * enteredValue = [comboBox stringValue];
// Don't add blank entry to entities.
if ([enteredValue length] == 0)
return;
// Pull the entity and context info from the bindings...
NSArrayController * contentArrayController = [[comboBox infoForBinding:@"content"] objectForKey:@"NSObservedObject"];
NSString * contentEntityName = [contentArrayController entityName];
NSManagedObjectContext * contentContext = [contentArrayController managedObjectContext];
NSEntityDescription * contentEntity = [NSEntityDescription entityForName: contentEntityName
inManagedObjectContext: contentContext];
NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name = %@", enteredValue];
// Check if this entity already exists...
NSFetchRequest * fetch = [[NSFetchRequest alloc] init];
[fetch setEntity: contentEntity];
[fetch setPredicate: predicate];
NSArray * results = [contentContext executeFetchRequest:fetch error:nil];
[fetch release];
// If not, add it:
if (results == nil || [results count] == 0)
{
NSManagedObject * contentObject = [NSEntityDescription insertNewObjectForEntityForName: contentEntityName
inManagedObjectContext: contentContext];
[contentObject setValue: enteredValue forKey: @"name"];
// Refresh the items in the combo box so that we can... (this part is key)
[contentContext processPendingChanges];
[comboBox reloadData];
// ...select the new entry.
[comboBox setObjectValue: enteredValue];
}
}
@end
ComboBoxDelegate.h is pretty simple:
#import <Cocoa/Cocoa.h>
@interface comboBoxDelegate : NSObject
{
}
@end
Note that if you use a relationship (rather than a simple text value) to link up the combo box’s entity with whatever entity you’re editing, you’ll need to create a (reversible) value transformer so that Cocoa Bindings can figure out what the actual entity is given the text description in the combo box:
@implementation toNameTransformer
+ (Class)transformedValueClass { return [NSString class]; }
+ (BOOL)allowsReverseTransformation { return YES; }
- (void)setEntity:(NSString *)entity
{
[entityName release];
[entity retain];
entityName = entity;
}
- (id)transformedValue:(id)item
{
return [item valueForKey:@"name"];
}
- (id)reverseTransformedValue:(id)item
{
NSLog(@"%@", item);
NSManagedObjectContext * context = [[[NSDocumentController sharedDocumentController] currentDocument] managedObjectContext];
NSEntityDescription * entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:context];
NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name = %@", item];
NSFetchRequest * fetch = [[NSFetchRequest alloc] init];
[fetch setEntity: entity];
[fetch setPredicate: predicate];
NSArray * results = [context executeFetchRequest:fetch error:nil];
[fetch release];
if (results != nil && [results count] > 0)
{
return [results objectAtIndex:0];
}
return nil;
}
@end
Then set up a series of transformers, one for each entity, in your document or application’s initialization code:
toNameTransformer * cToNTransformer;
cToNTransformer = [[[toNameTransformer alloc] init] autorelease];
[cToNTransformer setEntity:@"Category"];
[NSValueTransformer setValueTransformer:sToNTransformer
forName:@"nameForCategory"];
and so on for each entity (the above would be for an entity named “Category”). Finally, set the value binding for your combo box to CategoryArrayController.nameForCategory(selection.category) (assuming a relationship named “category”).
There are a lot of newbie details I’ve glossed over (like checking “Automatically Prepares Content” in your array controllers). Contact me if you need clarification. But this would have been really helpful for this guy, so I’ll put it out on the internets on the off chance someone else can benefit.
Post a Comment