Не могу отменить более одной операции - PullRequest
0 голосов
/ 20 октября 2011

Когда я вызываю отмену в контексте после удаления одного объекта, все работает как положено. Но если пользователь удаляет объект, а затем удаляет другой объект, отмена будет работать только для восстановления второго объекта, независимо от того, сколько раз пользователь запрашивает отмену, как если бы для undoLevels было установлено значение 1. Это происходит, если для undoLevels установлено значение по умолчанию 0 ( неограниченно) или явно задано значение 6 в качестве теста.

Кроме того, если одно действие удаляет несколько объектов, последующий вызов отмены не имеет никакого эффекта; ни один из объектов не восстановлен. Я попытался явно заключить в скобки цикл удаления с begin / endUndoGrouping, но безрезультатно. GroupsByEvent для undoManager имеет значение YES (по умолчанию), но не имеет значения, вызываю ли я прямую отмену или 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;

}

(я не указывал в регенераторе LocatorObjCachesFromMuddies, потому что, хотя я использую его таблицу для просмотра результатов удаления и отмены, я могу перезагрузить таблицу с новой выборкой, полностью восстановив кеши таблицы, и этот тест по-прежнему показывает что отмена не работает.)

Как обычно, только задача составления SO-вопроса помогает решить проблему, и теперь я понимаю, что отмена работает нормально, если я работаю с простыми объектами, которые не связаны с взаимными отношениями SpecialObject-Series. Я там что-то не так делаю ...

Ответы [ 2 ]

1 голос
/ 22 октября 2011

Оказывается, что у вас может быть создание Foo, которое включает изменение отношений с ранее существующими столбцами и пользовательскими кэшами, и 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 игнорировал бы переходный кэш , было правильным. Об этом позаботился вызов rebuildSeriesCaches в cleanupFllwgUndoRedo.)

Отмена теперь работает, как для простых объектов, так и для объектов в отношениях SpecialObject-Series. Единственная оставшаяся проблема состоит в том, что для выполнения требуется более одной команды -Z. Мне придется больше экспериментировать с группировками ...


РЕДАКТИРОВАТЬ: Нет необходимости сохранять контекст после удаления, если пользовательские кеши управляемого объекта обрабатываются правильно:

1) Кэши НЕ должны быть восстановлены после отмены. Менеджер отмены позаботится об этом самостоятельно, даже для временного кэша, если временное свойство включено в модель управляемого объекта.

2) При изменении кэша NSMutableArray (emarrObjs) использование одного только removeObjectAtIndex приведет к путанице в диспетчере отмены. Кэш весь должен быть заменен так же, как и кэш NSArray dataObjIDsOrdered.

1 голос
/ 21 октября 2011

Я думаю, вы вступаете в борьбу с пользовательскими отменами и автоматической поддержкой Core Data.

В обычном коде отмены / возврата у вас есть точки отмены, которые нельзя отменить. Обычно отменяемое добавление и обратное отменяемое удаление. Вызов одного регистрирует другого как обратное действие и наоборот. Пользователь отменяет / повторяет, затем просто переходит между ними. Вы отделяете свой код «пользователь создал новый Foo» от своего кода «теперь добавьте этот foo в коллекцию недопустимо» (таким образом «удалить Foo» и «добавить Foo» работают независимо от предоставления вновь созданного Foo).

В Core Data добавить и удалить означает «вставить в контекст и удалить из контекста». Кроме того, вам все еще нужны пользовательские методы последовательности, потому что (в вашем случае) вы делаете некоторые дополнительные вещи (обновление кеша). Это достаточно просто сделать с Foo, но что произойдет, если вы захотите манипулировать отношениями между сборкой Foo / Bar, которая создается одним действием?

Если бы при создании Foo было создано несколько баров, это было бы одно (-awakeFromInsert и т. П.), Поскольку вам нужно было бы иметь дело только с обновлением кэширования (что вы могли бы сделать Кстати, через ключ / значение соблюдая контекст изменений). Поскольку создание Foo, по-видимому, устанавливает отношения с существующими панелями (которые уже находятся в контексте), вы сталкиваетесь с трудной стеной, пытаясь сотрудничать со встроенной поддержкой отмены CD.

Нет простого решения в этом случае, если вы используете встроенную поддержку отмены / возврата Core Data. В этом случае вы можете сделать, как предлагает этот пост и отключить его. Затем вы можете обрабатывать отмену / возврат полностью самостоятельно ... но вам нужно будет написать много кода, чтобы наблюдать за вашими объектами на предмет изменений в интересных атрибутах, регистрируя обратное действие для каждого.

Хотя это не решение вашей проблемы, я надеюсь, что оно хотя бы укажет на сложность того, что вы пытаетесь сделать, и даст вам возможный путь вперед. Не зная много о вашей модели (по крайней мере, на концептуальном уровне) и о том, как ваш пользовательский интерфейс представляет ее пользователю, трудно дать конкретный архитектурный совет.

Надеюсь, я ошибаюсь в этом деле - может быть, кто-то другой может дать вам лучший ответ. : -)

...