Applescripting core data

A few weeks ago I made a little Core Data application in XCode which has been working fine. It’s a little app that helps me keep track of my projects. The problem started when I decided I didn’t no longer wanted to type in the path to a project folder, I wanted to do a choose folder and store the result in my app and do useful stuf with it, preferably in Applescript of course, since I’ve no experience in Cocoa programming. (I posted this problem to the Cocoa-Dev list as well, but I guess they want nothing to do with applescripters).
Anyway, I soon found out that for that I needed to subclass my entity Projecten (the app speaks Dutch, as do I) and that ProjectBuilder very gallantly writes all the code if one selects the data model and chooses File >> New File > Managed Object Class. So now I have Projecten.h and Projecten.m with all the setters and getters, like so:

Projecten.m —I didn’t write this, Xcode did, so it should be good-------------------------

#import “Projecten.h”

@implementation Projecten

  • (NSString *)projectFolder
    {
    NSString * tmpValue;

    [self willAccessValueForKey: @“projectFolder”];
    tmpValue = [self primitiveValueForKey: @“projectFolder”];
    [self didAccessValueForKey: @“projectFolder”];

    return tmpValue;
    }

  • (void)setProjectFolder:(NSString *)value
    {
    [self willChangeValueForKey: @“projectFolder”];
    [self setPrimitiveValue: value forKey: @“projectFolder”];
    [self didChangeValueForKey: @“projectFolder”];
    }
    @end


The problem, however, is that no matter what I do I cannot seem to get “setProjectFolder:” to do anything. I have a button to whose on Click I’ve attached the following applescript, which achieves exactly nothing: no errors, but no result either:


on clicked theObject
	if the name of theObject is "AddProjectFolder_btn" then
		local strProjectFolder
		set strProjectFolder to (choose folder) as string
		call method "setProjectFolder:" of class "Projecten" with parameter strProjectFolder
	end if
end clicked

I’ve read Jobu’sHow to create an Objective-C subclass on http://applescriptsourcebook.com/viewtopic.php?id=17559, and I’ve tried subclassing NSManagedObjectContext in IB. I’ve called the subclass “Projecten” and I’ve added an action called setProjectFolder:. When click on the View in editor button in the Inspector, XCode does open up Projecten.m, which gave me hope, but still call method “setProjectFolder:” of class “Projecten” produces nothing and neither does

call method "projectFolder" of class "Projecten" 

.

Before this I tried another route: I connected the “Awake on nib” action of the Projecten Array Controller to my Projecten_view.applescript. The results seemed hopeful at first but in the end led me nowhere.


on awake from nib theObject
	log (call method "entityName" of theObject) -- returns "Projecten"
	set objMOC to call method "managedObjectContext" of theObject
	log (call method "hasChanges" of objMOC) -- returns 0
end awake from nib

This seems encouraging, but when I store objMOC in a property and then later, for instance in the onClick handler, ask it again to

log (call method "hasChanges" of objMOC) -- returns 

again nothing happens: no errors, no results. I really hope someone can shine a light on this, because I’m out of ideas. Core Data is good but if I could figure this out, it would come close to perfection.

EliseVanLooij,

I’m not sure exactly what your application does, but it seems to me that all you really need in this situation is a subroutine (or handler). Something like this:

on clicked theObject
   if the name of theObject is "AddProjectFolder_btn" then
       local strProjectFolder
       set strProjectFolder to (choose folder) as string
       my doSomething (strProjectFolder)
   end if
end clicked

on strProjectFolder (folderChosen)
-- this is where the stuff gets done to your chosen folder.
end strProjectFolder

I may be misunderstanding your problem, so excuse me if this is not a solution to your problem.

PreTech

That would work if I only ever wanted to do stuff to the chosen folder at that particular moment. As, however, I want to keep track of my projects over a long period of time, I need to store the data. I could use any old database but Core Data comes with a beautiful data modeling tool and all the view, controller and model classes all ready for me to use. Now I just need to figure out how to talk to them,

Hallelujah, I did manage to figure it out. I finally realized that the ManagedObjectContext is a property of the application. And, according to the Applescript Studio Terminology Reference, when one issues a call method without specifying an object, AS assumes you want to target the application or its delegate. This finally enabled me to get a proper reference to the ManagedObjectContext (which handles the communication between the interface and the Core Data persistent store. The rest was a matter of reading the documentation on all the relevant NS objects.

For those who want to try it out for themselves:
–download the Core Recipes sample application at

–in the datamodel, add to the entity “Recipe” the attribute “recipeDoc” (string).
–ctrl-click on the folder “Linked Frameworks” >> Add Framework >> Existing Frameworks. Select AppleScriptkit.framework (it’s in System >> Library >> Frameworks).
–choose File >> New file >> Applescript text file. Name it “Recipe_model.applescript”.
–ctrl_click on the target and choose Add new build fase >> New Applescript build fase.
– Make sure that Recipe_model.applescript has a check in the Target column, otherwise it will be ignored when you build the project.
–Open the menu.nib in Interface Builder and add the following to the interface:

  • a text field. In the Bindings pane of the Inspector, bind the value to RecipesController, controller key: selection, model key path: name. In the Applescript pane of the Inspector, give it the name “Name_textField”.
  • another text field. In the Bindings pane of the Inspector, bind the value to RecipesController, controller key: selection, model key path: recipeDoc.
  • a button. In the Applescript pane of the Inspector, give it the name “Browse_btn” and connect its “on click” handler to Recipe_model.applescript.

Now put the following code in Recipe_model.applescript and you can link your recipes to any document you wish.


-- Recipe_model.applescript
-- CoreRecipesCocoaBindings-End

--  Created by Elise van Looij on 8-11-06.
--  Copyright 2006 Elise van Looij. All rights reserved.


on clicked theObject
	if the name of theObject is equal to "Browse_btn" then
		local strName
		set strName to contents of text field "Name_textField" of window 1
		log (strName)
		setRecipeDoc given theRecipeName:strName, theRecipeDoc:(choose file without invisibles)
	end if
end clicked

property theRecipeDoc : "recipeDoc"

on setRecipeDoc given theRecipeName:strName, theRecipeDoc:strRecipeDoc
	log "setRecipeDoc given theRecipeName:" & strName & ", theRecipeDoc:" & strRecipeDoc
	local objMOC
	set objMOC to getManagedObject given theEntityName:"Recipe", theKey:"name", theValue:strName
	call method "willChangeValueForKey:" of objMOC with parameter theRecipeDoc
	call method "setPrimitiveValue:forKey:" of objMOC with parameters {(strRecipeDoc as string), theRecipeDoc}
	call method "didChangeValueForKey:" of objMOC with parameter theRecipeDoc
end setRecipeDoc

on getManagedObject given theEntityName:strEntityName, theKey:strKey, theValue:strValue
	log "getManagedObject given theEntityName:" & strEntityName & ", theKey:" & strKey & ", theValue:" & strValue
	local ManagedObjects_array, myManagedObject, myEntityName, theResult
	
	set theResult to missing value
      	log (call method "description" of (call method "registeredObjects" of (call method "managedObjectContext"))) -- this is for debugging purposes only, it shows you want the ManagedObjectContext sees
	set ManagedObjects_array to call method "allObjects" of (call method "registeredObjects" of (call method "managedObjectContext")) --returns NSArray
	repeat with myManagedObject in ManagedObjects_array
		set myEntityName to (call method "name" of (call method "entity" of (call method "objectID" of myManagedObject)))
		if myEntityName is equal to strEntityName then
			try
				if strValue is equal to (call method "valueForKey:" of myManagedObject with parameter strKey) then
					set theResult to myManagedObject
					exit repeat
				end if
			end try
			
		end if
	end repeat
	return theResult
end getManagedObject

Note that getting the name property of the recipe via the bound text field is a workaround. The name (or any other attribute whose value in the data model is set to “required”) is needed to locate the active ManagedObject in the ManagedObjectContext. The ManagedObjectContext itself has no notion of which object the user wants to modify–see what is produced in the log by the description method on the Registered Objects.
The selection (the object on which the user is working) is a function of ArrangedObjects which belongs to the view and as yet I don’t know how to talk to that.

The whole thing is even easier then I thought–which is always a good sign. Never mind finding a ManagedObject in the ManagedObjectContext, simply update the selectedObject. So, here’s my even-simpler-way to applescript a Core Data application:

  1. Get yourself a Core Data Application, for instance by downloading, the Core Recipes sample application at

–in the datamodel, add to the entity “Recipe” the attribute “recipeDoc” (string).
2) Ctrl-click on the folder “Linked Frameworks” >> Add Framework >> Existing Frameworks. Select AppleScriptkit.framework (it’s in System >> Library >> Frameworks).
3) Ctrl_click on the target and choose Add new build fase >> New Applescript build fase.
4) Choose File >> New file >> Applescript text file. Name it “Recipe.applescript”. Make sure that Recipe.applescript has a check in the Target column, otherwise it will be ignored when you build the project.
5) Open the menu.nib in Interface Builder and add the following to the interface:

  • another text field. In the Bindings pane of the Inspector, bind the value to RecipesController, controller key: selection, model key path: recipeDoc.
  • a button. In the Applescript pane of the Inspector, give it the name “Browse_btn” and connect its “on click” handler to Recipe.applescript.
  1. In the Instances panel, select the ArrayController called “RecipesController”. In the Applescript pane of the Inspector, connect its “awake from nib” handler to Recipe.applescript. (You can give RecipesController an applescript name as well, but you won’t be able to use it.)

Now put the following code in Recipe_model.applescript and you can link your recipes to any document you wish.


on clicked theObject
	if the name of theObject is equal to "Browse_btn" then
		setRecipeDoc given theRecipeDoc:(choose file without invisibles)
	end if
end clicked

on awake from nib theObject
	try
		log the name of theObject
	on error m
		--objects that don't have a name property will throw an error
		if m contains "NSReceiverEvaluationScriptError" then
			--let's see if theObject is an NSObjectController
			log (call method "entityName" of theObject)
			if (call method "entityName" of theObject) is equal to "Recipe" then
				setRecipe_OC given theRecipe_ObjectController:theObject
			end if
		end if
	end try
end awake from nib

property Recipe_OC : missing value

on setRecipe_OC given theRecipe_ObjectController:objRecipe_ObjectController
	set Recipe_OC to objRecipe_ObjectController
end setRecipe_OC

on getRecipe_OC()
	return Recipe_OC
end getRecipe_OC

on setRecipeDoc given theRecipeDoc:strRecipeDoc
	log "setRecipeDoc given theRecipeDoc:" & strRecipeDoc
	setManagedObject given theKey:"recipeDoc", theValue:strRecipeDoc
end setRecipeDoc

on setManagedObject given theKey:strKey, theValue:someValue
	local objMO
	
	set objMO to item 1 of (call method "selectedObjects" of getRecipe_OC())
	call method "willChangeValueForKey:" of objMO with parameter strKey
	call method "setPrimitiveValue:forKey:" of objMO with parameters {someValue, strKey}
	call method "didChangeValueForKey:" of objMO with parameter strKey
       --willChangeValueForKey: and didChangeValueForKey: *must* be called to let the ManagedObjectContext know that the persistent store must be updated.
end setManagedObject

Have fun!