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