Core Data Package Documents

July 19, 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.

One Response to “Core Data Package Documents”


  1. [...] so a few days ago, I started the journey to switch ReceiptWallet to document based. Based on some work that others have done in making NSPersistentDocument work with packages, I created a test [...]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.