2

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?

Community
  • 1
  • 1
msteffens
  • 33
  • 2
  • 6

1 Answers1

1

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.

TechZen
  • 64,370
  • 15
  • 118
  • 145
  • TechZen, many thanks for your answer & suggestions! My situation is slightly convoluted and I should have explained this better in my original post, sorry. I plan to post a dedicated question about my situation, but I guess I should give some quick background info here: At my company, my task ist to develop a Mac "version" of an existing Windows application (wich has already lots of business logic and an existing file format that should also be used for the Mac version). – msteffens Jul 01 '11 at 17:58
  • (comment continued) In order to avoid ending up with a "bad port" (instead of a first-class Mac program with a native GUI), we've decided to develop the GUI (and its accompanying window/view controller code) in Cocoa using Core Data, Bindings & Co. However, to make use of the existing file format & functionality, we thought of exposing the "Windows engine" to Core Data as a custom store (e.g. a NSAtomicStore subclass). In addition, parts of the business logic of this Windows engine would be exposed to the Cocoa app via Mono + MonoMac. – msteffens Jul 01 '11 at 17:59
  • (comment continued) That said, I realize that it's quite unusual to expose yet another "engine" as a custom store to Core Data. I'm aware that this may not be the best idea and may pose memory and syncronisation issues. However, in a way, our situation didn't seem much different to me than having two separate managed object contexts (usually in different threads) where changes occur independently, and which need to be synchronised. Thus, I was thinking whether I could use the same "context merging" techniques for my situation. – msteffens Jul 01 '11 at 17:59
  • If you already have a working model layer in Mono, I would suggest just skipping Core Data entirely. It will be easier to write controller code for an existing model than it will be to shoehorn an existing model into Core Data. Your situation with the two context on two threads is a common one but you still can't roll back past a save without jumping through a lot of hoops. – TechZen Jul 01 '11 at 20:24
  • I've seen people use a divided process app to port a difficult model. You split the app into two processes that communicate. One process runs the model, in Mono in this case, while the other is a pure Cocoa app with all the nifty UI goodness. – TechZen Jul 01 '11 at 20:26
  • TechZen, thanks again for your comments & the helpful suggestions! It was my hope to be able to leverage the advantages of Core Data. E.g., our Windows engine (to be exposed via MonoMac) doesn't support Undo while Core Data does. By wiring the two engines together via NSNotifications (containing collections of all inserted/updated/deleted objects) the Undo feature could be maintained – at least if the issues of my above question could be resolved. Also, another major reason for our approach was, that we already have a working prototype of the GUI that was rapidly developed with Core Data. – msteffens Jul 02 '11 at 09:34
  • You might try treating the Mono engine the same way you might treat a server i.e. by passing objects back and forth using some standard object data format like JSON. However, the default Core Data undo will not preserve undos past the last save. That is an intentional feature of the API design. – TechZen Jul 02 '11 at 15:55
  • Ok. We've also thought about the "server approach" (passing objects back & forth in a standard representation), and will consider it again. Thanks again! – msteffens Jul 04 '11 at 09:31