I am using MonoMac to build a desktop download manager for Mac in C#.
My XIB has a Table View, whose columns are bound to an NSArrayController. The array controller is connected to my Main Window Controller through an IBOutlet. The array holds a bunch of HttpDownload objects, which derive from NSObject. These HttpDownload objects contain properties such as TotalSize, TotalDownloaded, Bandwidth, etc. I have decorated these properties with an [Export] attribute.
In the controller I add some HttpDownload objects to the NSArrayController using the AddObject method. A background process, started with Task.Factory.StartNew() begins the download asynchronously and updates the bound properties such as TotalDownloaded and Bandwidth as data is received.
I can see these new values being reflected in the Table View, but only once I've "forced" a UI update, for instance by causing the window to lose focus, gain focus, or by clicking on a button within the window.
I have tried setting Continuously Updates Value in IB, but this makes no difference (and reading the docs, I didn't think it should).
Does anyone know to make the UI update the bound values in "real time", instead of only when a window event occurs?
I figured this out shortly after I posted this question.
It seems that we need to manually call WillChangeValue() and DidChangeValue() for at least one of the keys that are being updated, for instance, when I updated the total downloaded:
WillChangeValue("DownloadedBytes");
DownloadedBytes += bytesRead;
DidChangeValue("DownloadedBytes");
In my case, calling these methods for just one of the updated keys seems to be enough to force an update of all the bound values.
For reference, in Objective-C these selectors are called [self willChangeValueForKey:#"keyname"] and [self didChangeValueForKey:#"keyname"].
Related
I am pretty new to Core Data and am currently working on a small (OSX) app that uses an NSTableView to organise objects. I would now like to delete a row/object with the click of a button on that targeted row.
I access the managed object within the table controller by calling [NSApp managedObjectContext] (still trying to figure out that dependency injection thing) but I can't easily delete an objectAtIndex: like I used to with the array (which has now been replaced by the core data stack, right?).
How do I identify the object to be deleted? And consequently, how can I cleanly remove it from the stack?
This is probably a really basic question but I couldn't find any resources on it. Bindings obviously don't work because the row does not get selected before the click occurs.
Any help is much appreciated!
Bindings would work, in that you could have the button's IBAction query the objectValue for the parent NSTableCellView. Once you have that objectValue, you could call the bound arrayController to delete the object, and then the cell/row would disappear.
So, if you have a tableCellView that has a delete button with an IBAction, within that IBAction, you could get the sender's superview, ensure it's an NSTableCellView, get the objectValue, and call [myArrayController removeObject:...]
As it says in the NSTableCellView class reference:
The objectValue is automatically set by the table when using bindings or is the object returned by the NSTableViewDataSource protocol method tableView:objectValueForTableColumn:row:.
This is actually a typical pattern with views in cocoa. objectValue or often representedObject are properties on the views that refer to the data model objects they represent, so if you have a view pointer from sender on the IBAction, you can get the related data model object. And if you're using bindings and a controller, you can then just have the controller remove that object.
With bindings, you will often create buttons that need IBActions attached, rather than some direct binding. But those IBActions can most definitely interact with the controller and not the view.
And with core data, array controllers are really slick vs. assuming you have to do it all programmatically.
I have an NSArrayController bound to CoreData in my application. It is also bound to a TableView that displays the data. Two buttons are bound to the ArrayController that add and remove lines. All of this is working as expected. I can add, edit, save, and remove CoreData Entries.
There is a section of my app that is to accept drag and drop operations from files (working). It takes the data from the files, looks for various information, and is to insert this information into the Core Data database via the NSArray Controller.
I have added the class handling the parsing/adding of the file to the database as an object in IB. I created an IBOutlet for the array controller in the class, and bound the controller to the class' referencing outlet.
If I add a button to the interface to directly call the method that adds a custom record to the database, everything works. If the method is called via the drag and drop operation, nothing works, even logging a simple [arrayController className] returns null (though returns NSArrayController as expected when the method is called from the button click).
The only difference I can see is that when accessed through the button click, the method is called directly, while the other way passes through my drag and drop class before loading the parsing class, but I'm completely stuck on how to remedy this situation. I'll be happy to provide code, just not sure which code you'll need.
Any help is appreciated. Thanks!
==================
UPDATE
turns out I was connecting the IBOutlet to a class (a subclass of a view) object in IB instead of to the view itself handling the drops. Connecting these up made things work. Well, not work, I have other issues to iron out now, but the Array controller is now instantiated.
Moved from comment to answer: The array controller you are trying to add stuff is not instantiated. I assume you are not referring to your original NSArrayControllerinstance but maybe a new created one? Probably a problem of communication between your class instances.
Debugging this should be straightforward ... using the debugger. Set a few breakpoints (one at each action the button(s) call, and one at each point where your class instances are meant to talk to each other (your importer and your main controller)). Run, test, step through the code when the debugger breaks at each breakpoint.
My guess: An outlet is not hooked up (is nil) in IB or is not yet reconnected at runtime (see -awakeFromNib and make sure you're not trying to touch an outlet or action that hasn't been fully reconnected from the nib at runtime by the time you're trying to use it).
Something’s not hooked up right, BUT you don’t want to do it this way anyways. There’s no advantage to inserting via an NSArrayController. Just create new objects with NSEntityDescriptions:
+ (id)insertNewObjectForEntityForName:(NSString *)entityName inManagedObjectContext:(NSManagedObjectContext *)context;
And you’re done. If your NSArrayController is hooked up correctly it’ll auto-fetch the new objects at the end of the event so the user will see them “immediately.”
I have a problem when reloading views. OSX is not as simple as IOS.
In MainMenu.xib I have an NSBox. As per Hillegrath, several views are stored in an array and when a segmented button is pressed the views are exposed. This works properly. My frustration involves revisiting these views when changes have been made that should cause them to redisplay new values through [self someAction]. Actions in two of the views alter (global)values that should propagate changes in the other views. I am using a window controller w/xib (MainMenu) to hold the box which contains the views. I also title the views in code.
The global ivar values change properly when required and the log shows that. However dependent operations do not occur when the view are revisited, ie, update view specifics.
What appears to happen is that loadView is not called when the various views are displayed, ** **. awakeFromNib and loadView operate correctly when each view is first displayed but not ever on redisplays. This implies that that the view may be hidden but viewDidUnhide has no effect.
The view changing code (from an SO MVC answer and Hillegrath) is
NSViewController *activeVC =
(NSViewController *) self.viewControllers[index];
// [_box setContentView:nil];
[_box setContentView:activeVC.view];
[_box setNeedsDisplay:YES];
From the copied code it can be seen that I have also attempted setting the active content to nil before setting a new view but, to no avail.
Any ideas, recommendations, notifications fail to work either but may not set up correctly.
Thanks
It looks like you’re confusing view loading with display. -awakeFromNib is received by a nib file object only when the nib file is loaded. Similarly, -loadView is received by the view controller only when its corresponding nib file is loaded. Since you are keeping your view controllers in an array, those view controllers do not get deallocated, hence their corresponding nib files are never unloaded whilst the array is alive. This is reasonable behaviour, but you must bear in mind that -awakeFromNib and -loadView are only executed once in this case because nib loading is executed only once.
Since it seems that you are manually populating the view in -loadView, you’ll also have to do that whenever you set that view as the box’s content view (assuming you’re not always updating the view whenever the model changes). For instance, you could have a -reloadData method in your view controller and both -loadView and your box’s content view swapping method would use -reloadData.
Alternatively, you could set your model object as the represented object of your view controller and bind the controls in the corresponding view to properties of that represented object. NSViewController exposes a representedObject property that’s convenient for bindings.
For the record, -[NSBox setContentView:] marks the box for redisplay, so you don’t need to send it -setNeedsDisplay:YES.
(and make sure _box actually points to the NSBox instance)
In my Cocoa app, I have a sheet with a one-column NSTableView that lists a bunch of files in a directory (the app makes back-ups of it's main database, provides this list to users so they can revert to a particular back-up). The content is loaded into and provided to the table view by an NSArrayController, each object is just an NSFileWrapper (I'm considering using NSURL instead, but I digress). The NSArrayController handles sorting, enabling the buttons when a row is selected via bindings, that's all great. I have an NSWindowController subclass object (BackupsSheetController) that hooks all this up and exists in the sheet's nib.
However, when a user edits one of the cells, I want to respond to that change from BackupsSheetController by appropriately re-naming the file represented by that cell, putting it in its new location. Since the table view is bound to the NSArrayController, I don't get sent the NSTableViewDataSource message – tableView:setObjectValue:forTableColumn:row:. If I set my BackupsSheetController as the datasource for the NSTableView object in the nib, I get sent that message sometimes, but not very often, to say nothing of every time.
Most questions and examples I see out there for this scenario handle this all by using a custom model class for items in their table view, and make some controller object an observer for changing properties that they wish to respond to. In other words, each item would be something like a BackupNode object, and BackupsSheetController would observe each for changes to the name property (or whatever I would call it). That seems totally overkill for my scenario, but I also don't want to ditch the bindings I've already got in use and I don't see another way to do this. Is there another way to do this, to make sure I reliably get the setObject:... message? Or should I drop the NSArrayController and make BackupsSheetController the delegate and datasource for the table?
In the "BackupNode" scenario, I don't see why BackupsSheetController would observe each for changes in its name. That's a very roundabout way of doing things. I would think that the hypothetical BackupNode object would simply do the necessary work in its setter for the name property.
Anyway, I recommend using proper model objects. When you try to build a model with only Cocoa-provided objects like NSFileWrapper, NSURL, or NSMutableDictionary, you end up doing more work in the long run than if you just make a proper model object.
On a tangential topic, why is your window controller in the NIB? It should be the thing which loads (and owns) the NIB, which of course requires that it exist prior to the NIB being loaded, which means it can't be instantiated in the NIB.
I am building a Cocoa desktop application. I want to know when a NSView's isHidden status has changed. So far using target/action doesn't help, and I can't find anything in NSNotification for this task. I would like to avoid overriding the setHidden method, because then I'll have to override all the NSView derived class that I am using.
UPDATE: I ended up using KVO. The path for "isHidden" is "hidden", probably because the setter is "setHidden".
You could use Key-Value Observing to observe the isHidden property of the NSView(s). When you receive a change notification from one of these views, you can check if it or one of its superviews is hidden with -isHiddenOrHasHiddenAncestor.
A word of warning: getting Key-Value Observing right is slightly tricky. I would highly recommend reading this post by Michael Ash, or using the -[NSObject gtm_addObserver:forKeyPath:selector:userInfo:options] method from the NSObject+KeyValueObserving category from the Google Toolbox for Mac.
More generally, one can override viewWillMoveToWindow: or the other related methods in NSView to tell when a view will actually be showing (i.e. it's window is in the window display list AND the view is not hidden). Thus the dependency on KVO for the 'hidden' key used above is removed, which only works if setIsHidden has been called on that view. In the override, 'window' (or [self window]) will indicate whether the view is being put into a visible view hierarchy (window is non-nil) or being taken out of it (window is nil).
I use it for example to start/stop a timer to update a control from online data periodically - when I only want to update while the control is visible.
Could you override the setter method for the hidden property so that it will trigger some custom notification within your application?