2

Je vois un comportement bizarre avec KVC/KVO de Cocoa et les liaisons. J'ai un objet NSArrayController, avec son 'contenu' lié à un NSMutableArray, et j'ai un contrôleur enregistré en tant qu'observateur de la propriété arrangedObjects sur le NSArrayController. Avec cette configuration, je m'attends à recevoir une notification KVO chaque fois que le tableau est modifié. Cependant, il semble que la notification KVO n'est envoyée qu'une seule fois; la toute première fois que le tableau est modifié.KVC/KVO et fixations: pourquoi ne reçois-je qu'une seule notification de modification?

J'ai mis en place un tout nouveau projet "Cocoa Application" dans Xcode pour illustrer le problème. Voici mon 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 

Et voici la sortie:

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 

Comme vous pouvez le voir , la La notification KVO est uniquement envoyée la première fois (et une fois de plus lorsque l'application se termine). Pourquoi cela serait-il le cas?

mise à jour:

Merci à orque de remarquer que je lie au contentArray de mon NSArrayController, non seulement son content. Le code ci-dessus affiché fonctionne, dès ce changement:

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

Répondre

7

D'abord, vous devez lier à la contentArray (non content):

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

Ensuite, la manière simple est d'utiliser simplement le arrayController de modifier le tableau:

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

(dans un vrai scénario, vous aurez probablement voulez juste l'action du bouton pour appeler -addObject :)

L'utilisation de - [NSMutableArray addObject] n'informera pas automatiquement le contrôleur. Je vois que vous avez essayé de contourner cela en utilisant manuellement willChange/didChange sur le mutableArray. Cela ne fonctionnera pas parce que le tableau lui-même n'a pas changé. Autrement dit, si le système KVO interroge mutableArray avant et après la modification, il aura toujours la même adresse.

Si vous souhaitez utiliser - [NSMutableArray addObject], vous pourriez willChange/didChange sur 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"]; 
} 

Il peut y avoir une clé moins cher qui donnerait le même effet. Si vous avez le choix, je recommanderais simplement de passer par le contrôleur et de laisser les notifications au système sous-jacent.

+0

+1 et merci pour une réponse très détaillée. J'ai changé ma liaison à "contentArray" au lieu de "content", et tout a fonctionné comme un charme. Quant à changer le tableau à travers le contrôleur: C'était un exemple simplifié. Dans ma vraie application, le tableau est une propriété sur un objet modèle, et il est modifié par un autre processus. Si je devais utiliser arrayController pour modifier mon objet, mes classes Model devraient être couplées à mes classes Controller, ce qui va complètement à l'encontre du pattern MVC. –

+0

L'action du bouton serait 'ajouter:', pas 'addObject:' (ce qui ajouterait le bouton!). –

5

Un moyen bien meilleur que l'affichage explicite de notifications KVO de valeur entière consiste à implémenter array accessors et à les utiliser. Ensuite, le KVO publie les notifications gratuitement.

De cette façon, au lieu de cela:

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

Vous devez faire cela:

[self insertObject:[NSString stringWithString:@"something"] inThingsAtIndex:[self countOfThings]]; 

Non seulement KVO afficher la notification de changement pour vous, mais ce sera une notification plus précise, étant un changement d'insertion de tableau plutôt qu'un changement de tableau entier.

Je l'habitude d'ajouter une méthode addThingsObject: qui fait ce qui précède, pour que je puisse faire:

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

Notez que add<Key>Object: est pas un format de sélection reconnu KVC pour les propriétés de tableau (uniquement propriétés de l'ensemble), alors que insertObject:in<Key>AtIndex: est, donc votre mise en œuvre de l'ancien (si vous choisissez de le faire) doit utiliser ce dernier.

+0

Merci! Il semble que je devrais faire quelques lectures dans les accesseurs de tableau. Cela ressemble certainement à la pièce manquante dans ma compréhension des reliures. Juste pour clarifier: je pourrais avoir plusieurs propriétés 'NSMutableArray', et chacune aurait besoin de son propre' insertObject: in AtIndex: méthode? –

+0

Droite. J'ai une paire de scripts que je cours en tant que services (en utilisant ThisService), qui prennent une déclaration ivar et génèrent la plupart des accesseurs qui seraient utiles pour cela. De nos jours, seulement utile pour les propriétés array et set. http://boredzo.org/make-objc-accessors/ –

+0

Pour copier la déclaration ivar (par exemple, "NSMutableArray * mutableArray;' "), collez-la dans l'en-tête, sélectionnez ce que vous venez de coller, exécutez la commande Make Service Obj-C Accessor Declarations, collez la même déclaration ivar dans l'implémentation, sélectionnez ce que vous venez de coller et exécutez le service Make Obj-C Accessor Definitions. –

0

Oh, je cherchais depuis longtemps cette solution! Merci à tous ! Après avoir obtenu l'idée & jouer autour, j'ai trouvé une autre façon très chic:

Supposons que j'ai un CubeFrames objet comme celui-ci:

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

Mon tableau contient des objets de Cubeframes, ils sont gérés via (MVC) par un objectController et affiché dans un tableauView. Les liaisons sont effectuées de la manière habituelle: "Content Array" de objectController est lié à mon tableau. Important: set « Nom de la classe » de objectController à CubeFrames classe

Si j'ajoute des observateurs comme ça dans mon Appdelegate:

-(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); 
} 

Maintenant, en effet, je prends tous les changements: l'ajout et la suppression de lignes, changement sur les boucles ou le nombre :-)

Questions connexes