Core Data: undo and non-object attribute

  • I'm surely missing a fundamental piece but I can't find it.Here's my
    problem. I'm writing a drawing app and I want to implement an ivar in
    my graphic object like this:

    typedef struct __XSAPPoint
    {
        double    x;
        double    y;
    } XSAPPoint;

    @interface mybasic : NSManagedObject
    {
    XSAPPoint _point;
    }

    I'm using CD to manage save/undo and object graph operations. In my
    entity I declared four attributes: x and y as transient undefined and
    xValue, yValue as double optional persistent.
    Here's the code I wrote:

    @implementation mybasic
    - (void)awakeFromFetch
    {
    NSNumber *value;
        [super awakeFromFetch];
        value = [self valueForKey:@"xValue"];
        if (value != nil )
            _point.x=[value doubleValue];
        value = [self valueForKey:@"yValue"];
        if (value != nil )
            _point.y=[value doubleValue];
    }

    - (double)x
    {
        [self willAccessValueForKey: @"x"];
        double tmpValue = _point.x;
        [self didAccessValueForKey: @"x"];

        return tmpValue;
    }

    - (void)setX:(double)value
    {
        [self willChangeValueForKey: @"x"];
    _point.x=value;
    [self didChangeValueForKey: @"x"];
    }

    - (double)y
    {
        [self willAccessValueForKey: @"y"];
        double tmpValue = _point.y;
        [self didAccessValueForKey: @"y"];

        return tmpValue;
    }

    - (void)setY:(double)value
    {
        [self willChangeValueForKey: @"y"];
    _point.y=value;
    [self didChangeValueForKey: @"y"];
    }

    - (void)willSave
    {
    [self setPrimitiveValue:[NSNumber numberWithDouble:_point.x]
    forKey:@"xValue"];
    [self setPrimitiveValue:[NSNumber numberWithDouble:_point.y]
    forKey:@"yValue"];

        [super willSave];
    }
    @end

    Then I created a simple interface by option-dragging my entity  in my
    doc window in IB.I can save and undo/redo insert/delete operations
    but I can't undo a value editing. The undo is available after an edit
    but the value stays the same. What's wrong with my code ?
    Thank you
    Matteo Rossi
  • On 14/10/2006, at 20.53, Matteo Rossi wrote:

    > Then I created a simple interface by option-dragging my entity  in
    > my doc window in IB.I can save and undo/redo insert/delete
    > operations but I can't undo a value editing. The undo is available
    > after an edit but the value stays the same. What's wrong with my
    > code ?

    Undo/redo is operating on the primitive values stored by the
    NSManagedObject. You never set the primitive values for the x and y
    keys, so they are always nil as far as the undo manager is concerned.

    What exactly are you trying to achieve? Did you read the
    documentation about ivars in NSManagedObject?

    http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/
    Articles/cdAccessorMethods.html#//apple_ref/doc/uid/TP40002154-SW5
  • >
    > Undo/redo is operating on the primitive values stored by the
    > NSManagedObject. You never set the primitive values for the x and y
    > keys, so they are always nil as far as the undo manager is concerned.

    I've tried adding [self setPrimitiveValue:[NSNumber
    numberWithDouble:value] forKey:@"x"] but it doesn't work anyway. Or
    perhaps you're suggesting that I should manage undo in my own
    setPrimitiveValue:forKey?
    > What exactly are you trying to achieve? Did you read the
    > documentation about ivars in NSManagedObject?
    >
    The problem is that I'm using a C-Structure as ivar. CD doesn't seem
    to like it. In fact, docs report:

    "If you choose to represent an attribute using a scalar type (such as
    int or float), or as one of the structures supported by
    NSKeyValueCoding (NSRect, NSPoint, NSSize, NSRange), then you should
    implement accessor methods as illustrated in Listing 2. If you want
    to use any other attribute type, then you should use a different
    pattern, described in Non-Standard Attributes."

    But it's not clear for me.
    Thank you, anyway
  • On 14/10/2006, at 22.18, Matteo Rossi wrote:

    >>
    >> Undo/redo is operating on the primitive values stored by the
    >> NSManagedObject. You never set the primitive values for the x and
    >> y keys, so they are always nil as far as the undo manager is
    >> concerned.
    >
    > I've tried adding [self setPrimitiveValue:[NSNumber
    > numberWithDouble:value] forKey:@"x"] but it doesn't work anyway.

    It would work if you also used [primitiveValueForKey:]

    > Or perhaps you're suggesting that I should manage undo in my own
    > setPrimitiveValue:forKey?

    Nonono, don't even think about it.

    >> What exactly are you trying to achieve? Did you read the
    >> documentation about ivars in NSManagedObject?
    >>
    > The problem is that I'm using a C-Structure as ivar.

    Yes, the question is why are you doing that?

    > CD doesn't seem to like it. In fact, docs report:
    >
    > "If you choose to represent an attribute using a scalar type (such
    > as int or float), or as one of the structures supported by
    > NSKeyValueCoding (NSRect, NSPoint, NSSize, NSRange), then you
    > should implement accessor methods as illustrated in Listing 2. If
    > you want to use any other attribute type, then you should use a
    > different pattern, described in Non-Standard Attributes."
    >
    > But it's not clear for me.

    I don't blame you. The situation with instance variables,
    primitiveValueForKey:, and valueForKey: is a little confusing.

    Here is one way of thinking about it:

    A managed object has two layers: Guts and skin.

    The guts is what you access with primitiveValueForKey: It provides
    storage for all the properties in the entity.
    [primitiveValueForKey:@"foo"] looks for the value in three places:

    1. A primitive accessor method -[primitiveFoo]
    2. An instance variable named "foo". (Probably also "_foo")
    3. NSManagedObject's internal storage. This is where everything is
    stored by default.

    The guts is simply a way of storing values, nothing more. Think of it
    as an extension of instance variables. It does not send KVO
    notifications or anything. You can only store KVC compliant values,
    though.

    The outer layer, the skin, is what you access with valueForKey: It is
    a wrapper around the guts that makes the object easier to use. It
    makes sure that willAccess/didAccess/willChange/didChange methods are
    called, and you can add business logic here, if you want.
    [valueForKey:@"foo"] does one of two things:

    1. Either use a custom accessor method if it exists -[foo]
    2. Or use primitiveValueForKey:@"foo" properly wrapped by willAccess/
    didAccess calls.

    Now, Core Data is all about the guts. Fetching and saving use the
    guts. Undo and redo use the guts. Core Data does not really care
    about the skin.
    Core Data makes the very important assumption that all the state of
    an object is stored in the guts. When you perform an undo operation,
    Core Data simply changes the guts back the the old values. It does
    not call your custom accessor methods to do that - they are skin
    methods, and Core Data does not care about the skin.

    The reason your code is not working with undo is that you are keeping
    your state outside the guts. That is bad. Sure, you are moving values
    in and out of the guts when fetching and saving, but there are no
    such call backs for undo. The undo manager is recording and restoring
    what is in the guts, and your code is ignoring it.

    Actually that it the only thing your code is doing. You have cleverly
    created an object with two double attributes, x and y, that behave
    like normal attributes except they don't support undo.

    That is not what you were trying to do, but it is not easy to see,
    what you really are trying to do. Why do you need to keep x and y in
    a struct instance variable?
  • First of all, thank you, Jacob: you solved my problem and made me
    clearly see what I was doing wrong.
    By implementing primitiveX, primitiveY, I was able to support undo.
    Congratulations for your simple but very effetive explanations: it's
    always a pleasure to find someone like you.

    > Why do you need to keep x and y in a struct instance variable?

    That's the hardest part of the problem. I'll try to explain. My
    drawing application needs to handle many graphic objects (say
    10.000). Since I'm using OpenGL and find myself refreshing the
    drawing view many times per second during panning and zooming I want
    to store object coordinates in ivars because I suspect that using
    valueForKey: would be a total waste of time. Yes I know CD is "highly
    optimized and continuously improved to be efficient" but it still
    requires much more time than the good old way. Am I wrong ?  By
    keeping x and y in a struct I can pass them as a double pointer to
    opengl functions. I must be sincere: I haven't tested performances in
    both cases. It's just an assumption, by now. Thank you again.
  • On 15/10/2006, at 19.09, Matteo Rossi wrote:

    > I'll try to explain. My drawing application needs to handle many
    > graphic objects (say 10.000). Since I'm using OpenGL and find
    > myself refreshing the drawing view many times per second during
    > panning and zooming I want to store object coordinates in ivars
    > because I suspect that using valueForKey: would be a total waste of
    > time. Yes I know CD is "highly optimized and continuously improved
    > to be efficient" but it still requires much more time than the good
    > old way. Am I wrong ?  By keeping x and y in a struct I can pass
    > them as a double pointer to opengl functions. I must be sincere: I
    > haven't tested performances in both cases. It's just an assumption,
    > by now. Thank you again.

    As always with performance issues, you really should measure before
    deciding.

    Remember, you still must call -willAccessValueForKey: and -
    didAccessValueForKey: before using your instance variables directly.

    I would be very interested to hear if you are able to measure a
    performance difference between the plain

    XSAPPoint p;
    p.x = [[self valueForKey:@"x"] doubleValue];
    p.y = [[self valueForKey:@"y"] doubleValue];

    and your

    [self willAccessValueForKey:@"x"];
    [self willAccessValueForKey:@"y"];
    // use _point
    [self didAccessValueForKey:@"x"];
    [self didAccessValueForKey:@"y"];

    Another option would be to use two instance variables named x and y.
    They would be part of "the guts", but you don't have to write your
    primitiveX accessors.

    Please post your measurements, if you make them.
  • >
    > As always with performance issues, you really should measure before
    > deciding.
    >
    > Remember, you still must call -willAccessValueForKey: and -
    > didAccessValueForKey: before using your instance variables directly.

    Just when I thought I had understood everything... If I'm not wrong,
    willAccessValueForKey should be used when the value could be stored
    in the persistent store and a fault should be fired to retrieve the
    correct value. Now, I have two persistent attributes xValue and
    yValue which are sync'ed to my ivars only in willSave and the updated
    values are in my ivars _point.x and _point.y. Why should I use
    willAccessValueForKey: and didAccessValueForKey: ? It's meaningful
    only for CD attributes which must be retrieved in the persistent
    store, I think.  I've tried removing them in the accessors X and Y
    and everything seems to work fine.
    Meanwhile I've run some tests to check real performances. The direct
    ivars access is definitely faster. If I can enable openGL Profiler I
    will post some specs.
  • On 15/10/2006, at 21.36, Matteo Rossi wrote:
    >>
    >> Remember, you still must call -willAccessValueForKey: and -
    >> didAccessValueForKey: before using your instance variables directly.
    >
    > Just when I thought I had understood everything... If I'm not
    > wrong, willAccessValueForKey should be used when the value could be
    > stored in the persistent store and a fault should be fired to
    > retrieve the correct value. Now, I have two persistent attributes
    > xValue and yValue which are sync'ed to my ivars only in willSave
    > and the updated values are in my ivars _point.x and _point.y. Why
    > should I use willAccessValueForKey: and didAccessValueForKey: ?
    > It's meaningful only for CD attributes which must be retrieved in
    > the persistent store, I think.  I've tried removing them in the
    > accessors X and Y and everything seems to work fine.
    > Meanwhile I've run some tests to check real performances. The
    > direct ivars access is definitely faster. If I can enable openGL
    > Profiler I will post some specs.

    Oh, dear. It sounds like you still have a bit of a mess.

    I was hoping you had deleted all of the code you posted. You don't
    need it, and you don't need the transient attributes. They cannot
    help you with your structs. The section about non-standard attributes
    only deals with KVC-compliant values, i.e. objects and NSPoint,
    NSSize, NSRect, or NSRange.

    What you want to do is to declare to perfectly normal persistent
    attributes x and y. If you want to store these in instance variables
    rather than the internal storage of NSManagedObject, you can do it in
    two ways:

    1. Simply create two instance variables with the proper names:

    @interface mybasic : NSManagedObject
    {
    double x, y;
    }

    2. Keep your _point struct and implement primitive get/set accessors:

    - (double)primitiveX
    {
    return _point.x;
    }

    - (void)setPrimitiveX:(double)x
    {
    _point.x = x;
    }

    That is all.

    However, even with the values stored in instance variables, you
    cannot just use them like this:

    - (double)sumBAD
    {
    return _point.x + _point.y;
    }

    Blindingly fast as it may be, it doesn't fire faults, so the values
    may not be initialized. setPrimitiveX: is not called until the fault
    is fired. Using willAccessValueForKey: and -didAccessValueForKey:
    ensures that the instance variables have valid values. If your object
    happens to be faulted already, methods like sumBAD actually do work,
    but only as long as Core Data does not decide to turn the object back
    into a fault.

    The code you posted is not any better in that respect. awakeFromFetch
    is also not called until the fault is fired.

    I am not sure doing this is a good idea at all, in fact the
    documentation clearly states that it isn't.

    I suppose you could write methods like sumBAD that only work once the
    fault has been fired, but you are playing with fire.

    If you need seriously high-speed access to a list of points, you
    should copy them to an old-fashioned plain-old-data C array.
  • >
    > Oh, dear. It sounds like you still have a bit of a mess.
    >
    Surely, I have.
    > I was hoping you had deleted all of the code you posted. You don't
    > need it, and you don't need the transient attributes. They cannot
    > help you with your structs. The section about non-standard
    > attributes only deals with KVC-compliant values, i.e. objects and
    > NSPoint, NSSize, NSRect, or NSRange.
    OK
    > What you want to do is to declare to perfectly normal persistent
    > attributes x and y. If you want to store these in instance
    > variables rather than the internal storage of NSManagedObject, you
    > can do it in two ways:
    >
    > 1. Simply create two instance variables with the proper names:
    >
    > @interface mybasic : NSManagedObject
    > {
    > double x, y;
    > }
    >
    > 2. Keep your _point struct and implement primitive get/set accessors:
    >
    > - (double)primitiveX
    > {
    > return _point.x;
    > }
    >
    > - (void)setPrimitiveX:(double)x
    > {
    > _point.x = x;
    > }
    >
    > That is all.
    >
    No, it can't be all. If I don't implement setX and X, setY and Y KVO
    doesn't work nor undo and save.

    >
    > Blindingly fast as it may be, it doesn't fire faults, so the values
    > may not be initialized. setPrimitiveX: is not called until the
    > fault is fired. Using willAccessValueForKey: and -
    > didAccessValueForKey: ensures that the instance variables have
    > valid values. If your object happens to be faulted already, methods
    > like sumBAD actually do work, but only as long as Core Data does
    > not decide to turn the object back into a fault.
    Here's what I was missing: if I don't use willAccessValueForKey, the
    OBJECT might not be faulted.

    >
    > If you need seriously high-speed access to a list of points, you
    > should copy them to an old-fashioned plain-old-data C array.
    Definitely, I should.
    Now I have declared only two persistent double attributes x and y and
    have deleted all the code about awakeFromFetch and willSave.
    The code now looks like this:

    ...

    - (double)y
    {
        [self willAccessValueForKey: @"y"];
        double tmpValue = _point.y;
        [self didAccessValueForKey: @"y"];

        return tmpValue;
    }
    - (void)setY:(double)value
    {
        [self willChangeValueForKey: @"y"];
    _point.y=value;
    [self didChangeValueForKey: @"y"];
    }

    - (double) primitiveY
    {
    return _point.y;
    }

    - (void) setPrimitiveY:(double)value
    {
    _point.y=value;
    }

    I repeat, I MUST implement Y and setY, otherwise it doesn't work.
    Jacob, I think I've abused of your patience too much. I really thank
    you.
  • On 15/10/2006, at 23.37, Matteo Rossi wrote:

    >> 2. Keep your _point struct and implement primitive get/set accessors:
    >>
    >> - (double)primitiveX
    >> {
    >> return _point.x;
    >> }
    >>
    >> - (void)setPrimitiveX:(double)x
    >> {
    >> _point.x = x;
    >> }
    >>
    >> That is all.
    >>
    > No, it can't be all. If I don't implement setX and X, setY and Y
    > KVO doesn't work nor undo and save.

    That is very interesting. It means my skin-and-guts story was wrong,
    and valueForKey: does not use the primitive accessors. My bad.
previous month october 2006 next month
MTWTFSS
            1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31          
Go to today