Search code examples
cocoacocoa-bindingskey-value-observing

Add editing behaviors to bound NSFormCell


I've a Core Data model class with a customer ID attribute. It's bound to a form cell. When the user finishes editing the text I want a chance to convert their entry to upper case, using logic which depends on the old and new values.

Ideally I want to keep the behavior close to the view where it belongs, using an object I can instantiate in the nib and hook up to the text cells. But I'd settle for an object I had to hook up to the model.

I've implemented this three different ways:

  1. Custom setter method in the model class
  2. Text editing delegate implementing NSControlTextEditingDelegate
  3. Helper class which uses KVO to notice the change and initiate a subsequent change

All three implementations have problems. The issues, respectively:

  1. This behavior doesn't belong in the model. I should be able to set the attribute in code, for example, without triggering it.
  2. I can't get the "before" value because the form cell doesn't provide controlTextDidBeginEditing: calls (and the old value is gone by the time controlTextDidEndEditing: is called). Furthermore tabbing in and out of the field without typing anything triggers a call to controlTextDidEndEditing:.
  3. When the observation fires for the user's change, and I initiate a subsequent change to that property, the view ignores the change notification and doesn't redraw. (I presume the binder does this for efficiency. Normally when updating the model, it can ignore the KVO observations from the field being updated.)

How would you solve this problem?


Solution

  • After some discussion here, it sounds like some possible ways to do tho:

    1. Put a category on the model class and override validateMyKey
    2. Subclassing NSFormCell

    I tried both. More issues:

    1. validateMyKey isn't called until after the model updates itself, so the old value isn't available.
    2. editWithFrame:inView:editor:delegate:event: isn't always called upon entering a field, so it's difficult to access the old value in endEditing:.

    New solution is a refinement on my original #2: text editing delegate implementing NSControlTextEditingDelegate.

    Instead of controlTextDidBeginEditing: and controlTextDidEndEditing:, implement only control:textShouldEndEditing:. In that method, manipulate the text if necessary, then return YES.

    I instantiate this in the nib and make it the form's delegate (not the cells'). In the code below I get the old value using infoForBinding: but if you aren't using bindings, you could add an outlet to the model object instead.

    -(BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor {
        NSCell *cell = [(NSForm *)control selectedCell];
        NSString *identifier = [(NSCell *)[(NSForm *)control selectedCell] identifier];
        if (!identifier) return YES;
    
        NSDictionary *bindingInfo = [cell infoForBinding:@"value"];
        if (!bindingInfo) return YES;
        NSString *oldValue = [[bindingInfo valueForKey:NSObservedObjectKey] valueForKeyPath:[bindingInfo valueForKey:NSObservedKeyPathKey]];
    
        NSString *newValue = cell.stringValue;
    
        if ([identifier isEqualTo:@"firstField"]) {
            if (criteria)
                cell.stringValue = ....;
        
        } else if ([identifier isEqualTo:@"secondField"]) {
            if (criteria)
                cell.stringValue = ....;
        }
    
        return YES;
    }