简体   繁体   English

无法撤消多个操作

[英]Can’t undo more than one operation

When I call undo on the context following deletion of a single object, all works as expected. 删除单个对象后,在上下文中调用undo时,所有操作都按预期工作。 But if user deletes an object, then deletes another object, undo will work only to restore the second object, no matter how many times user requests undo, as though undoLevels were set to 1. This happens whether undoLevels is at the default of 0 (unlimited) or is explicitly set to 6 as a test. 但是如果用户删除了一个对象,然后删除了另一个对象,则无论用户请求撤消多少次,undo都只能恢复第二个对象,就好像undoLevels被设置为1.无论undoLevels是否为默认值0,都会发生这种情况。无限制)或显式设置为6作为测试。

Furthermore, if a single action deletes multiple objects, calling undo afterward has no effect; 此外,如果单个操作删除多个对象,则之后调用undo无效; none of the objects is restored. 没有任何对象被恢复。 I tried explicitly bracketing the deletion loop with begin/endUndoGrouping, to no avail. 我尝试用begin / endUndoGrouping明确包围删除循环,但无济于事。 The undoManager's groupsByEvent is YES (by default), but it makes no difference whether I call a straight undo or undoNestedGroup. undoManager的groupsByEvent是YES(默认情况下),但是我调用直接撤销还是undoNestedGroup没有区别。

Is the context somehow being saved after each operation? 在每次操作后,上下文是否以某种方式被保存? No, because if I quit and relaunch the app after running these tests, all objects are still present in the database. 不,因为如果我在运行这些测试后退出并重新启动应用程序,则所有对象仍然存在于数据库中。

What am I missing? 我错过了什么?


OK, you want code. 好的,你想要代码。 Here's what I imagine is most relevant: 这是我想象的最相关的:

Context getter: 上下文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;
}

Multiple-object deletion method (called by a button on a modal panel): 多对象删除方法(由模态面板上的按钮调用):

/* 
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];
}

Here's the Series method that removes the object from its relationship and ordered cache: 这是从方法关系和有序缓存中删除对象的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.
}

Here's the method, and its follow-up, called by comm-z undo: 这是方法及其后续工作,由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

}

Here's the series method that regenerates its caches following 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;

}

(I've omitted the Locator's regenerateObjCachesFromMuddies because, although I am using its table to view the results of the deletion and undo, I can reload the table with a new fetch, completely regenerating the table's caches, and this test still shows that the undo isn't working.) (我省略了Locator的regenerateObjCachesFromMuddies,因为虽然我使用它的表来查看删除和撤消的结果,但我可以使用新的fetch重新加载表,完全重新生成表的缓存,此测试仍显示撤消不工作。)

As usual, just the task of putting together a SO question helps solve the problem, and I realize now that undo works fine as long as I'm working with simple objects that don't involve the reciprocal SpecialObject-Series relationship. 像往常一样,只是组合一个SO问题的任务有助于解决问题,我现在意识到只要我使用不涉及互惠的SpecialObject-Series关系的简单对象,撤消就可以正常工作。 I'm doing something wrong there... 我在那里做错了......

I think you're getting into a fight with custom undo stuff and Core Data's automagic support. 我认为你正在与自定义撤消内容和Core Data的自动支持进行斗争。

In normal undo/redo code, you have undoable funnel points. 在正常的撤消/重做代码中,您有可撤消的漏斗点。 Usually an undoable add and its inverse undoable remove. 通常是可撤销的添加及其反向可撤消删除。 Calling one registers the other as the inverse action and vice-versa. 调用一个将另一个注册为反向操作,反之亦然。 User undo/redo then just goes back and forth between them. 用户撤消/重做然后只是在它们之间来回。 You separate your "user created a new Foo" code from your "now add this foo to the collection undoably" code (that way "remove Foo" and "add Foo" work independently of supplying a newly-created Foo). 您将“用户创建新Foo”代码与“现在将此foo添加到集合中的可撤销”代码分开(这样“删除Foo”和“添加Foo”工作独立于提供新创建的Foo)。

With Core Data, add and remove means "insert into the context and remove from the context". 使用Core Data,添加和删除意味着“插入上下文并从上下文中删除”。 Also, you still need custom funnel methods because (in your case), you're doing some additional stuff (updating a cache). 此外,您仍然需要自定义漏斗方法,因为(在您的情况下),您正在做一些额外的事情(更新缓存)。 This is easy enough to do with a Foo, but what happens when you want to manipulate the relationship between a Foo/Bar assembly that gets created in one action? 这对于Foo来说很容易,但是当您想要操纵在一个动作中创建的Foo / Bar程序集之间的关系时会发生什么?

If creating a Foo created a few Bars with it, it'd be one thing (-awakeFromInsert and the like) since you'd only have to deal with updating your caching (which you could do, by the way, through key/value observing the context for changes). 如果创建一个Foo用它创建了几个Bars,那就是一件事(-awakeFromInsert等),因为你只需要处理更新你的缓存(顺便说一下,你可以通过键/值来做观察变化的背景)。 Since creating a Foo seems to establish relationships with existing Bars (which are already in the context), you run into a difficult wall when trying to cooperate with CD's built-in undo support. 由于创建Foo似乎与现有的Bars(已经在上下文中)建立了关系,因此在尝试与CD的内置撤消支持合作时会遇到困难。

There is no easy solution in this case if you're using the built-in Core Data undo/redo support. 如果您使用内置的Core Data撤消/重做支持,则在这种情况下没有简单的解决方案 In this case, you can do as this post suggests and turn it off. 在这种情况下,您可以按照此帖建议并将其关闭。 You can then handle undo/redo entirely yourself ... but you'll have a lot of code to write to observe your objects for changes to interesting attributes, registering the inverse action for each. 然后,您可以完全自己处理撤消/重做...但是您需要编写大量代码来观察对象以更改有趣的属性,为每个属性注册反向操作。

While it isn't a solution to your problem, I hope it at least points out the complexity of what you're trying to do and gives you a possible path forward. 虽然它不是您问题的解决方案,但我希望它至少指出您尝试做的事情的复杂性并为您提供可能的前进道路。 Without knowing a LOT more about your model (at the conceptual level at least) and how your UI presents it to the user, it's hard to give specific architectural advice. 如果不了解您的模型(至少在概念层面)以及您的UI如何将其呈现给用户,则很难提供具体的架构建议。

I hope I'm wrong about this case - maybe someone else can give you a better answer. 我希望我对这个案子有误 - 也许其他人可以给你一个更好的答案。 :-) :-)

It turns out that you can have Foo creation that involves changing relationships with pre-existing Bars, and custom caches, and NSUndoManager can still handle it all — but with a kink: You have to save the context after each such change; 事实证明,你可以让Foo创建涉及改变与预先存在的Bars和自定义缓存的关系,并且NSUndoManager仍然可以处理它 - 但是有一个问题:你必须在每次这样的改变之后保存上下文; otherwise the undo manager will cease to function. 否则撤消管理器将停止运行。

Since undo can actually reach back to states before the save, this is not such a bad thing. 由于撤销实际上可以在保存之前回到状态,因此这不是一件坏事。 It does complicate matters if you want the user to be able to revert to the state when they last chose to save, but that can be handled by making a copy of the database whenever the user chooses to save. 如果您希望用户能够恢复到上次选择保存时的状态,那么事情会变得复杂,但是只要用户选择保存,就可以通过制作数据库的副本来处理。

So in the deleteMultiple method, following the while deletion loop, I added a call to save the context. 所以在deleteMultiple方法中,在while删除循环之后,我添加了一个调用来保存上下文。

There's another error in my scheme, which is that I erroneously thought NSUndoManager would ignore transformable attributes. 我的方案中还有一个错误,就是我错误地认为NSUndoManager会忽略可转换的属性。 Well, obviously, since transformable attrs are persisted, they are tracked by the persistentStoreCoordinator and are therefore included in undo operations. 很明显,由于可变形的attrs是持久的,因此它们由persistentStoreCoordinator跟踪,因此包含在撤消操作中。 So when I failed to update the xformable attr array upon deletion, thinking I would need its info for restoration in the event of undo, I was ruining the action/inverse-action symmetry. 因此,当我在删除时无法更新xformable attr数组时,我认为在撤消时我需要其恢复信息,我正在破坏动作/反作用对称性。

So in the deleteObject:fromSeriesIndex method, the Series method that handles the caches, I added this code, updating the transformable ObjectID array: 所以在deleteObject:fromSeriesIndex方法中,处理缓存的Series方法,我添加了这段代码,更新了可转换的ObjectID数组:

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

(My assumption that the NSUndoManager would ignore the transient cache was correct. The call to rebuildSeriesCaches in cleanupFllwgUndoRedo takes care of that.) (我假设NSUndoManager会忽略瞬态缓存是正确的。在cleanupFllwgUndoRedorebuildSeriesCaches的调用会rebuildSeriesCaches这个问题。)

Undo now works, both for simple objects and for objects in SpecialObject-Series relationships. 撤销现在适用于简单对象和SpecialObject-Series关系中的对象。 The only remaining problem is that it takes more than one command-Z to happen. 唯一剩下的问题是它需要多个命令-Z才能发生。 I'll have to experiment more with the groupings… 我将不得不尝试更多的分组......


EDIT: It isn't necessary to save the context post-deletion if the managed object's custom caches are handled correctly: 编辑:如果正确处理托管对象的自定义缓存,则无需保存删除后的上下文:

1) The caches should NOT be rebuilt following undo. 1)撤消后不应重建缓存。 The undo manager will take care of this on its own, even for the transient cache, as long as the transient property is included in the managed object model. 只要临时属性包含在托管对象模型中,撤消管理器就会自行处理,即使对于临时缓存也是如此。

2) When changing the NSMutableArray cache ( emarrObjs ), using removeObjectAtIndex alone will confuse the undo manager. 2)更改NSMutableArray缓存( emarrObjs )时,单独使用removeObjectAtIndex会使撤消管理器混淆。 The entire cache must be replaced, the same way it is with the NSArray cache dataObjIDsOrdered . 必须替换整个缓存,与NSArray缓存dataObjIDsOrdered方式相同。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM