0

当我在删除单个对象后对上下文调用 undo 时,一切都按预期工作。但是如果用户删除了一个对象,然后又删除了另一个对象,那么无论用户请求撤消多少次,撤消都只会恢复第二个对象,就好像 undoLevels 设置为 1。无论 undoLevels 是否默认为 0(无限制)或明确设置为 6 作为测试。

此外,如果单个操作删除了多个对象,那么之后调用 undo 是没有效果的;没有对象被恢复。我尝试用 begin/endUndoGrouping 明确地将删除循环括起来,但无济于事。undoManager 的 groupsByEvent 为 YES(默认情况下),但无论我调用直接 undo 还是 undoNestedGroup 都没有区别。

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

我错过了什么?


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

上下文获取器:

- (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

}

这是在撤消/唤醒后重新生成其缓存的系列方法:

- (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 关系的简单对象,撤消就可以正常工作。我在那里做错了什么......

4

2 回答 2

1

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

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

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

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

如果您使用内置的 Core Data 撤消/重做支持,在这种情况下没有简单的解决方案。在这种情况下,您可以按照这篇文章的建议进行操作并将其关闭。然后,您可以完全自己处理撤消/重做……但是您将需要编写大量代码来观察对象是否更改了有趣的属性,并为每个对象注册相反的操作。

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

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

于 2011-10-20T20:18:16.600 回答
1

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

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

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

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

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

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

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

撤消现在适用于简单对象和特殊对象系列关系中的对象。唯一剩下的问题是它需要多个命令-Z 才能发生。我将不得不对分组进行更多试验……</p>


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

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

2)当改变 NSMutableArray 缓存(emarrObjs)时,单独使用 removeObjectAtIndex 会混淆 undo manager。必须替换整个缓存,就像 NSArray缓存一样dataObjIDsOrdered

于 2011-10-22T14:31:34.603 回答