views:

39

answers:

2

Hi,

Let's say you have a custom control similar to NSSlider but with support for choosing a range of values, instead of a single value. The obvious choice of properties is minValue, maxValue, leftValue, rightValue, or however you wanna name them.
You'd probably also want to make sure that leftValue and rightValue always lay in between minValue and maxValue. Same for minValue and maxValue. You don't want them to potentially invalidate the existing leftValue and rightValue, like by—let's say—setting maxValue to a value lower than the current rightValue.

Pretty straight forward so far.

What however do you do in case of ensuring proper KVO accessibility/compliency? You simply can't assure that KVO sets the properties in the proper order (that being first setting the min and max limits and then the other values).

I happen to have such an UI element and simply can't figure out hot to get this running without opening the doors for falsely set values.

The custom control is meant to be bound to a model object. If I now create such a model with the values (min: 0.00 max: 42.00 left: 14.00 right: 28.00) I end up with the following setup in the custom control (min: 0.00 max: 42.00 left: 1.00 right: 1.00)

The reason for this is that instead of calling minValue and maxValue first, it calls leftValue and rightValue first, which causes both values to end up with 1.0 (which is the default maxValue). (After which the maxValue is then set to its proper value.)

//[self prepare] gets called by both, [self initWithCoder:] and [self initWithFrame:]
- (void)prepare
{
    minValue = 0.0;
    maxValue = 1.0;
    leftValue = 0.0;
    rightValue = 1.0;       
}

- (void)setMinValue:(double)aMinValue
{
    if (aMinValue < maxValue && aMinValue < leftValue) {
        minValue = aMinValue;
        [self propagateValue:[NSNumber numberWithDouble:minValue] forBinding:@"minValue"];
        [self setNeedsDisplay:YES];
    }   
}

- (void)setMaxValue:(double)aMaxValue
{
    if (aMaxValue > minValue && aMaxValue > rightValue) {
        maxValue = aMaxValue;
        [self propagateValue:[NSNumber numberWithDouble:maxValue] forBinding:@"maxValue"];
        [self setNeedsDisplay:YES];
    }
}

- (void)setLeftValue:(double)aLeftValue
{
    double newValue = leftValue;
    if (aLeftValue < minValue) {
        newValue = minValue;
    } else if (aLeftValue > rightValue) {
        newValue = rightValue;
    } else {
        newValue = aLeftValue;
    }
    if (newValue != leftValue) {
        leftValue = newValue;
        [self propagateValue:[NSNumber numberWithDouble:leftValue] forBinding:@"leftValue"];
        [self setNeedsDisplay:YES];
    }
}

- (void)setRightValue:(double)aRightValue
{
    double newValue = leftValue;
    if (aRightValue > maxValue) {
        newValue = maxValue;
    } else if (aRightValue < leftValue) {
        newValue = leftValue;
    } else {
        newValue = aRightValue;
    }
    if (newValue != rightValue) {
        rightValue = newValue;
        [self propagateValue:[NSNumber numberWithDouble:rightValue] forBinding:@"rightValue"];
        [self setNeedsDisplay:YES];
    }

It's times like these when you wish Apple had opened up the source of Cocoa. Or at least some of the controls for deeper inspection.

How would you have implemented such a control?

Or more general: What's the best practice for implementing a class that does have cross-dependent properties while complying to KVO?

A: 

Not exactly sure I understand what your trying to do but your problem has nothing to do with KVO.

The first time that setLeftValue: is called after prepare to set it to 14.0, you see the following with values of variables in parethesis:

- (void)setLeftValue:(double)aLeftValue(14.0)
{
    double newValue = leftValue(0.0);
    if (aLeftValue(14.0) < minValue(0.0)) {
        newValue = minValue(0.0);
    } else if (aLeftValue(14.0) > rightValue(1.0)) { // always evaluates true for all aleftValue>1
        newValue = rightValue(1.0); // now newValue==1.0
    } else {
        newValue = aLeftValue(14.0);
    }
    if (newValue(1.0) != leftValue(0.0)) {
        leftValue = newValue(1.0); // now leftvalue==1.0
        [self propagateValue:[NSNumber numberWithDouble:leftValue] forBinding:@"leftValue"];
        [self setNeedsDisplay:YES];
    }
}

For all aLeftvalues>1, this code will always set leftValue==1.0.

Since you never call either of the minValue and maxValue setters, they have no effect on this code.

TechZen
+1  A: 

Wow. This is a tricky problem. As you've already identified, there's nothing you can do to ensure that KVO messages are sent in any particular order.

Here's one thought: in your setLeftValue: and setRightValue: accessors, instead of trimming the value, why not just store the value that's passed in, and then trim the value on access.

For example:

- (void)setLeftValue:(double)value {
    leftValue = value;
    [self propagateValue:[NSNumber numberWithDouble:self.leftValue] forBinding:@"leftValue"]; // Note the use of the accessor here
    [self setNeedsDisplay:YES];
}

- (double)leftValue {
    return MAX(self.minValue, MIN(self.maxValue, leftValue));
}

This way, once minValue and maxValue are set, leftValue and rightValue will represent the correct values. And, by using custom accessors, you can ensure that your constraint that minValue <= leftValue/rightValue <= maxValue is preserved.

Also, for the code you're writing, the MIN and MAX preprocessor macros will save you a lot of effort.

Alex
Probably not the _optimal_ way to do it, but definitely one that fixed my problem (so far at least). Thanks a lot!However setMaxValue: and setMinValue: should probably directly affect the value of leftValue and rightValue. At least that's what Apple's implementation appears to be doing.Proof: Setting an NSSlider's value to 10, then setting the max value to 5 and then up to 15 makes the NSSlider return 5 on next request of its value.
Regexident