Xamarin.Mac NSUndoManager with NSTableView: undoing/redoing adding and removing items

Working through an example in the Big Nerd Ranch Cocoa Programming Guide of an editable cell-based NSTableView that is bound to a NSArrayController in the .xib file, I ran into a snag implementing the undo/redo functionality to undo/redo adding and removing items from the NSTableView. The NSArrayController is bound to the Employees property  which is a NSMutableArray. Here is the Objective-C code I was working from:

- (void)insertObject:(Person *)p inEmployeesAtIndex:(NSUInteger)index {
    NSLog(@"adding %@ to %@", p, employees);
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    // The issue is with the following method call. 
    // This does not work in Xamarin.Mac. Bug report:https://bugzilla.xamarin.com/show_bug.cgi?id=19623
    [[undo prepareWithInvocationTarget:self]
	 removeObjectFromEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Add Person"];
    }
    // Add the Person to the array
    [employees insertObject:p atIndex:index];
}
- (void)removeObjectFromEmployeesAtIndex:(NSUInteger)index
{
    Person *p = [employees objectAtIndex:index];
    NSLog(@"removing %@ from %@", p, employees);
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    // The issue is with the following method call. 
    // This does not work in Xamarin.Mac. Bug report:https://bugzilla.xamarin.com/show_bug.cgi?id=19623
    [[undo prepareWithInvocationTarget:self] insertObject:p
                                       inEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Remove Person"];
    }
    [employees removeObjectAtIndex:index];
}

The issue with the above in Xamarin.Mac is that this call:

undo.PrepareWithInvocationTarget(this);

throws an exception in Xamarin.Mac due to the above linked bug report. I also do not see a way to then pass the needed selector and arguments to the NSUndoManager even if the method were not causing an exception. So the suggestion I found online at the Xamarin Forums said to use:

undo.RegisterUndoWithTarget(this, new Selector("removeObjectFromEmployeesAtIndex:"), new NSObject());

//and

undo.RegisterUndoWithTarget(this, new Selector("insertObject:inEmployeesAtIndex:"), new NSObject());

The problem here is that removeObjectFromEmployeesAtIndex: and insertObject:inEmployeesAtIndex:, which are called by the NSArrayController when an item is being added or removed, only takes an int (or uint) as an argument. As such I could find no way to pass these Objective-C messages to the NSUndoManager with the appropriate argument type. To work around this, I passed in a different selector with an NSArray that held the Person object and the index as an NSNumber. Then in my UndoAdd(NSObject o) and UndoRemove(NSObject o) methods, I extract the Person object and the index int and forward those to the NSArrayController so that the items can be added and removed. Here is the relevant code:

[Export("insertObject:inEmployeesAtIndex:")]
public void InsertObjectInEmployeesAtIndex(Person p, int index)
{
     NSUndoManager undo = this.UndoManager;

     // Add the inverse of this operation to the undo stack
     NSArray args = NSArray.FromObjects(new object[]{p, new NSNumber(index)});
     undo.RegisterUndoWithTarget(this, new Selector("undoAdd:"), args);
     if (!undo.IsUndoing) {
          undo.SetActionname("Add Person");
     }
     // Add the person to the array
     Employees.Insert(p, index);
}

[Export("removeObjectFromEmployeesAtIndex:")]
public void RemoveObjectFromEmployeesAtIndex(int index)
{
     NSUndoManager undo = this.UndoManager;
     Person p = Employees.GetItem<Person>(index);

     // Add the inverse of this operation to the undo stack
     NSArray args = NSArray.FromObjects(new object[]{p, new NSNumber(index)});
     undo.RegisterUndoWithTarget(this, new Selector("undoRemove:"), args);
     if (!undo.IsUndoing) {
          undo.SetActionname("Remove Person");
     }
     // Remove the person from the array
     Employees.RemoveObject(index);
}

[Export("undoAdd:")]
public void UndoAdd(NSObject o)
{
     Person p = ((NSArray)o).GetItem<Person>(0);

     // Tell the array controller to remove the specific person object, 
     // not the 'object at index' with removeAt(i.ToInt32); 
     // as the index can change with a sort
     arrayController.RemoveObject(p);
}

[Export("undoRemove:")]
public void UndoRemove(NSObject o)
{
     Person p = ((NSArray)o).GetItem<Person>(0);
     NSNumber i = ((NSArray)o).GetItem<NSNumber>(1);

     // Tell the arrayController to insert the person
     arrayController.Insert(p, i.Int32Value);
}

You can download and look over the full project here: RaiseMan

There is still an issue as to where an item is re-added when using undo or redo to undo/redo removing an item. It will not necessarily get added in the same position it was if sort was used between the initial action and the undo/redo command. When using the Objective-C code at the start of this blog entry, when an item is re-added, it either goes back to its original position (if unsorted) or goes to its sorted position. Not sure how to get that to happen yet. I tried using arrayController.RearrangeObjects(); whenever an item is inserted, but this caused odd behavior when undoing or redoing. The RearrangeObjects(); method created an extra entry in the arrayController, i.e. it is displayed in the NSTableView, but this entry does not exist in the underlying NSMutableArray. Very odd.

UPDATE: I resolved the above issue. Calling arrayController.RearrangeObjects() from the UndoRemove() method instead of the InsertObjectInEmployeesAtIndex() method resolved the issue. Linked project has been updated with the changes.

I hope this helps someone!

Leave a Reply

Your email address will not be published. Required fields are marked *