Editing Tasks

Editing Tasks

Your sample application now works pretty well as a read-only application, but it's time to start adding some features to add and change data. You are going to start by adding the ability to edit existing tasks.

Edit Tasks

TaskEditorWindowController

Create a new window and controller (TaskEditorWindowController) and add the appropriate controls. You can see all the code you need to create in the sample code.

//
//  TaskEditorWindowController.m

#import "TaskEditorWindowController.h"
#import "DataAccess.h"

@interface TaskEditorWindowController ()
- (void)populatePriorities;
- (void)bindPopup:(NSPopUpButton *)popup withField:(NSString *)fieldName atRow:(DADataTableRow *)r;
@end

@implementation TaskEditorWindowController

#pragma mark - Properties

- (void)setRow:(DADataTableRow *)value {
    
    _row = value;
    
    //refresh priority popup content and selection at row change
    [self populatePriorities];
    [self bindPopup:self.popupPriority withField:@"Priority" atRow:_row];
    
}

#pragma mark - Actions

-(IBAction)okAction:(id)sender {
    
    // make sure we complete editing
    [self.window makeFirstResponder:nil];
    
    // End sheet with OK as response (will be consumed in Main Window)
    NSWindow *parentWindow = [[NSApplication sharedApplication] mainWindow];
    [parentWindow endSheet:self.window returnCode:NSModalResponseOK];
}

-(IBAction)cancelAction:(id)sender {
    // make sure we complete editing
    [self.window makeFirstResponder:nil];
    
    // End sheet with Cancel as response (will be consumed in Main Window)
    NSWindow *parentWindow = [[NSApplication sharedApplication] mainWindow];
    [parentWindow endSheet:self.window returnCode:NSModalResponseCancel];
}

// this action method will be called each time we change priority popup
- (IBAction)didChangePriority:(id)sender {
    // dispatch change to our model
    NSMenuItem *selection = [self.popupPriority selectedItem];
    self.row[@"Priority"] = [NSNumber numberWithInteger:selection.tag];
}

#pragma mark - Helpers

// populate priorities popup from data table
- (void)populatePriorities {
    
    [self.popupPriority removeAllItems];
    for (DADataTableRow *r in [[[DataAccess sharedInstance] prioritiesTable] rows]) {
        NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:r[@"Name"] action:nil keyEquivalent:@""];
        mi.tag = [r[@"Id"] integerValue];
        [self.popupPriority.menu addItem:mi];
    }
}

// bind popup with edited row field
- (void)bindPopup:(NSPopUpButton *)popup withField:(NSString *)fieldName atRow:(DADataTableRow *)r {
    
    id value = r[fieldName];
    if (value)
        [popup selectItemWithTag:[value integerValue]];
    else
        [popup selectItem:nil];
}

@end

Binding

The controls will bind to the fields in the active DADataTableRow, so add a row property to the .h file for your TaskEditorWindowController.

//TaskEditorWindowController.h
@property (strong, nonatomic) DADataTableRow *row;

Make sure each control is set to bind the File's Owner and its Model Key Path to self.row.fieldName;.

Priorities

To populate the values of the NSPopUpButton (used for selecting a tasks priority) add an IBOutlet to it.

@property (weak) IBOutlet NSPopUpButton *popupPriority;

Populate popupPriority by accessing the prioritiesTable property of the DataAccess object and iterate through its rows.

//TaskEditorWindowController.m
// populate priorities popup from data table
- (void)populatePriorities {
    
    [self.popupPriority removeAllItems];
    for (DADataTableRow *r in [[[DataAccess sharedInstance] prioritiesTable] rows]) {
        NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:r[@"Name"] action:nil keyEquivalent:@""];
        mi.tag = [r[@"Id"] integerValue];
        [self.popupPriority.menu addItem:mi];
    }
}

Also add an IBAction method didChangePriority: to the TaskEditorWindowController to change the value of the Priority field in the row when it changes.

You change the data for any field in the row simply by giving it a new value of the appropriate type.

//TaskEditorWindowController.m
// this action method will be called each time we change priority popup
- (IBAction)didChangePriority:(id)sender {
    // dispatch change to our model
    NSMenuItem *selection = [self.popupPriority selectedItem];
    self.row[@"Priority"] = [NSNumber numberWithInteger:selection.tag];
}

Don't forget to import DataAccess.h.

You will also need to make sure the NSPopUpButton gets bound to the current row’s Priority value each time a row is set.

//TaskEditorWindowController.m

- (void)bindPopup:(NSPopUpButton *)popup 
        withField:(NSString *)fieldName 
            atRow:(DADataTableRow *)r {
 
    id value = r[fieldName];
    if (value)
        [popup selectItemWithTag:[value integerValue]];
    else
        [popup selectItem:nil];
}

- (void)setRow:(DADataTableRow *)value {
    _row = value;
    //refresh priority popup content and selection at row change
    [self populatePriorities];
    [self bindPopup:self.popupPriority withField:@"Priority" atRow:_row];
}

The AppDelegate

Back in AppDelegate.m, you can now add code to launch the editing of tasks.

Create a TaskEditorWindowController

Add code to AppDelegate.m to create a new instance of the new TaskEditorWindowController when needed.

//AppDelegate.m
#import "TaskEditorWindowController.h"
 
@interface AppDelegate ()
@property (strong, nonatomic) TaskEditorWindowController *taskEditorWindowController;
@end
//AppDelegate.m
- (TaskEditorWindowController *)taskEditorWindowController {

if (_taskEditorWindowController == nil) {
    _taskEditorWindowController =  [[TaskEditorWindowController alloc] 
                                    initWithWindowNibName:@"TaskEditorWindowController"];
}
return _taskEditorWindowController;
}

Editing and Saving

Add another button to the toolbar on the main form, call it Edit and give it an IBAction method to run when clicked called editTaskAction.

- (IBAction)editTaskAction:(id)sender {
    id task = [[self.tableController selectedObjects] lastObject];
 
    [self editTask:task];
}

You get the currently selected tasks from the DADataTableController and pass it to the editTask method, which you will now need to implement.

- (void)editTask:(DADataTableRow *)task {
    NSWindow *editorWindow = self.taskEditorWindowController.window;
    self.taskEditorWindowController.row = task;
    [task edit];
    [self.window beginSheet:editorWindow completionHandler:^(NSModalResponse returnCode) {
        if (returnCode == NSModalResponseOK) {
            [task post];
            NSLog(@"Ok");
            [[DataAccess sharedInstance] beginApplyUpdates];
        } else {
            [task discard];
            NSLog(@"Not Ok :)");
        }
        [self.tableController rearrangeObjects];
    }];
}

There are quite few important concepts in this short bit of code you are going to need to understand. First, notice that the method calls [task edit].

The task object is a DADataTableRow object. Calling edit on the row puts it into edit mode, which will allow the data within it to be changed.

You will hopefully notice that elsewhere in the method you either call [task discard], which will take the DADataTableRow out of edit mode and discard all the changes that were made while in edit mode, or you call [task post], which takes the DADataTableRow out of edit mode but preserves all the changes that were made since it was put into edit mode.

[task post] only preserves the changes in the local client data, which is why you have to call [[DataAccess sharedInstance] beginApplyUpdates]; next, which starts of the process of applying the updates you just made back to the main data via Relativity Server.

beginApplyUpdates

The beginApplyUpdates method is one of the stub methods in the DataAccess class that you are required to provide some implementation for. Take a look at what was created by the project template.

The commented code is there as an example of what you need to implement. Let's step through the code, and you can change what needs changing when you get there.

The code first checks to see if the DataAccess object is already applying updates in the background by checking the object’s busy property. If it is busy, it just returns nil and exits.

    if (busy) return nil;

Next you get to the part of the code you are going to need to change. The code is checking to see if any of the tables controlled by the DataAccess object were changed.

- (DAAsyncRequest *)beginApplyUpdates
    NSMutableArray *a = [NSMutableArray array];

    // Add any tables named myFirstTable and mySecondTable 
    //that might have updates to this array.
    /*
     if ([myFirstTable hasChanges]) [a addObject:myFirstTable];
     if ([mySecondTable hasChanges]) [a addObject:mySecondTable];
     */
}

You will see that the example code calls the hasChanges method on the table.

hasChanges will return true if any row in that table has been changed. A row is considered changed if edit has been called on it, its data has been updated and then post was called.

If the table has changes, it is added to an array of tables.

You need to change this code to check if the Tasks table has had any changes, and if it has, it has to be added to the array.

if ([self.tasksTable hasChanges]) {
        [a addObject:self.tasksTable];
}

Once any changed tables have been added to the array, the method calls [rda beginApplyChangesForTables:a start:NO];, passing in the array of tables, which will create a new DAAsyncRequest request object.

    if ([a count] > 0)
    {
        DAAsyncRequest *ar = [rda beginApplyChangesForTables:a start:NO];
        [ar setDelegate:self];
        [ar setContext:@"AppyUpdates"];

        [self setBusy:YES];
        [ar start];
        return ar;
    }
}

The DAAsyncRequest will handle applying the updates back to Relativity Server in an asynchronous manner. The code has asked it not to start when created by setting the start: parameter of the beginApplyChangesForTables: start: call to NO.

This is done so that it gets a chance to set the delegate property on the DAAsyncRequest to point to self (the DataAccess object). The code also gives the request a context, so it can later be identified.

The DataAccess object implements the DAAsyncRequestDelegate protocol that the request will call when it finishes or fails.

On success or failure, the DataAccess class implementations of the DAAsyncRequestDelegate protocol both reset the busy property and call triggerDataReady, which will fire the NOTIFICATION_DATA_READY notification that will refresh the UI data.

If the update failed, the DataAccess objects delegate method alertError: message:; will also get called (implemented in AppDelegate.m).

With all that done, the busy flag is set on the DataAccess object , the request is started using [ar start] and is then returned from the method.

Your final code for beginApplyUpdates should look like this:

- (DAAsyncRequest *)beginApplyUpdates {
    if (busy)
        return nil;

    NSMutableArray *a = [NSMutableArray array];

    if ([self.tasksTable hasChanges])
        [a addObject:self.tasksTable];

    if ([a count] > 0) {
        DAAsyncRequest *ar = [rda beginApplyChangesForTables:a start:NO];
        [ar setDelegate:self];
        [ar setContext:@"AppyUpdates"];

        [self setBusy:YES];
        [ar start];
        return ar;
    }
    return nil;
}

With that done, your sample app should now be able to edit tasks.