无法撤消多个操作

时间:2011-10-20 14:32:19

标签: objective-c cocoa core-data nsundomanager

当我在删除单个对象后对上下文调用undo时,所有内容都按预期工作。但是如果用户删除了一个对象,然后删除了另一个对象,则无论用户请求撤消多少次,undo都只能恢复第二个对象,就好像undoLevels被设置为1.无论undoLevels是否为默认值0,都会发生这种情况。无限制)或明确设置为6作为测试。

此外,如果单个操作删除多个对象,则之后调用undo无效;没有任何对象被恢复。我尝试用begin / endUndoGrouping明确包围删除循环,但无济于事。 undoManager的groupsByEvent是YES(默认情况下),但是我调用直接撤消或undoNestedGroup没有区别。

每次操作后上下文是否以某种方式保存?不,因为如果我在运行这些测试后退出并重新启动应用程序,则所有对象仍然存在于数据库中。

我错过了什么?


好的,你想要代码。以下是我认为最相关的内容:

上下文getter:

- (NSManagedObjectContext *) managedObjectContextMain {

if (managedObjectContextMain) return managedObjectContextMain;

NSPersistentStoreCoordinator *coordinatorMain = [self persistentStoreCoordinatorMain];
if (!coordinatorMain) {
    // present error...
    return nil;
}
managedObjectContextMain = [[NSManagedObjectContext alloc] init];
[managedObjectContextMain setPersistentStoreCoordinator: coordinatorMain];

// Add undo support. (Default methods don't include this.)
NSUndoManager *undoManager = [[NSUndoManager  alloc] init];
// [undoManager setUndoLevels:6]; // makes no difference
[managedObjectContextMain setUndoManager:undoManager];
[undoManager release];

// ...

return managedObjectContextMain;
}

多对象删除方法(由模态面板上的按钮调用):

/* 
NOTE FOR SO: 
SpecialObject has a to-one relationship to Series. 
Series has a to-many relationship to SpecialObject.
The deletion rule for both is Nullify.
Series’ specialObject members need to be kept in a given order. So Series has a transformable attribute, an array of objectIDs, used to prepare a transient attribute, an array of specialObjects, in the same order as their objectIDs.
*/
- (void) deleteMultiple {
Flixen_Foundry_AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
NSManagedObjectContext *contextMain = [appDelegate managedObjectContextMain];

NSUndoManager *undoMgr = [contextMain undoManager];
[undoMgr beginUndoGrouping];

// Before performing the actual deletion, drop the seln in the locator table.
[appDelegate.objLocatorController.tvObjsFound deselectAll:self];

// Get the indices of the selected objects and enumerate through them.
NSIndexSet *selectedIndices = [appDelegate.objLocatorController.tvObjsFound selectedRowIndexes];
NSUInteger index = [selectedIndices firstIndex];
while (index != NSNotFound) {
    // Get the obj to be deleted and its series.
    SpecialObject *sobj = [appDelegate.objLocatorController.emarrObjsLoaded objectAtIndex:index];       
    Series *series = nil;
    series = sobj.series;
    // Just in case...
    if (!series) {
        printf("\nCESeries' deleteMultiple was called when Locator seln included objs that are not a part of a series. The deletion loop has therefore aborted.");
        break;
    }
    // Get the obj's series index and delete it from the series.
    // (Series has its own method that takes care of both relnshp and cache.)
    NSUInteger uiIndexInSeries = [series getSeriesIndexOfObj:sobj];
    [series deleteObj:sobj fromSeriesIndex:uiIndexInSeries];
    // Mark the special object for Core Data deletion; it will still be a non-null object in emarrObjsLoaded (objLocatorController’s cache).
    [contextMain deleteObject:sobj];
    // Get the next index in the set.
    index = [selectedIndices indexGreaterThanIndex:index];
}

[undoMgr endUndoGrouping];

// Purge the deleted objs from loaded, which will also reload table data.
[appDelegate.objLocatorController purgeDeletedObjsFromLoaded];
// Locator table data source has changed, so reload. But end with no selection. (SeriesBox label will have been cleared when Locator seln was dropped.)
[appDelegate.objLocatorController.tvObjsFound reloadData];

// Close the confirm panel and stop its modal session.
[[NSApplication sharedApplication] stopModal];
[self.panelForInput close];
}

这是从对象和有序缓存中删除对象的Series方法:

/**
Removes a special object from the index sent in.
(The obj is removed from objMembers relationship and from the transient ordered obj cache, but it is NOT removed from the transformable array of objectIDrepns.)
*/
- (void) deleteObj:(SpecialObject *)sobj fromSeriesIndex:(NSUInteger)uiIndexForDeletion {
// Don't proceed if the obj is null or the series index is invalid.
if (!sobj)
    return;
if (uiIndexForDeletion >= [self.emarrObjs count]) 
    return;

// Use the safe Core Data method for removing the obj from the relationship set.
// (To keep it private, it has not been declared in h file. PerformSelector syntax here prevents compiler warning.)
[self performSelector:@selector(removeObjMembersObject:) withObject:sobj];
// Remove the obj from the transient ordered cache at the index given.
[self.emarrObjs removeObjectAtIndex:uiIndexForDeletion];

// But do NOT remove the obj’s objectID from the transformable dataObjIDsOrdered array. That doesn't happen until contextSave. In the meantime, undo/cancel can use dataObjIDsOrdered to restore this obj.
}

这是方法及其后续工作,由comm-z undo:

调用
- (void) undoLastChange {
Flixen_Foundry_AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
NSManagedObjectContext *contextMain = [appDelegate managedObjectContextMain];

// Perform the undo. (Core Data has integrated this functionality so that you can call undo directly on the context, as long as it has been assigned an undo manager.)
//  [contextMain undo]; 
printf("\ncalling undo, with %lu levels.", [contextMain.undoManager levelsOfUndo]);
[contextMain.undoManager undoNestedGroup]; 

// Do cleanup.
[self cleanupFllwgUndoRedo];
}


- (void) cleanupFllwgUndoRedo {
Flixen_Foundry_AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
NSManagedObjectContext *contextMain = [appDelegate managedObjectContextMain];
DataSourceCoordinator *dataSrc = appDelegate.dataSourceCoordinator;

// ... 

// Rebuild caches of special managed objects.
// (Some managed objects have their own caches, i.e. Series' emarrObjs. These need to be refreshed if their membership has changed. There's no need to use special trackers; the context keeps track of these.)
for (NSManagedObject *obj in [contextMain updatedObjects]) {
    if ([obj isKindOfClass:[Series class]] && ![obj isDeleted])
        [((Series *)obj) rebuildSeriesCaches];
}

// ...

// Regenerate locator's caches.
[appDelegate.objLocatorController regenerateObjCachesFromMuddies]; // also reloads table

}

这是在undo / awake之后重新生成缓存的系列方法:

- (void) rebuildSeriesCaches {  

// Don't proceed if there are no stored IDs.
if (!self.dataObjIDsOrdered || [self.dataObjIDsOrdered count] < 1) {    
    // printf to alert me, because this shouldn’t happen (and so far it doesn’t)
    return;
}

NSMutableArray *imarrRefreshedObjIdsOrdered = [NSMutableArray arrayWithCapacity:[self.objMembers count]];
NSMutableArray *emarrRefreshedObjs = [NSMutableArray arrayWithCapacity:[self.objMembers count]];

// Loop through objectIDs (their URIRepns) that were stored in transformable dataObjIDsOrdered.
for (NSURL *objectIDurl in self.dataObjIDsOrdered) {
    // For each objectID repn, loop through the objMembers relationship, looking for a match.
    for (SpecialObject *sobj in self.objMembers) {
        // When a match is found, add the objectID repn and its obj to their respective replacement arrays.
        if ([[sobj.objectID URIRepresentation] isEqualTo:objectIDurl]) {
            [imarrRefreshedObjIdsOrdered addObject:objectIDurl];
            [emarrRefreshedObjs addObject:sobj];
            break;
        }
        // If no match is found, the obj must have been deleted; the objectID repn doesn't get added to the replacement array, so it is effectively dropped.
    }
}

// Assign their replacement arrays to the transformable and transient attrs.
self.dataObjIDsOrdered = imarrRefreshedObjIdsOrdered;
self.emarrObjs = emarrRefreshedObjs;

}

(我省略了Locator的regenerateObjCachesFromMuddies,因为虽然我使用它的表来查看删除和撤消的结果,但我可以用新的fetch重新加载表,完全重新生成表的缓存,这个测试仍然显示撤消无效。)

像往常一样,只是组合一个SO问题的任务有助于解决问题,我现在意识到只要我使用不涉及互惠的SpecialObject-Series关系的简单对象,撤消工作正常。我在那里做错了......

2 个答案:

答案 0 :(得分:1)

我认为你正在与自定义撤消内容和Core Data的自动支持进行斗争。

在正常的撤消/重做代码中,您有可撤消的漏斗点。通常是可撤销的添加及其反向可撤消删除。调用一个将另一个注册为反向操作,反之亦然。用户撤消/重做然后只是在它们之间来回。您将“用户创建新Foo”代码与“现在将此foo添加到集合中的可撤销”代码分开(这样“删除Foo”和“添加Foo”工作独立于提供新创建的Foo)。

使用Core Data,添加和删除意味着“插入上下文并从上下文中删除”。此外,您仍然需要自定义漏斗方法,因为(在您的情况下),您正在做一些额外的事情(更新缓存)。这对于Foo来说很容易,但是当您想要操纵在一个动作中创建的Foo / Bar程序集之间的关系时会发生什么?

如果创建Foo用它创建了几个Bars,那就是一件事(-awakeFromInsert等),因为你只需要处理更新你的缓存(你可以)顺便说一下,通过键/值来观察变化的上下文。由于创建Foo似乎与现有的Bars(已经在上下文中)建立了关系,因此在尝试与CD的内置撤消支持合作时会遇到困难。

如果您使用内置的Core Data撤消/重做支持,则在这种情况下没有简单的解决方案。在这种情况下,您可以do as this post suggests将其关闭。然后你可以完全自己处理撤销/重做......但是你需要编写很多代码来观察你的对象以改变有趣的属性,为每个属性注册反向动作。

虽然它不是您问题的解决方案,但我希望它至少指出您尝试做的事情的复杂性并为您提供可能的前进道路。如果不了解您的模型(至少在概念层面)以及您的UI如何将其呈现给用户,则很难提供具体的架构建议。

我希望我对这个案子有误 - 也许其他人可以给你一个更好的答案。 : - )

答案 1 :(得分:1)

事实证明,你可以让Foo创建涉及改变与预先存在的Bars和自定义缓存的关系,而NSUndoManager仍然可以处理它 - 但是有一个问题:你必须在每次这样的改变之后保存上下文;否则撤消管理器将停止运行。

由于撤消实际上可以在保存之前回到状态,这不是一件坏事。如果您希望用户能够恢复到选择保存时的状态,那么事情会变得复杂,但是只要用户选择保存,就可以通过制作数据库的副本来处理。

所以在deleteMultiple方法中,在while删除循环之后,我添加了一个保存上下文的调用。

我的方案中还有一个错误,就是我错误地认为NSUndoManager会忽略可转换的属性。很明显,由于可变形的attrs是持久的,因此它们由persistentStoreCoordinator跟踪,因此包含在撤消操作中。因此,当我在删除时无法更新xformable attr数组时,我认为在撤消时我需要恢复信息,我正在破坏动作/反动作对称。

所以在deleteObject:fromSeriesIndex方法中,处理缓存的Series方法,我添加了这段代码,更新了可转换的ObjectID数组:

NSMutableArray *emarrRemoveID = [self.dataObjIDsOrdered mutableCopy];
[emarrRemoveID removeObjectAtIndex:uiIndexForDeletion];
self.dataObjIDsOrdered = emarrRemoveID;
[emarrRemoveID release];

(我假设NSUndoManager会忽略瞬态缓存是正确的。rebuildSeriesCaches中对cleanupFllwgUndoRedo的调用会解决这个问题。)

撤销现在适用于简单对象和SpecialObject-Series关系中的对象。唯一剩下的问题是它需要多个命令-Z才能发生。我将不得不对分组进行更多实验...


编辑:如果正确处理了托管对象的自定义缓存,则无需保存删除后的上下文:

1)撤消后不应重建缓存。只要临时属性包含在托管对象模型中,撤消管理器就会自行处理,即使对于临时缓存也是如此。

2)更改NSMutableArray缓存(emarrObjs)时,单独使用removeObjectAtIndex会使撤消管理器混淆。必须替换整个缓存,与NSArray缓存dataObjIDsOrdered的方式相同。