Search code examples
objective-ccocoacore-datacocoa-bindingsosx-lion

Bindings with NSManagedObject from child context only working for NEW objects


Background:

In my app, I'm specifically targeting Mac OS X Lion. This issue involves Core Data, an NSPopover and a child NSManagedObjectContext (created by using the new parentContext property of NSManagedObjectContext).

I have a table of NSManagedObjects of class "Location". There's an Add button that calls addLocation: and if a table row is double-clicked, I call tableViewDoubleClick:.

For either case, what I do is create a new NSManagedObjectContext and set its parent context to that of the document's context. I then either create a new Location in that context or fetch the Location to be edited from the temporary context. I set the popover's representedObject property to the location in question. If I cancel the popover, nothing is saved. If the user clicks a Save button in the popover, I just call save: on the temporary context and the changes get pushed to the main context.

addLocation:

- (IBAction)addLocation:(id)sender
{    
    LocationEditViewController *popupController = [[[LocationEditViewController alloc] init] autorelease];
    popupController.title = @"Add New Location";

    NSManagedObjectContext *tempContext = [[[NSManagedObjectContext alloc] init] autorelease];
    tempContext.parentContext = self.document.managedObjectContext;

    Location *tempLocation = [NSEntityDescription insertNewObjectForEntityForName:@"Location" inManagedObjectContext:tempContext];

    popupController.representedObject = tempLocation;
    popupController.managedObjectContext = tempContext;

    [popupController.popover showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge];
}

tableViewDoubleClick:

- (void)tableViewDoubleClick:(id)sender
{
    NSInteger selectedRow = [self.table selectedRow];
    if (selectedRow != -1) 
    {
        NSRect rectOfSelectedRow = [self.table rectOfRow:selectedRow];
        LocationEditViewController *popupController = [[[LocationEditViewController alloc] init] autorelease];
        popupController.title = @"Edit Location";

        Location *locationToEdit = [self.locationController.selectedObjects objectAtIndex:0];

        NSManagedObjectContext *tempContext = [[[NSManagedObjectContext alloc] init] autorelease];
        tempContext.parentContext = self.document.managedObjectContext;

        Location *tempLocation = (Location *)[tempContext fetchObjectEqualTo:locationToEdit]; // Custom fetch helper method        

        popupController.managedObjectContext = tempContext;
        popupController.representedObject = tempLocation;

        [popupController.popover showRelativeToRect:rectOfSelectedRow ofView:sender preferredEdge:NSMaxXEdge];

    }
}

Here's the problem that I'd like an explanation for:

The text fields in the popover are connected to the popover's representedObject via bindings in the nib. These work perfectly with a new object (addLocation:).

If the Location is an existing object (tableViewDoubleClick:), the bindings work well enough to pre-populate the fields with the Location's properties. However, changing the text in the fields does not alter the Location's properties at all. When the Save button in the popup is clicked, I tried logging the Location's properties before saving the temporary context. If it's an existing object, whatever I type into the fields isn't being reflected in the Location's properties - as if the bindings are only communicating one-way.

My workaround: I found that if I skip the bindings and just manually set the Location's properties to the values in the text fields before the save, that the changes do take effect.

- (IBAction)popoverSave:(id)sender
{
    // These two methods always work. But if I remove these and use bindings instead, it only works for NEW Locations.
    [(Location *)self.representedObject setLabel:self.labelField.stringValue];
    [(Location *)self.representedObject setLocation:self.locationField.stringValue];

    NSLog(@"representedObject = %@", self.representedObject);

    NSError *error = nil;
    [self.managedObjectContext save:&error];
    [self.popover close];
}

I'd really like to know why this is the case, just in case I'm actually doing something wrong.

Thanks!


Solution

  • I think it's likely that it is the cast in these lines:

    [(Location *)self.representedObject setLabel:self.labelField.stringValue];
    [(Location *)self.representedObject setLocation:self.locationField.stringValue];
    

    … that makes them work. If so, then you probably have a NSObject or NSManagedObject set somewhere in the bindings as the class instead of the Location class. When the binding sends a Location class specific message e.g. set an attribute with a specific name, to the generic class, the generic class silently ignores the message.

    BTW, I would caution against using multiple context instead of using the undo API. I see a lot of people get in trouble that way. It's easier to roll back a single context than it is to manage multiple context.