Cocoa, Core Data, and me (VII)

Time to figure out how to make a certain textfield gain focus. Remember, when doubleclicking the countries table, we want the drawer to slide open and the first field in that drawer to get keyboard focus automatically. We already got the drawer to open, so now is the time to get that focus thing working.

It turns out that in Cocoa, each window maintains a pointer to the view that has keyboard focus. This is so, even if that view is part of another view, so the pointer can go “past” several levels of views. Views don’t have any such pointer.

Checking out our app, it turns out that the “Countries” pane with the browser table in it is an NSPanel, which is an NSWindow and it is controlled by our CountriesController object, which is derived from an NSWindowsController, that in turn contains a pointer to our window. The drawer itself (DetailsView) is an NSView, so it does not contain a pointer to the field with focus (or “first responder” as the cocoa docs define it).

The docs for NSWindow gives us the makeFirstResponder method. The docs for NSWindowController gives us the “window” method. Together, this let’s me guess that a good way of making an arbitrary field a first responder is with the following call:

The focus call.

The assumption here is that we call it from inside the CountriesController, that’s why it’s a call to “self”, and that the parameter is an object derived from NSResponder, which all views are (I think).

The next problem is how to get the pointer to that “arbitrary field”, and let’s use outlets for that once again. They’re neat.

Add another outlet to the CountriesController.h header file:

The second outlet

Save the header file, drag and drop it on the countries.nib pane in Interface Builder (no, I’m not going to show it yet again, there are several screen shots of this already in this series). Click “merge” as I do, unless you have a reason not to.

Now, control-drag from the “File’s Owner” (which is a reference to our CountriesController) to the actual field you want:

Control-drag in the nib

In the inspector pane, doubleclick the “firstDrawerField” line to connect it:

Connect the outlet.

Finally, add a line to the implementation in the CountriesController.m file:

Add line to implementation.

Save, build, run, and be amazed. It works. When you doubleclick a country in the table, the drawer opens and the first field gets the focus. Yay!

Now it’s time to add save and cancel buttons to the drawer. Open the countries.nib file in Interface Builder and add two neat buttons to the DetailsView pane:

Adding save and cancel buttons.

These buttons need to trigger actions in the CountriesController, so we add methods for that in the CountriesController.h file:

CountriesController.h file

We now do the familiar drag-and-drop of that header file from XCode to the Countries.nib pane in Interface Builder. After doing that, we can control-drag from the buttons we added to the actions we just created:

Controldrag from cancel button

Set the action to the “detailsViewCancelButton” by doubleclicking that row in the inpector or by clicking the “Connect” button:

Selecting the action in the inspector.

Repeat that process for the “Save” button and the “detailsViewSaveButton” action.

Undoing stuff

Now we come to a more difficult part. Naturally, I want the “Cancel” button to close the drawer without actually applying any changes to the entity I’m editing, in this case “country”. Core Data is built in such a way that it directly updates the entity in memory, which we can see when we edit a field in the drawer and the corresponding column in the browser table changes immediatly we tab away from the field in the drawer. This doesn’t yet change the contents in the database, however.

I see three ways of handling this:

  1. Save the entity to the database before the edit, then either save or rollback the changes depending on which button the user clicks.
  2. Maintain the variables before and after changes in code in the entity managed object, then copy the old values back if the user clicks “cancel”
  3. Use the built-in undo manager

None of the above alternatives are perfect, and this is the reason why:

Save and rollback

If we have nested edits, this will fail. By a “nested edit” I mean, for example, that you create an invoice, and while doing so you create a new customer. If you then “save” after creating the customer, you’ll at the same time save the invoice that is in preparation and incomplete. Or any variations on that theme. Not good.

Maintain the variables in code

This ought to be the most complete handling of the problem, but at the same time it circumvents the built-in functionality in core data and uses too much code. I don’t like it.

Use the built-in undo manager

This is my preferred solution. But it isn’t without problems. For instance, if I do a nested undo, with one group being the invoice and one being the customer, what happens if I save the customer, but undo the entire invoice? Actually, I don’t know. I’d like the customer to not be undone, but one can argue that it should be. So, we’ll just wait and see what these more complex sequences will bring.

It seems that the undo manager that is part of the object manager context is intricately used inside the managed objects. I added a boatload of tracing to the methods to find a way to make it work right, and the following seems to cover it, at least experimentally. At the start of the edit, you need to:

  • make the first edit field the first responder
  • make the object context process any pending changes (no, this does not mean saving to disk)
  • start a new undo grouping
  • disable “group by event” in the undo manager

Note that the order of these (and the following) operations is critical.

If you save changes, do the following:

  • make the clicked button the first responder (this is just a trick to complete the last edited field, if the user didn’t tab out of it before he clicked the button)
  • tell the managed object context to process pending changes
  • tell the undo manager to end the undo grouping
  • tell the undo manager to again group by event

Note that you don’t save to disk in this operation.

If you instead cancelled the edits, this is what has to be done:

  • make the clicked button the first responder
  • tell the managed object context to process any pending changes
  • tell the undo manager to end the grouping
  • tell the undo manager to undo the last nested group
  • tell the undo manager to enable groups by event again

Let’s look at some code

I added a litte function to trace what happens during my experiments with the undo manager:

dispLog function

Then I called this function like crazy during the methods, like in the following example:

cancelButton method with tracing

I’m not going to bore you with the instrumented version of the methods, so this is how everything looks after removing the tracing code:

When a row is doubleclicked in the browser, this method is called:

the clickAction method

Then, when you click “save”, this is what is called:

the saveButton method

Finally, when you click “cancel” this method gets called:

cancelButton method

Experimenting with the app shows that the save and undo seems to work as intended, with the glaring exception of the radiobuttons. If I cancel, they don’t revert. Owell, that has to be the topic of another chapter in this saga.

2 thoughts on “Cocoa, Core Data, and me (VII)”

  1. I stumbled across your website whilst looking for sample code in terms of programming a database in cocoa. I have not seen much.

    I am looking for something on the line of ADO.NET or Borland db adapter using COCA ie wrappers that can allow me to quickly put together a database application with any backend.

    If you have any input I would appreciate it. Thanks

  2. This miniseries stopped in midstride because of two things: I realized I’d taken a wrong turn with doing record editing this way, so I have to back down a little. The other reason was that I’m into a programming job for .NET now so I sadly had to let this project rest. When I pick up this again, the problem you pose is one I will have to solve as well. But I have no idea when I get around to doing that.

Comments are closed.