views:

1222

answers:

4

I have an NSDocument which has the following structure:

@interface MyDocument : NSDocument
{
    NSMutableArray *myArray;

    IBOutlet NSArrayController *myArrayController;
    IBOutlet MyView *myView;
}
@end

I instantiate the NSArrayController and the MyView in MyDocument.xib, and have made the connections to the File's Owner (MyDocument), so I am pretty sure that from the point of view of Interface Builder, I have done everything correctly.

The interface for MyView is simple:

@interface MyView : NSView {
    NSMutableArray *myViewArray;
}
@end

Now, in MyDocument windowControllerDidLoadNib, I have the following code:

- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
    [super windowControllerDidLoadNib:aController];
    [myArrayController setContent:myArray];
// (This is another way to do it)    [myArrayController bind:@"contentArray" toObject:self withKeyPath:@"myArray" options:nil];

    [myView bind:@"myViewArray" toObject:myArrayController withKeyPath:@"arrangedObjects" options:nil];
}

In the debugger, I have verified that myViewArray is an NSControllerArrayProxy, so it would appear that my programmatic binding is correct. However, when I try to add objects in MyView's methods to the MyView myViewArray, they do not appear to update the MyDocument's myArray. I have tried both of the following approaches:

[myViewArray addObject:value];
[self addMyViewArraysObject:value];

(The second approach causes a compiler error, as expected, but I thought that the Objective-C runtime would "implement" this method per my limited understanding of KVO.)

Is there something wrong with how I'm trying to update myViewArray? Is there something wrong with my programmatic binding? (I am trying to do this programatically, because MyView is a custom view and I don't want to create an IB palette for it.)

+1  A: 

I can see two possibilities here:

First, do you instantiate the NSMutableArray object (and release it) in your MyDocument class? It should look something like this:

- (id)init
{
    if ((self = [super init]) == nil) { return nil; }
    myArray = [[NSMutableArray alloc] initWithCapacity:0];
    return self;
}

- (void)dealloc
{
    [myArray release];
    [super dealloc];
}

Second, did you declare myViewArray as a property in MyView? It should look something like this:

// MyView.h:
@interface MyView : NSView
{
    NSMutableArray * myViewArray;
}
@property (assign) NSMutableArray * myViewArray;
@end
    // MyView.m:
    @implementation MyView
    
    @synthesize myViewArray;
    
    @end
    

    Other than that, it looks to me like you have done all of the binding properly.

    update: How about using the NSArrayController to add items to the array:

    // MyView.h:
    @interface MyView : NSView
    {
        NSMutableArray * myViewArray;
        IBOutlet NSArrayController * arrayController;
    }
    @property (assign) NSMutableArray * myViewArray;
    - (void)someMethod;
    @end
    
      // MyView.m:
      @implementation MyView
      
      @synthesize myViewArray;
      
      - (void)someMethod
      {
          id someObject = [[SomeClass alloc] init];
          [arrayController addObject:[someObject autorelease]];
      }
      
      @end
      
      e.James
      Yes, I didn't include it in the code snippets but I am allocating and retaining an NSMutableArray in the init method.I didn't make MyView's myViewArray variable into a property - I will try that, but for completion's sake, it was my assumption that this should also work if the myViewArray is just a straight-up instance variable?
      erikprice
      I'm pretty sure that myViewArray has to be a property. The cocoa bindings system relies heavily on the notification system, and @synthesized properties automatically implement the necessary notifications.
      e.James
      Well, I added the property using exactly the syntax you recommend, and it doesn't seem to have had the effect I was hoping for. I'm assuming, based on your code, that there is no namespace collision between properties and instance variables? (ISTR that by default properties use the instance variable of the same name, so there shouldn't be.)
      erikprice
      Yes, that's correct. If you specify an @property with the same name as an instance variable, that instance variable will be used for the property.
      e.James
      One more thought came to mind. Have you tried using the NSArrayController to add items to the array? I have a feeling that would be the proper way to do things.
      e.James
      I've posted some code for that option.
      e.James
      Thank you for the suggestion. However, it feels wrong to me - I would prefer for my custom view to not have a dependency on NSArrayController at all, and that it simply accepts an NSMutableArray as its data source. This allows me to keep all responsibility for managing bindings in the controller layer of the code. I'd rather keep the views as simple as possible.
      erikprice
      I found the problem - I was binding the custom view's array to the wrong property of the NSArrayController. The following line in MyDocument is what fixed it: [ovalView bind:@"ovals" toObject:ovalsController withKeyPath:@"content" options:nil];If you can post this as the solution I will mark it as the correct answer (since I can't mark my own answer as correct).
      erikprice
      (Assuming, of course, that it is actually correct to bind to the "content" property of an NSArrayController. I am more concerned with doing this correctly than with getting results that appear to do what I want.)
      erikprice
      I'm glad you got it to work! You should post an answer to your own question so that a record of the solution exists on SO.
      e.James
      As far as the bindings go, doing it right is a noble pursuit. To that end, I believe it is supposed to be the controller's responsibility to add items to an array. The array is your model object, the NSArrayController is (of course) the controller, and your view should really just show the existing contents of the array.
      e.James
      I assume you have a button or a menu somewhere that the user clicks on to add items to the array? If so, you can either bind the action of that control to the "Add" method of the NSArrayController, or you can bind it to a custom IBAction in your MyController, which in turn calls the appropriate method on the NSArrayController.
      e.James
      Thanks, I didn't know I could answer my own question. (Either a new feature or a privilege of reputation.)To answer your question: MyView tracks click-drag movements and creates objects on mouse up. Since I don't have a target/action for this gesture, it seemed to make sense to update the data source - the NSMutableArray - and use bindings to ensure that the controller gets notified of the additions. ( http://stackoverflow.com/questions/681247/how-should-a-custom-view-update-a-model-object )
      erikprice
      Properties don't do anything. Neither do accessors, really. KVO will override your accessors to post the necessary notifications—it doesn't matter whether they're synthesized from a property, or hand-rolled. So you don't really need a property; the notifications will Just Work as long as you have *and use* your accessors. You should use a property, just because it makes it easier to do..
      Peter Hosey
      +1  A: 

      The problem appears to be that I had been binding MyView's myViewArray to the NSArrayController's arrangedObjects property instead of its content property.

      When binding to arrangedObjects, I found that the actual object pointed to by myViewArray was an instance of NSControllerArrayProxy. I didn't find a definitive answer as to what this object actually does when I searched online for more information on it. However, the code examples I found suggest that NSControllerArrayProxy is intended to expose conveniences for accessing the properties of objects in the array, rather than the objects (in the array) themselves. This is why I believe that I was mistaken in binding to arrangedObjects.

      The solution was to instead bind MyView's myViewArray to the NSArrayController's content property:

      - (void)windowControllerDidLoadNib:(NSWindowController *) aController
      {
          [super windowControllerDidLoadNib:aController];
      
          [myArrayController setContent:myArray];
          [myView bind:@"myViewArray" toObject:myArrayController withKeyPath:@"content" options:nil];
      }
      

      Although this appears to work, I am not 100% sure that it is correct to bind to content in this case. If anyone can shed some light on programmatically binding to the various properties of an NSArrayController, I would welcome comments to this answer. Thanks.

      erikprice
      It's not. You should bind to arrangedObjects.
      Peter Hosey
      When I change the binding to arrangedObjects instead of content (even when I implement the indexed accessor methods that you suggest in your answer), the NSArrayController sets MyView's myViewArray to an object that doesn't respond to the insertObject:atIndex: method that I am calling from the indexed accessor method:2009-04-18 07:20:17.670 MyProj[37032:10b] *** -[_NSControllerArrayProxy insertObject:atIndex:]: unrecognized selector sent to instance 0x16dea0(Error message is printed twice.)
      erikprice
      I'm continuing to find code examples online which support my (possibly incorrect) theory that "arrangedObjects" is not intended to be bound-to directly. Rather, that it is a useful proxy object for situations where you want to refer to a property of an item in the array. All the examples show bindings to "arrangedObject.someProperty", but never to just "arrangedObjects" itself.
      erikprice
      Yes, you do need to bind to a property of the model objects in the array. Nonetheless, arrangedObjects is the correct array to bind to. Try adding a self-property to the model objects, where the getter simply returns self and the setter works like -[NSMutableString setString:] (in-place replacement of contents). You'll want to follow the same naming pattern: If your model class is MyThing, the getter is -thing and the setter is -setThing:.
      Peter Hosey
      The model objects are NSValue instances, each of which simply wraps an NSRect. As far as I can tell, there is no property that I can easily append to the @"arrangedObjects" key path to give me the data in the NSValue (that is, the NSRect) that I really care about. Thus, I need some way to obtain the actual elements of the arrangedObjects. Don't I?
      erikprice
      +1  A: 

      The problem is that you're mutating your array directly. Implement indexed accessor methods and call those.

      KVO overrides your accessor methods (as long as you conform to certain formats) and posts the necessary notifications. You don't get this when you talk directly to your array; anything bound to the property won't know that you've changed the property unless you explicitly tell it. When you use your accessor methods, KVO tells the other objects for you.

      The only time to not use your accessor methods (synthesized or otherwise) is in init and dealloc, since you would be talking to a half-inited or -deallocked object.

      Once you're using your own accessor methods to mutate the array, and thereby getting the free KVO notifications, things should just work:

      • The view, when mutating its property, will automatically notify the array controller, which mutates its content property, which notifies your controller.
      • Your controller, when mutating its property, will automatically notify the array controller, which mutates its arrangedObjects property, which notifies the view.
      Peter Hosey
      For some reason I thought that the array controller had some ability to listen for changes on the array itself. I may have gone about this whole thing entirely wrong, because (as I replied to your comment on my answer), even if I add insertObject:inMyViewArrayAtIndex: to MyView, and try to insert the NSValue into the myViewArray in that method, and call that method from mouseUp:, it only works if I'm binding myViewArray to NSArrayController.content in MyDocument. The arrangedObjects proxy fails.
      erikprice
      “For some reason I thought that the array controller had some ability to listen for changes on the array itself.” This is KVO. The array controller adds itself as an observer for the bound-to property; when you mutate that property using its accessors, KVO posts the appropriate notifications, which the array controller receives.
      Peter Hosey
      Ok, good, because that's what I assumed was happening. Thanks for clearing that up. In the meantime, I'm still trying to figure out how NSArrayController can be used with an array of NSValue elements, if we're supposed to be binding to arrangedObjects (which doesn't seem to expose the elements themselves directly).
      erikprice
      NSValue objects are primitive objects (wrapping structures and numbers), not model objects. Go up a level and make a real model class, and put instances of that into the array. Example: If these are rectangles, you could make a Rectangle class that also has properties for fill, stroke, etc. You could also state the rectangle itself differently: for example, as four (bindable) corner-points instead of one corner and a size.
      Peter Hosey
      +1  A: 

      First of all, there's nothing wrong with binding to arrangedObjects: an NSTableColumn, for instance, should have its content bound to arrangedObjects only, and its contentValues to arrangedObjects.someProperty.

      The common mistake is to regard arrangedObjects as the content of an arrayController but that, as you have seen, will lead to grief: arrangedObjects is a representation of the way the arrayController has currently arranged the objects in its content, not the content itself.

      That said, the way to bind an array to an arrayController is:

      [self.myArrayController bind:NSContentArrayBinding 
       toObject:self
       withKeyPath:@"myView.myViewArray" 
       options:nil];
      

      Are you sure, by the way, your view needs to hold the myViewArray? That usually falls under the responsibility of a controller or model object.

      Now you can add objects by calling addObject on the arrayController, since that is the controller's responsibility.

      [self.myArrayController addObject: anObject]
      
      Elise van Looij
      A view needs to hold the array in order to display it. But he's not binding the array controller's content to the view, as you showed; he's binding it to the document.
      Peter Hosey
      Luckily views do not need to hold an object in order to display it - it would render the MVC-pattern useless if they did. Strictly speaking, the view does not display the array, it, or more likely one of its subviews, displays the arrangedObjects of the arrayController. I'm not sure what you mean by the arrayController's content being bound to the document: both Quinn Taylor's code and mine established bindings only between the arrayController and an array.
      Elise van Looij