Search code examples
objective-ccocoacore-datansmanagedobjectcontextundo-redo

Core Data: How to merge inserts/updates/deletes between two NSManagedObjectContext's while maintaining the merge as an undoable step?


I have a document-based Core Data application (running on Mac OS X 10.5 and above) where I'm trying to use two NSManagedObjectContext's on the main thread. I'd like to merge the changes made in the secondary context into my main (primary) context. In addition, I want the changes that were merged in from the secondary context to be undoable and to cause the document to be marked "dirty". I guess my question is similar to "Undoing Core Data insertions that are performed off the main thread" but, ATM, I'm not using different threads.

I've been observing the NSManagedObjectContextDidSaveNotification (which gets sent from the second context when calling -[self.secondaryContext save:]) like this:

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(mocDidSave:)
                                             name:NSManagedObjectContextDidSaveNotification
                                           object:self.secondaryContext];

In the -mocDidSave: method called by the observer I tried to use -[NSManagedObjectContext mergeChangesFromContextDidSaveNotification:] on the primary context to merge the changes from the secondary context into the primary context:

- (void)mocDidSave:(NSNotification *)notification
{
    [self.primaryContext mergeChangesFromContextDidSaveNotification:notification];
}

However, while the, say, inserted objects readily appear in my array controller, the document is not marked dirty and the isInserted property of the newly added managed objects is not set to YES. Also the insertion (into the primary context) is not undoable.

Re-faulting any inserted objects will at least mark the document dirty but the insertion is still not undoable:

- (void)mocDidSave:(NSNotification *)notification
{
    [self.primaryContext mergeChangesFromContextDidSaveNotification:notification];

    for (NSManagedObject *insertedObject in [[notification userInfo] objectForKey:NSInsertedObjectsKey]) {
        [self.primaryContext refreshObject:[self.primaryContext existingObjectWithID:[insertedObject objectID] error:NULL] mergeChanges:NO];
    }
}

W.r.t. -mocDidSave:, I had slightly better results with a custom implementation:

- (void)mocDidSave:(NSNotification *)notification
{
    NSDictionary *userInfo = [notification userInfo];

    NSSet *insertedObjects = [userInfo objectForKey:NSInsertedObjectsKey];
    if ([insertedObjects count]) {
        NSMutableArray *newObjects = [NSMutableArray array];
        NSManagedObject *newObject = nil;
        for (NSManagedObject *insertedObject in insertedObjects) {
            newObject = [self.primaryContext existingObjectWithID:[insertedObject objectID] error:NULL];
            if (newObject) {
                [self.primaryContext insertObject:newObject];
                [newObjects addObject:newObject];
            }
        }

        [self.primaryContext processPendingChanges];

        for (NSManagedObject *newObject in newObjects) {
            [self.primaryContext refreshObject:newObject mergeChanges:NO];
        }
    }

    NSSet *updatedObjects = [userInfo objectForKey:NSUpdatedObjectsKey];
    if ([updatedObjects count]) {
        NSManagedObject *staleObject = nil;
        for (NSManagedObject *updatedObject in updatedObjects) {
            staleObject = [self.primaryContext objectRegisteredForID:[updatedObject objectID]];
            if (staleObject) {
                [self.primaryContext refreshObject:staleObject mergeChanges:NO];
            }
        }
    }

    NSSet *deletedObjects = [userInfo objectForKey:NSDeletedObjectsKey];
    if ([deletedObjects count]) {
        NSManagedObject *staleObject = nil;
        for (NSManagedObject *deletedObject in deletedObjects) {
            staleObject = [self.primaryContext objectRegisteredForID:[deletedObject objectID]];
            if (staleObject) {
                [self.primaryContext deleteObject:staleObject];
            }
        }

        [self.primaryContext processPendingChanges];
    }
}

This causes my array controller to get updated, the document gets marked dirty, and the insertions & deletions are undoable. However, I'm still having problems with updates which aren't yet undoable. Should I instead manually loop over all updatedObjects and use -[NSManagedObject changedValues] to reapply the changes in the primary context?

This custom implementation would, of course, duplicate a lot of work from the secondary context on the main context. Is there any other/better way of getting a merge between two contexts while maintaining the merge as undoable step?


Solution

  • If you are not using separate threads, then you don't actually need to separate contexts. Using two context on the same thread adds complexity without gaining anything. If you don't know for certain you will employ threads, then I would highly recommend just using the one context. Premature optimization is the root of all evil.

    Saves reset the Undo manager so you can't use NSManagedObjectContextDidSaveNotification to perform any operation that can be undone. As you found you can trick the app into thinking the document is dirty but you can't force the Undo manager to remember past the last save.

    The only way to do that, to get unlimited undo, is to save multiple versions of the doc behind the scenes. IIRC, you can also serialize the undo manager so that it can be written to file and reloaded to backtrack.