0

This is a follow-on question from my previous one relating to why my managedObjectContext was returning to nil. I thought the direction of the question would get buried in the old one.

I now get my mangedObjectContext to not return nil and when performing [outlineView reloadData], nothing at all happens to my outlineView. I tried selectively removing parts of the code that are intended to update the outlineView to see if anything changes and the answer is no. I've since found out (after I wrote this question originally) that my dataSource association is disappearing at some stage.

Notes:

  • the code included below is run once on awakeFromNib of the datasource class and works perfectly.
  • It is on calling it at any other time that my problem arrises. I get no errors debugging but my outlineView remains unchanged.
  • I narrowed this down to running on NSLog of [outlineView dataSource]. When the method is called from awakeFromNib it returns the dataSource as being my dataSourceClass correctly. Every other time it returns the dataSource as nil.
  • the dataSourceClass was bound to the outlineView in InterfaceBuilder.
  • All other NSLog checks I've made to object updates in arrays of my code comes back as expected with correct updates. In fact, I checked the final projectsArray and clientsArray and they contain all new objects introduced before attempting to create nodes in the outlineView.
  • I'm using the standard xcode generated core data app delegate code.
  • I'm not using NSTreeController and using my own NSOutlineViewDataSource. It was not possible (or not documented) on how to populate an outlineView based on the parent/child relationships of two core-data entities.
  • I'll also include my NSOutlineViewDataSource code below.

Updates:

Update 1: Hmmm... just before I call [outlineView reloadData] I tried an NSLog for [outlineView dataSource] and it returned as nil. I associated the dataSource to the outlineView originally by bindings in interfaceBuilder. I'm now assuming this is my problem. Why is my datasource being released? and how do I get it back if I can't prevent it being released?

Code:

refreshOutLineView:

rootNode = [[IFParentNode alloc] initWithTitle:@"Root" children:nil];
    NSInteger clientCounter;
    clientCounter = 0;
    NSFetchRequest *clientsFetchRequest = [[NSFetchRequest alloc] init];
    NSManagedObjectContext *clientsMoc = [clientsController managedObjectContext];
    NSLog(@"clientsMoc is : %@", clientsMoc);
    if(clientsMoc == nil) {
        NSLog(@"And, yes, clientsMoc is = nil");
        clientsMoc = [(Voiced_AppDelegate *)[[NSApplication sharedApplication] delegate] managedObjectContext]; 
        NSLog(@"After managedObjectContext: %@",  clientsMoc);
    }
    NSEntityDescription *clientsEntity = [NSEntityDescription entityForName:@"Clients" inManagedObjectContext:clientsMoc];
    //NSLog(@"clientsEntity, after the 'if nil' code is now: %@", clientsEntity);
    [clientsFetchRequest setEntity:clientsEntity];
    //sort
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"clientCompany" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
    [clientsFetchRequest setSortDescriptors:sortDescriptors];
    NSError *clientsFetchError = nil;
    clientsArray = [clientsMoc executeFetchRequest:clientsFetchRequest error:&clientsFetchError];
    [clientsFetchRequest release];
    //NSLog(@"clientsArray, after fetching is now: %@", clientsArray);

    NSInteger projectCounter;
    projectCounter = 0;
    NSFetchRequest *projectsFetchRequest = [[NSFetchRequest alloc] init];
    NSManagedObjectContext *projectsMoc= [projectsController managedObjectContext];
    if(projectsMoc == nil) {
        NSLog(@"And, yes, projectsMoc is = nil");
        projectsMoc = [(Voiced_AppDelegate *)[[NSApplication sharedApplication] delegate] managedObjectContext]; 
        NSLog(@"After managedObjectContext: %@",  projectsMoc);
    }
    NSEntityDescription *projectsEntity = [NSEntityDescription entityForName:@"Projects" inManagedObjectContext:projectsMoc];
    [projectsFetchRequest setEntity:projectsEntity];
    NSError *projectsFetchError = nil;
    projectsArray = [projectsMoc executeFetchRequest:projectsFetchRequest error:&projectsFetchError];
    [projectsFetchRequest release];
    //NSLog(@"projectsArray, after fetching is now: %@", projectsArray);

    for (NSString *s in clientsArray) {
        NSManagedObject *clientMo = [clientsArray objectAtIndex:clientCounter];  // assuming that array is not empty
        id clientValue = [clientMo valueForKey:@"clientCompany"];
        //NSLog(@"Company is %@", parentValue);

        IFParentNode *tempNode = [[IFParentNode alloc] initWithTitle:[NSString stringWithFormat:@"%@", clientValue] children:nil];

        clientCounter = clientCounter + 1;
        [rootNode addChild:tempNode];
        [tempNode release];
    }

    for (NSString *s in projectsArray) {
        NSInteger viewNodeIndex;
        viewNodeIndex = 0;
        NSManagedObject *projectMo = [projectsArray objectAtIndex:projectCounter];  // assuming that array is not empty
        id projectValue = [projectMo valueForKey:@"projectTitle"];
        id projectParent = [[projectMo valueForKey:@"projectParent"] valueForKey: @"clientCompany"];
        // find if theres an item with the projetParent name
        id nodeTitle = [[rootNode children] valueForKey:@"title"];
        for(NSString *companies in nodeTitle) {
            if([companies compare:projectParent] == NSOrderedSame) {
                //NSLog(@"Company is %@ and parent is %@ and id is: %d", companies, projectParent, viewNodeIndex);
                // then assign that node to be the tempnode.
                IFParentNode *tempNode = [rootNode.children objectAtIndex:viewNodeIndex];
                IFChildNode *subTempNode = [[IFChildNode alloc] initWithTitle:[NSString stringWithFormat:@"%@", projectValue]];
                [tempNode addChild:subTempNode];
                [subTempNode release];
                [tempNode release];
            } else {
                // do nothing.
            }
            viewNodeIndex = viewNodeIndex + 1;
        }
        projectCounter = projectCounter + 1;
    }
    [outlineView expandItem:nil expandChildren:YES];
    [outlineView reloadData];
}

outlineViewDataSource:

- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
    if([item isKindOfClass:[IFChildNode class]]) {
        return nil;
    }
    return (item == nil ? [rootNode childAtIndex:index] : [(IFParentNode *)item childAtIndex:index]);
}

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
    return (item == nil || [item isKindOfClass:[IFParentNode class]]);
}

- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
    if([item isKindOfClass:[IFChildNode class]]) {
        return 0;
    }

    return (item == nil ? [rootNode numberOfChildren] : [(IFParentNode *)item numberOfChildren]);
}

- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
    if([item isKindOfClass:[IFChildNode class]]) {
        return ((IFChildNode *)item).title;
    }

    if([item isKindOfClass:[IFParentNode class]]) {
        return ((IFParentNode *)item).title;
    }

    return nil;
}

// Unessential methods for datasource

- (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
    return (item == nil || [item isKindOfClass:[IFParentNode class]]);
}

- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item {
    return ([item isKindOfClass:[IFChildNode class]]);
}

/* - - - - - - - - - - - - - - - - - - - -
IfChild
 - - - - - - - - - - - - - - - - - - - - */

@implementation IFChildNode
@synthesize title;
- (id)initWithTitle:(NSString *)theTitle {
    if(self = [super init]) {
        self.title = theTitle;
    }

    return self;
}

- (void)dealloc {
    self.title = nil;
    [super dealloc];
}
@end

/* - - - - - - - - - - - - - - - - - - - -
IfParent
 - - - - - - - - - - - - - - - - - - - - */

@implementation IFParentNode
@synthesize title, children;
- (id)initWithTitle:(NSString *)theTitle children:(NSMutableArray *)theChildren {
    if(self = [super init]) {
        self.title = theTitle;
        self.children = (theChildren == nil ? [NSMutableArray new] : theChildren);
    }

    return self;
}

- (void)addChild:(id)theChild {
    [self.children addObject:theChild];
}

- (void)insertChild:(id)theChild atIndex:(NSUInteger)theIndex {
    [self.children insertObject:theChild atIndex:theIndex];
}

- (void)removeChild:(id)theChild {
    [self.children removeObject:theChild];
}

- (NSInteger)numberOfChildren {
    return [self.children count];
}

- (id)childAtIndex:(NSUInteger)theIndex {
    return [self.children objectAtIndex:theIndex];
}

- (void)dealloc {
    self.title = nil;
    self.children = nil;
    [super dealloc];
}
biscuitstack
  • 11,591
  • 1
  • 26
  • 41

2 Answers2

1

Most of the time, this means that you have inadvertently created two instances of your class. One is in your nib and hooked up to everything, while the other is either created in code or created elsewhere in a nib without any connections. An easy way to prove that this is happening is to log self both in awakeFromNib and some method that's seeing nil — you'll see different addresses for the two objects. You'll also find that outlineView itself is nil, since the outlet isn't hooked up to anything.

Chuck
  • 234,037
  • 30
  • 302
  • 389
  • I had no idea that this may be the case but you may well be onto my problem, thankyou! The reason I think so is that I was noticing some of my console messages duplicating. It doesn't happen for anything I've called so far with NSLog, but it happens with some error messages, like *unrecognized selector sent...* so I just have to make sense of that now. – biscuitstack Mar 15 '11 at 20:30
  • Aha. I do indeed get two different addresses for self. – biscuitstack Mar 15 '11 at 20:36
  • 1
    @biscuitstack: First thing I'd do is run a quick project search for `[YourClass alloc]` — if you get a hit, you've found the origin of your phantom instance. – Chuck Mar 15 '11 at 20:47
  • Chuck - the problem swerved away from this area so I didn't get to fully implement your help but appreciated it a lot. – biscuitstack Mar 16 '11 at 15:43
0

It looks to me like you are imposing a tree structure on the data in the controller instead of in the data model. You shouldn't have to fetch two entities into two arrays and then shoehorn them together to create your tree. The idea behind bindings is that controllers simply link the data model to the control and the control displays the object graph within the data model.

So, I think you need to back up and look at your data model again. I infer you need a data model something like this:

Client{
    name:string
    //... some other attributes
    projects<-->>Project.client
}

Project:
    name:string
    // ... some other attributes
    client<<-->Client.projects
}

You want an outline that shows Clients with their related Project objects as children.

If you use NSTreeController you would bind the tree controller to the entity Client with a child path of projects

If you use an Outline Data source, you would just fetch the Client objects sorted as you wish and then return them and their projects as needed.

The important thing here is that the outline or tree structure should be innate in the data model itself. If it is not, then you probably don't want the user looking at and thinking about the data in an outline form.

Remember as well that the data model is an entire layer of the Model-View-Controller design and is not just a dumb store of bits. It can and should contain all the logic necessary to represent the active data in the app. Arguably, the data model is the core of the app with the UI, networking or persistence tacked on as needed. Don't be afraid to put logic related to the data in the data model.

TechZen
  • 64,370
  • 15
  • 118
  • 145
  • TechZen - this was actually my preferred way of doing things - I see my entities and their relationships have everything needed to form the NSOutlineView hierarchy but I abandoned it after my research led to very little examples of this and advice to stay away from NSTreeController because of all its bugs. I posed this question here before and it met silence (except from advice from you that wasn't the definite way to go) and I took the lack of answers as a sign to follow the advice to avoid NSTreeController. Unless xcode questions are just quiet on StackOverflow? – biscuitstack Mar 16 '11 at 12:29
  • This was the previous question: http://stackoverflow.com/questions/5181150/is-there-no-way-to-use-ib-and-the-relationship-of-two-entities-to-populate-an-out The factor that's important here is that the entities can't contain the hierarchy in each. They must be distinct entities of 'clients', 'projects', etc. as they are used extensively elsewhere in the project. – biscuitstack Mar 16 '11 at 12:30
  • Reading your answer again, I don't think you were suggesting breaking away from my current *client* and *projects* entities. So, infact, this is exactly what I have existing. If I attempt to make my *client* child relationship the children value in my treeControllers key paths, I am told that projects is not KVC compliant on trying to expand a client node in the outlineView. Understandable, as I haven't defined what attribute. So if I try *clientChild.projectTitle* instead, I get told that its not supported and the outlineView doesn't load at all. – biscuitstack Mar 16 '11 at 14:26
  • This leaves me wondering should I subclass the `NSTreeController` to interpret the children key path differently. Or should I (if possible) create a bound new entity that merges required aspects of clients and projects together and stays in sync. The latter sounds difficult so I'm hoping it's the former or, preferably, I can get NSTreeController to work without subclassing. – biscuitstack Mar 16 '11 at 14:29
  • 1
    The child keypath should point to an object, not an attribute. So, let's assume the data model I outlined above. The tree controller's entity will be `Client` and the child keypath will be `projects`. The tableview column's `value` attribute will be bound to the tree controller's `arrangedObjects.name`. That should give you a table of client names which will expand to show a list of project names. That's assuming the tree controller works which it should for something this simple. – TechZen Mar 16 '11 at 14:36
  • I might suggest that you skip the outline view for the moment and instead make sure you understand your keypaths throughly. Just use code to send keypath messages to various objects to make sure you are getting back what you expect. – TechZen Mar 16 '11 at 14:44
  • Although not claiming to have a thorough knowledge of key paths, I have been using them sucessfully up to this point and they have been behaving as I hoped. I think we're on the same page in terms of the data model you outlined. My *clients* entity attribute 'clientChild' is equivalent to where you used 'projects' and had a to-many relationship to the projects entity. This returned the KVC error and, as I said, I expected an error as the controller can't assume to use 'projectTitle' to get its value for the child. Thanks again for the ongoing help, TechZen. – biscuitstack Mar 16 '11 at 14:59
  • My column's `value` attribute was also bound exactly as you suggested. Unless the title of *name* being *name* and nothing else is crucial to the treeController understanding children, I think my setup is already as you're suggesting. – biscuitstack Mar 16 '11 at 15:02
  • Ok, this has been driving me insane. I really wish there was clearer documentation for this. My child entity has to have an attribute named exactly the same as the the arrangedObjects key path used for the tableColumn of its parent entity. It's actually very inconvenient as it means I've now an additional attribute in my child entity thats name is a bit irregular. Did I word this question terribly in the question I linked to a few above because this is exactly the solution I was asking for back then. Thanks TechZen! – biscuitstack Mar 16 '11 at 15:37
  • 1
    Let me think about this a bit. I think we're talking past each other here. Let me run off a little project to refamilarize myself with outline views and I'll get back to you – TechZen Mar 16 '11 at 20:20
  • I think that was the problem alright :) This is all working ok now so don't go to too much trouble. Thanks to you inadvertently answering a previous question I managed to get the `NSTreeController` working fine with 2 entities and this has actually made a few of my questions (including this one) needless. It was more of a side note that I said it was a shame the method I had to use was so poorly documented and that it had the inconvenience of upsetting my entity naming system. – biscuitstack Mar 16 '11 at 21:03
  • 1
    I ran up the problem in a little project. I forgot that NSTreeController requires the ***exact*** same keypath name for children for ***every*** entity. The normal solution is to create a NSManagedObject subclass for your entities and then add properties of `children` and `isLeaf` to every subclass. (The properties don't show in the data model). The `children` property just returns the relationship you want as the child, in this case `properties` while the `isLeaf` returns a Bool indicating if the entity has children or not. This allows the tree controller to display any entities. – TechZen Mar 17 '11 at 12:14
  • That sounds perfect. I'll try something like that. Thanks TechZen, you've been a great help. – biscuitstack Mar 17 '11 at 16:04