KVC/KVO 和绑定:为什么我只收到一份更改通知?

发布于 2024-08-02 08:37:16 字数 3706 浏览 7 评论 0原文

我发现 Cocoa 的 KVC/KVO 和绑定有一些奇怪的行为。我有一个 NSArrayController 对象,其“内容”绑定到 NSMutableArray,并且我有一个注册为 arrangedObjects 属性的观察者的控制器在 NSArrayController 上。通过此设置,我希望每次修改数组时都会收到 KVO 通知。然而,KVO 通知似乎只发送一次;第一次修改数组时。

我在Xcode中建立了一个全新的“Cocoa Application”项目来说明这个问题。这是我的代码:

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

这是输出:

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

如您所见,KVO 通知仅在第一次发送(并且当再次发送时)应用程序退出)。为什么会出现这样的情况呢?

更新:

感谢orque 指出我应该绑定到我的 NSArrayControllercontentArray,而不仅仅是它的 content.一旦进行此更改,上面发布的代码就可以工作:

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

I'm seeing some quirky behaviour with Cocoa's KVC/KVO and bindings. I have an NSArrayController object, with its 'content' bound to an NSMutableArray, and I have a controller registered as an observer of the arrangedObjects property on the NSArrayController. With this setup, I expect to receive a KVO notification every time the array is modified. However, it appears that the KVO notification is only sent once; the very first time the array is modified.

I set up a brand new "Cocoa Application" project in Xcode to illustrate the problem. Here is my 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

And here is the output:

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

As you can see, the KVO notification is only sent the first time (and once more when the application exits). Why would this be the case?

update:

Thanks to orque for pointing out that I should be binding to the contentArray of my NSArrayController, not just its content. The above posted code works, as soon as this change is made:

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

大姐,你呐 2024-08-09 08:37:16

,您应该绑定到 contentArray(而不是内容):

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

然后,最简单的方法是仅使用 arrayController 来修改数组:(

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

在实际场景中,您可能只希望按钮操作调用 -addObject:)

首先 -[NSMutableArray addObject]不会自动通知控制器。我发现您尝试通过在 mutableArray 上手动使用 willChange/didChange 来解决此问题。这不起作用,因为数组本身没有改变。也就是说,如果 KVO 系统在更改之前和之后查询 mutableArray,它仍然具有相同的地址。

如果你想使用-[NSMutableArray addObject],你可以在arrangedObjects上willChange/didChange:

- (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"];
}

可能有一个更便宜的密钥可以产生相同的效果。如果您有选择,我建议您仅通过控制器进行工作,并将通知留给底层系统。

First, you should bind to the contentArray (not content):

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

Then, the straightforward way is to just use the arrayController to modify the array:

- (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 a real scenario you'll likely just want the button action to call -addObject:)

Using -[NSMutableArray addObject] will not automatically notify the controller. I see that you tried to work around this by manually using willChange/didChange on the mutableArray. This won't work because the array itself hasn't changed. That is, if the KVO system queries mutableArray before and after the change it will still have the same address.

If you want to use -[NSMutableArray addObject], you could willChange/didChange on 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"];
}

There may be a cheaper key that would give the same effect. If you have a choice I would recommend just working through the controller and leaving the notifications up to the underlying system.

冷了相思 2024-08-09 08:37:16

比显式发布全值 KVO 通知更好的方法是实现 数组访问器并使用它们。然后 KVO 免费发布通知。

这样,而不是这样:

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

您可以这样做:

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

KVO 不仅会为您发布更改通知,而且这将是一个更具体的通知,是数组插入更改而不是整个数组更改。

我通常添加一个执行上述操作的 addThingsObject: 方法,以便我可以执行以下操作:

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

请注意,addObject: 当前不是 KVC 识别的选择器格式数组属性(仅设置属性),而 insertObject:inAtIndex: 是,因此前者的实现(如果您选择这样做)必须使用后者。

A much better way than explicitly posting whole-value KVO notifications is to implement array accessors and use them. Then KVO posts the notifications for free.

That way, instead of this:

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

You would do this:

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

Not only will KVO post the change notification for you, but it will be a more specific notification, being an array-insertion change rather than a whole-array change.

I usually add an addThingsObject: method that does the above, so that I can do:

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

Note that add<Key>Object: is not currently a KVC-recognized selector format for array properties (only set properties), whereas insertObject:in<Key>AtIndex: is, so your implementation of the former (if you choose to do that) must use the latter.

深爱不及久伴 2024-08-09 08:37:16

哦,我一直在寻找这个解决方案!感谢大家!
得到想法后&玩了一下,我发现了另一种非常奇特的方式:

假设我有一个像这样的对象 CubeFrames:

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

我的数组包含 Cubeframes 对象,它们由 objectController 通过(MVC)管理并显示在 tableView 中。
绑定以常见方式完成:
objectController 的“内容数组”绑定到我的数组。
重要提示:将 objectController 的“类名称”设置为 CubeFrames 类

如果我在 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);
}

现在,事实上,我捕获了所有更改:添加和删除行、循环或数字更改:-)

Oh, I was looking for a long time for this solution ! Thanks to all !
After getting the idea & playing around , I found another very fancy way:

Suppose I have an object CubeFrames like this:

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

My Array contains Objects of Cubeframes, they are managed via (MVC) by an objectController and displayed in a tableView.
Bindings are done the common way:
"Content Array" of the objectController is bound to my array.
Important: set "Class Name" of objectController to class CubeFrames

If I add observers like this in my 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);
}

Now, indeed, I catch all the changes : adding and deleting rows, change on loops or number :-)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文