views:

80

answers:

2

Hello,

I have an NSOutlineView with checkboxes. I have the checkbox state bound to a node item with the key shouldBeCopied. In the node item I have the getters and setters like so:

-(BOOL)shouldBeCopied {
    if([[self parent] shouldBeCopied])
        return YES;
    return shouldBeCopied;
}

-(void)setShouldBeCopied:(BOOL)value {
    shouldBeCopied = value; 
    if(value && [[self children] count] > 0)
        [[self delegate] reloadData];
}

The idea here is that if the parent is checked, so should the children. The problem I'm having is that when I check the parent, it does not update that view of the children if they are already expanded. I can understand that it should not be updated by the bindings because i'm not actually changing the value. But should reloadData not cause the bindings to re-get the value, thus calling -shouldBeCopied for the children? I have tried a few other things such as -setNeedsDisplay and -reloadItem:nil reloadChildren:YES but none work. I've noticed that the display gets refreshed when I swap to xcode and then back again and that's all I want, so how do I get it to behave that way?

+1  A: 

Your setter does not send -willChangeValueForKey: and -didChangeValueForKey: before and after the change, therefore the changes won't be "noticed" by the bindings mechanism.

Also, telling a view anything directly from a model object is ... not a good approach. In this case, since you're using Bindings, your tree controller should note the change (once you fix your setter to send the proper notifications) and update the outline view.

Joshua Nozzi
OK, well adding the -will/didChangeValueForKey calls didn't change anything (The view was updated for the checkbox clicked with the original code), but I guess that was cocoa compensating for the lack of them before. I'm aware that a controller should be dealing with the view, but when looking at how to detect a checkbox changing, all I could see in the notifications for an outline view were ones that dealt with expansion and select changes, nothing about the actual content being altered. So how should I be detecting this in the controller? Detecting clicks in the view seems over the top.
Septih
You need to describe you design in a lot more detail for anybody to make more concrete suggestions. There simply isn't enough information to give you anything better. Tell us exactly how and to what things are bound, their keypaths (so we have a clear picture), etc.
Joshua Nozzi
Setters do not need to send KVO notifications for `self` explicitly, unless you've explicitly turned automatic notifications off.
Peter Hosey
+1  A: 

I have an NSOutlineView with checkboxes. I have the checkbox state bound to a node item with the key shouldBeCopied.

Are you binding the column or the cell? You should bind the column.

-(BOOL)shouldBeCopied {
    if([[self parent] shouldBeCopied])
        return YES;
    return shouldBeCopied;
}

Would it not be better to use the child's value first? If nothing else, it's cheaper to retrieve (no message required).

The amended getter would then look like this:

- (BOOL) shouldBeCopied {
     return (shouldBeCopied || [[self parent] shouldBeCopied]);
}

The problem I'm having is that when I check the parent, it does not update that view of the children if they are already expanded.

There are two solutions. One is cleaner and will work on 10.5 and later. The other is slightly dirty and will work on any version of Mac OS X.

The dirty solution is to have the parent, from the setter method, post KVO notifications on behalf of all of its children. Something like:

[children performSelector:@selector(willChangeValueForKey:) withObject:@"shouldBeCopied"];
//Actually change the value here.
[children performSelector:@selector(didChangeValueForKey:) withObject:@"shouldBeCopied"];

This is dirty because it has one object posting KVO notifications about a property of another object. Each object should only claim to know the values of its own properties; an object that claims to know the values of another object's properties risks being wrong, leading to wrong and/or inefficient behavior, not to mention a propensity for the code to induce headache.

The cleaner solution is to have each object observe this property of its parent. Pass the NSKeyValueObservingOptionPrior option when adding yourself as an observer in order to get a notification before the change (which you'll respond to, in the observation method, by sending [self willChangeValueForKey:]) and a notification after the change (which you'll respond to, in the observation method, by sending [self didChangeValueForKey:]).

With the clean solution, each object only sends KVO notifications about its own property changing; no object is posting notifications about other objects' properties.

You might be tempted to have the child not send itself these KVO notifications when its own value for the property is YES, because in that case the parent's value doesn't matter. You shouldn't, though, as this would only work for children; if a farther-down descendant has the property set to NO, then that object's ancestors' values matter after all, and that object will only get the notification if every one of its ancestors posts it.

Peter Hosey
That's done the trick perfectly. Thank you. I've gone for the cleaner option since I'm already using 10.5 components.
Septih