Core Data Persistent Packages revisited
19 July 2007
This post is a follow-up to another post I wrote on the very same subject. I am showing here the full implementation of a NSPersistentDocument based class that allows to use package documents embedding a Core Data store.
I short, what this post adds to the previous one is:
- improved encapsulation;
- NSDocumentController subclass to correctly handle the Recent Document menus;
- fixed a problem with NSError handling, though still not doing any proper error management.
Those improvements originated from a discussion with Tim Perrett in the cocoa-dev mailing list and from a comment by Laurent Sansonetti to my original post. Thanks to both.
PersistentPackageDocument Class
The PersistentPackageDocument class can be used as a base class for your document classes whenever you want them use a document package embedding the actual Core Data data store. PersistentPackageDocument derives from NSPersistentDocument and overrides four methods: initWithContentsOfURL_ofType_error, writeToURL_ofType_forSaveOperation_originalContentsURL_error, readFromURL_ofType_error, displayname. Here’s the code:
class PersistentPackageDocument < OSX::NSPersistentDocument
#-- returns the document name to display in the window title
def displayName
if (fileURL)
documentNameFromDataStoreURL(fileURL)
else
'Untitled'
end
end
#-- returns the package document path by stripping the dataStoreName component
#-- from the data store URL; used in displayName
def documentNameFromDataStoreURL(url)
/([^\/]+)\/?$/ =~ url.relativePath.gsub(/#{dataStoreName}$/, '')
$1 + " - View"
end
def dataStoreURLFromPackageURL(url)
dataStorePath = url.relativePath.stringByAppendingPathComponent(dataStoreName)
OSX::NSURL.fileURLWithPath(dataStorePath)
end
def readFromURL_ofType_error(url, type, errorPtr)
path=url.relativePath
if (!OSX::NSFileManager.defaultManager.fileExistsAtPath_isDirectory(path, nil))
#-- YOUR ERROR MANAGEMENT HERE
end
result = super_readFromURL_ofType_error(url, type, nil)
if (!result)
#-- SET ERROR INFORMATION TO BE RETURNED VIA errorPtr.assign(nserror_object)
end
result
end
def writeToURL_ofType_forSaveOperation_originalContentsURL_error(url, type, op, content, errorPtr)
#-- if content is not nil, then we are saving a newly created document
#-- in this case, initWithURL is not called, so we had no chance to fix the url,
#-- let's do it here.
if (content == nil)
path = url.relativePath
url = dataStoreURLFromPackageURL(url)
isDirectory = false
if (!OSX::NSFileManager.defaultManager.createDirectoryAtPath_attributes(path, nil))
#-- YOUR ERROR MANAGEMENT HERE, set errorPtr
return false
end
end
ok = super_writeToURL_ofType_forSaveOperation_originalContentsURL_error(url, type, op, content, nil)
if (!ok)
#-- SET ERROR INFORMATION TO BE RETURNED VIA errorPtr.assign(nserror_object)
end
ok
end
def initWithContentsOfURL_ofType_error(url, type, errPtr)
url = dataStoreURLFromPackageURL(url)
ok, err = super_initWithContentsOfURL_ofType_error(url, type, nil)
if (!ok)
#-- SET ERROR INFORMATION TO BE RETURNED VIA errorPtr.assign(nserror_object)
end
ok
end
end
For a more detailed discussion of the rationale behind this implementation, see my previous post.
You should then change your MyDocument class (the one produced by XCode templates) so that it derives from PersistentPackageDocument instead of NSPersistentDocument and it adds a dataStoreName method that returns the data store file name for that specific document. Here an example:
class MyDocument < PersistentPackageDocument
def dataStoreName
'data.xml'
end
#-- default RubyCocoa implementation: managedObjectModel, setManagedObjectContext, windowNibName, etc.
end
Supporting Recent Documents
The PersistentPackageDocumentClass as given above is fully capable of dealing with package documents embedding a Core Data data store. Unfortunately, it alone cannot ensure that the Recent Documents menu is correctly handled in your application. To that aim, you need to override your NSDocumentController noteNewRecentDocumentURL method so that it does some juggling with the path that is stored with the recent document menus.
If your package document is enough rich, chances are that you are already subclassing NSDocumentController, so overriding noteNewRecentDocumentURL is a snap. Otherwise, here is a sample subclass:
class PersistentPackageDocumentController < OSX::NSDocumentController
def init
super_init
end
def packageURLFromDataStoreURL(url)
dataStoreName = currentDocument.dataStoreName
OSX::NSURL.fileURLWithPath(url.relativePath.gsub(/#{dataStoreName}$/, ''))
end
def noteNewRecentDocumentURL(url)
if (currentDocument)
super_noteNewRecentDocumentURL(packageURLFromDataStoreURL(url))
end
end
end
As already mentioned, the key point is the method noteNewRecentDocumentURL, while packageURLFromDataStoreURL is just responsible for string manipulation. Note also that packageURLFromDataStoreURL accesses the current document to retrieve its dataStoreName and this forces to guard against the case when there is no current document. There are many alternative implementation of this behaviour, in particular you could define the method dataStoreName in the document controller class and let PersistentPackageDocument access it there. This approach has the adavantage that a “current” document controller is always there, but for presentation reasons it is not taken here.
Sublassing NSDocumentController has its own particularities. The easiest way to do it is in Interface Builder MainMenu.nib file. Just subclass and instantiate it in the nib and the above code will be used for your document shared controller. Read this FAQ for more information.
Summing up
The two classes defined above will allow you to easily integrate package documents in your Core Data application.
One final note: make sure you define your document classes as packages in your target properties.
24 March 2008 at 3:45 am
I’m a real Cocoa newbie trying to get my head around all the concepts.
Your code helped me immensely! I had some problems porting it to Objective C (mainly a weird error that occurred if I didn’t pass an NSError to super’s writeToUrl:.. and the lack of RegExps) but I think it’s working now.
Besides the Recent Items thing, the window title icon seems to lock onto the data store, instead of the package (when opening a file). I don’t think that’s too hard to fix though.
Thank you!
14 July 2008 at 8:23 pm
I may be a year late, but I do have a suggestion for a modification. If instead of using pattern matching to do the path string manipulation, use the NSString path manipulation methods instead.
For dataStoreURLFromPackageURL:, use:
+ (NSURL *)dataStoreURLFromPackageURL:(NSURL *)URL {
NSString storePath = [[url relativePath] stringByAppendingPathComponent:[self fileStoreName]];
return [NSURL fileURLWithPath:storePath];
}
For packageURLFromDataStoreURL: it is much simpler, you can use:
+ (NSURL *)packageURLFromDataStoreURL:(NSURL *)URL {
NSString *storePath = [URL relativePath];
return [NSURL fileURLWithPath:[storePath stringByDeletingLastPathComponent]];
}
This removes the need for the NSDocumentSubcontroller subclass from having to check to see if a document exists, as the name translation is independent of an instantiated document. I do not know if some of the idioms are available from ruby, but in ObjC I use ’self’ in the class methods to make it polymorphic safe, i.e. subclasses can override the class method and have it still work. When the data store name is required in the instance methods I use [[self class] fileStoreName] for the same reason.
14 July 2008 at 8:36 pm
I just noticed a few issues with my last comment, here are some fixes:
+ (NSURL *)dataStoreURLFromPackageURL:(NSURL *)URL {
NSString *storePath = [[URL relativePath] stringByAppendingPathComponent:[self fileStoreName]];
return [NSURL fileURLWithPath:storePath];
}
I accidently did not make storePath a pointer and the capitalization of ‘URL’ was off.