2

Ich sehe ein eigenartiges Verhalten mit Cocoas KVC/KVO und Bindungen. Ich habe ein NSArrayController Objekt, mit seinem 'Inhalt' an eine NSMutableArray gebunden, und ich habe einen Controller als Beobachter der arrangedObjects Eigenschaft auf der NSArrayController registriert. Mit diesem Setup erwarte ich, dass jedes Mal, wenn das Array geändert wird, eine KVO-Benachrichtigung erhalten wird. Es scheint jedoch, dass die KVO-Benachrichtigung nur einmal gesendet wird; Das erste Mal, wenn das Array geändert wird.KVC/KVO und Bindings: Warum erhalte ich nur eine Änderungsbenachrichtigung?

Ich habe ein brandneues "Cocoa Application" -Projekt in Xcode eingerichtet, um das Problem zu veranschaulichen. Hier ist mein Code:

BindingTesterAppDelegate.h

#import <Cocoa/Cocoa.h> 

@interface BindingTesterAppDelegate : NSObject <NSApplicationDelegate> 
{ 
    NSWindow * window; 
    NSArrayController * arrayController; 
    NSMutableArray * mutableArray; 
} 
@property (assign) IBOutlet NSWindow * window; 
@property (retain) NSArrayController * arrayController; 
@property (retain) NSMutableArray * mutableArray; 
- (void)changeArray:(id)sender; 
@end 

BindingTesterAppDelegate.m

#import "BindingTesterAppDelegate.h" 

@implementation BindingTesterAppDelegate 

@synthesize window; 
@synthesize arrayController; 
@synthesize mutableArray; 

- (void)applicationDidFinishLaunching:(NSNotification *)notification 
{ 
    NSLog(@"load"); 

    // create the array controller and the mutable array: 
    [self setArrayController:[[[NSArrayController alloc] init] autorelease]]; 
    [self setMutableArray:[NSMutableArray arrayWithCapacity:0]]; 

    // bind the arrayController to the array 
    [arrayController bind:@"content" // see update 
       toObject:self 
       withKeyPath:@"mutableArray" 
        options:0]; 

    // set up an observer for arrangedObjects 
    [arrayController addObserver:self 
         forKeyPath:@"arrangedObjects" 
         options:0 
         context:nil]; 

    // add a button to trigger events 
    NSButton * button = [[NSButton alloc] 
         initWithFrame:NSMakeRect(10, 10, 100, 30)]; 
    [[window contentView] addSubview:button]; 
    [button setTitle:@"change array"]; 
    [button setTarget:self]; 
    [button setAction:@selector(changeArray:)]; 
    [button release]; 

    NSLog(@"run"); 
} 

- (void)changeArray:(id)sender 
{ 
    // modify the array (being sure to post KVO notifications): 
    [self willChangeValueForKey:@"mutableArray"]; 
    [mutableArray addObject:[NSString stringWithString:@"something"]]; 
    NSLog(@"changed the array: count = %d", [mutableArray count]); 
    [self didChangeValueForKey:@"mutableArray"]; 
} 

- (void)observeValueForKeyPath:(NSString *)keyPath 
         ofObject:(id)object 
         change:(NSDictionary *)change 
         context:(void *)context 
{ 
    NSLog(@"%@ changed!", keyPath); 
} 

- (void)applicationWillTerminate:(NSNotification *)notification 
{ 
    NSLog(@"stop"); 
    [self setMutableArray:nil]; 
    [self setArrayController:nil]; 
    NSLog(@"done"); 
} 

@end 

Und hier ist die Ausgabe:

load 
run 
changed the array: count = 1 
arrangedObjects changed! 
changed the array: count = 2 
changed the array: count = 3 
changed the array: count = 4 
changed the array: count = 5 
stop 
arrangedObjects changed! 
done 

Wie Sie sehen können , das Die KVO-Benachrichtigung wird nur beim ersten Mal gesendet (und noch einmal, wenn die Anwendung beendet wird). Warum sollte das der Fall sein?

Update:

Dank orque für den Hinweis auf, dass ich auf die contentArray meiner NSArrayController verbindlich sein sollte, nicht nur seine content. Die oben gepostet Code funktioniert, sobald diese Änderung vorgenommen wird:

// bind the arrayController to the array 
[arrayController bind:@"contentArray" // <-- the change was made here 
      toObject:self 
      withKeyPath:@"mutableArray" 
       options:0]; 

Antwort

7

Zuerst müssen Sie an die contentArray binden soll (nicht zufrieden):

[arrayController bind:@"contentArray" 
      toObject:self 
      withKeyPath:@"mutableArray" 
       options:0]; 

Dann wird der einfachste Weg ist, einfach nutzen die Arraycontroller das Array zu ändern:

- (void)changeArray:(id)sender 
{ 
    // modify the array (being sure to post KVO notifications): 
    [arrayController addObject:@"something"]; 
    NSLog(@"changed the array: count = %d", [mutableArray count]); 
} 

(in einem realen Szenario werden Sie wahrscheinlich nur die Button-Aktion wollen -addObject nennen :)

Mit - [NSMutableArray addObject] wird der Controller nicht automatisch benachrichtigt. Ich sehe, dass Sie versucht haben, dies zu umgehen, indem Sie manuell mit dem Befehl "wantChange/didChange" für das mutableArray arbeiten. Dies wird nicht funktionieren, da das Array selbst nicht geändert wurde. Das heißt, wenn das KVO-System mutableArray vor und nach der Änderung abfragt, hat es immer noch dieselbe Adresse.

Wenn Sie verwenden möchten - [NSMutableArray addObject], könnten Sie willchange/didChange auf arrangedObjects:

- (void)changeArray:(id)sender 
{ 
    // modify the array (being sure to post KVO notifications): 
    [arrayController willChangeValueForKey:@"arrangedObjects"]; 
    [mutableArray addObject:@"something"]; 
    NSLog(@"changed the array: count = %d", [mutableArray count]); 
    [arrayController didChangeValueForKey:@"arrangedObjects"]; 
} 

Es kann ein billiger Schlüssel sein, um den gleichen Effekt geben würde. Wenn Sie eine Wahl haben, würde ich empfehlen, nur durch den Controller zu arbeiten und die Benachrichtigungen dem zugrunde liegenden System zu überlassen.

+0

+1 und danke für eine sehr detaillierte Antwort. Ich habe meine Bindung an "contentArray" anstelle von "content" geändert, und alles hat wie ein Zauber funktioniert. Zum Ändern des Arrays durch den Controller: Dies war ein vereinfachtes Beispiel. In meiner realen Anwendung ist das Array eine Eigenschaft eines Modellobjekts und wird durch einen anderen Prozess modifiziert. Wenn ich den arrayController verwenden würde, um mein Objekt zu ändern, müssten meine Model-Klassen an meine Controller-Klassen gekoppelt werden, was völlig gegen das MVC-Muster verstößt. –

+0

Die Tastenaktion wäre 'add:', nicht 'addObject:' (was die Schaltfläche hinzufügen würde!). –

5

Ein wesentlich besserer Weg als das explizite Veröffentlichen von Vollwert-KVO-Benachrichtigungen besteht darin, array accessors zu implementieren und zu verwenden. Dann postet KVO die Benachrichtigungen kostenlos.

Auf diese Weise statt dessen:

[self willChangeValueForKey:@"things"]; 
[_things addObject:[NSString stringWithString:@"something"]]; 
[self didChangeValueForKey:@"things"]; 

Sie dies tun würde:

für Sie
[self insertObject:[NSString stringWithString:@"something"] inThingsAtIndex:[self countOfThings]]; 

Nicht nur werden KVO post die Änderungsbenachrichtigung, aber es wird eine spezifischere Meldung sein, eine Änderung der Array-Insertion statt einer Änderung des gesamten Arrays sein.

ich in der Regel eine addThingsObject: Methode hinzufügen, die oben der Fall ist, so dass ich tun kann:

[self addThingsObject:[NSString stringWithString:@"something"]]; 

Beachten Sie, dass add<Key>Object: ist nicht ein KVC anerkannte Selektor Format für Array-Eigenschaften (nur Eigenschaften festlegen), während insertObject:in<Key>AtIndex: ist, so Ihre Implementierung der ehemaligen (wenn Sie dies tun) müssen verwenden Sie die letzteren.

+0

Danke! Es scheint, dass ich etwas in Array Accessoren lesen muss. Das klingt definitiv nach dem fehlenden Teil meines Bindungsverständnisses. Nur um zu verdeutlichen: Ich könnte mehrere 'NSMutableArray'-Eigenschaften haben, und jeder würde sein eigenes' insertObject: in AtIndex: Methode brauchen? –

+0

Rechts. Ich habe ein paar Skripte, die ich als Dienste (mit ThisService) ausführen, die eine Ivar-Deklaration und generieren die meisten der Accessoren, die für sie nützlich wäre. Heutzutage nur nützlich für Array- und Set-Eigenschaften. http://boredzo.org/make-objc-accessors/ –

+0

So würden Sie die Ivar-Deklaration kopieren (zB "' NSMutableArray * mutableArray; '"), fügen Sie es in die Kopfzeile, wählen Sie, was Sie gerade eingefügt haben, führen Sie die Make Obj-C Accessor Declarations-Dienst, fügen Sie die gleiche IVAR-Deklaration in die Implementierung ein, wählen Sie aus, was Sie gerade eingefügt haben, und führen Sie den Dienst Make Obj-C Accessor-Definitionen aus. –

0

Oh, ich suchte lange nach dieser Lösung! Dank an alle ! Nachdem die Idee immer & herum spielen, fand ich eine andere sehr andere Art:

Angenommen, ich habe ein Objekt CubeFrames wie folgt aus:

@interface CubeFrames : NSObject { 
NSInteger number; 
NSInteger loops; 
} 

Mein Array enthält Objekte Cubeframes, werden sie es geschafft, über (MVC) von einem objectController und in einer TableView angezeigt. Bindings sind die übliche Art und Weise: "Content Array" der ObjectController ist an mein Array gebunden. Wichtig: set „Klassenname“ von objectController Klasse CubeFrames

Wenn ich Beobachter wie dies in meinem AppDelegate hinzufügen:

-(void)awakeFromNib { 

// 
// register ovbserver for array changes : 
// the observer will observe each item of the array when it changes: 
//  + adding a cubFrames object 
//  + deleting a cubFrames object 
//  + changing values of loops or number in the tableview 
[dataArrayCtrl addObserver:self forKeyPath:@"arrangedObjects.loops" options:0 context:nil]; 
[dataArrayCtrl addObserver:self forKeyPath:@"arrangedObjects.number" options:0 context:nil]; 
} 

- (void)observeValueForKeyPath:(NSString *)keyPath 
        ofObject:(id)object 
        change:(NSDictionary *)change 
        context:(void *)context 
{ 
    NSLog(@"%@ changed!", keyPath); 
} 

Nun, in der Tat, ich alle Änderungen fange: das Hinzufügen und Löschen von Zeilen, ändern auf Schleifen oder Nummer :-)