help with linking data

Hi everyone,

I am very new to Cocoa. However, I am willing and eager to solve this problem on my own. All I want to know is the “way” to solve it.

There are some countries (the number of countries will keep increasing).
Each country has some cities (the number of cities is not constant)
Each city has a river (again, (the number of rivers will keep increasing)

I want that if I select a country “A” in ComboBox1/PopUpButton1, then ComboBox2/PopUpButton2 must show a list of cities belonging to country “A” only and not other countries. Likewise, after a city is selected, only the rivers belonging to that city must be seen in ComboBox3/PopUpButton3.

Please, tell me the basics like how I must create my arrays, dictionaries and should I use ComboBox or PopUpButton?

Thanks.

That depends on whether you want the user to be able to enter new values.

yes, the user must be able to enter new values and they should be available for auto-completion next time.

I’m bound to say you’ve picked a fairly complex place to start. My suggestion would be to start off by making an array of countries and getting one combobox working how you want. Then move on to adding others. There’s really no one right way – it depends a bit on what you are doing with the data, and so on.

Thanks for the reply, Shane.
To simplify the problem, let us say I do not want to add new items and I shall use pop up buttons for countries and cities.
I have a country array and cities array containing country and city names respectively.
How do I make them talk to each other? If one country is selected in countryPopUpButton, how do I have only the cities belonging to that country available in the cityPopUpButton. Do I have to create a mess of arrays such that each country has its own city array?

(Would learning how to use “Relationships” in Core Data help with such kind of problem?)

I was under the impression AppleScriptObjC can’t use core data.

“a mess of arrays” also known as a two dimensional array is probably the way to go about it. Having to deal with two dimensional arrays is not as bad as it sounds.

I’m learning ObjC at the moment. This strikes me as a typical ObjC learning project where you’d create a river class, a city class and a country class.

The simplest method would be to have one array of countries, and a matching one containing arrays of the cities in each. Then choosing a menu item would trigger an action handler that found the selection, and changed the other menu accordingly.

That’s simple, but it might prove limiting later on. You might be better to use records/dictionaries instead of arrays.

Not unless you want to abandon AS.

That probably is the typical Objective-C approach, but depending on what you want to do with the result, it can sometimes just be a recipe for more work.

I know how to create dictionaries. But I am not sure how dictionaries would help in this situation.
Can you please elaborate?

I’m not sure what it is you want to do with this project, but I think the ability to have the choice in one list control what you see in another list is a general problem of interest, so I looked at how to implement this in ASOC. I have gotten as far as a project that has to combo boxes for countries and cities. I am using bindings to populate the combo box lists and to get the value chosen or typed in. So, the Content Values of the countries combo box is bound to Combos App Delegate with the property, theCountries, as the Model Key Path. Likewise, the city combo box has the property, theCities as its Model Key Path. I also have the properties countryChoice and cityChoice bound to the Value of the combo boxes, so these properties will reflect what is in the combo box text field, whether that was populated by typing in a new value or gotten by choosing from the list. The state of the combo boxes is set to “continuous” and the “Uses external Data Source” check box is left unchecked. The methods, choseCountry_ and choseCity_ , are connected to the country and city combo boxes respectively. I’m not sure if there are easier ways to implement this, but this is what I have so far:

script CombosAppDelegate
	property parent : class "NSObject"
	property NSMutableDictionary : class "NSMutableDictionary"
	property pathToRoot : POSIX path of ((path to documents folder as string) & "Geography:")
	property theCountries : missing value
	property theCities : missing value
	property countryChoice : missing value
	property cityChoice : missing value
	property allCities : {}
	
	on applicationWillFinishLaunching_(aNotification)
		set my allCities to NSMutableDictionary's alloc()'s initWithContentsOfFile_(pathToRoot & "Geography.plist")
		if allCities is equal to missing value then
			set my allCities to NSMutableDictionary's dictionaryWithObjects_forKeys_({{"New York", "Los Angeles", "Chicago"}, {"Vancouver", "Ottawa", "Calgary"}}, {"United States", "Canada"})
		end if
		set my theCountries to allCities's allKeys() as list
		writeToFile()
	end applicationWillFinishLaunching_
	
	on choseCountry_(sender)
		if theCountries does not contain countryChoice then
			if countryChoice is not missing value then
				set my theCountries to theCountries & countryChoice
				allCities's setObject_forKey_({}, countryChoice) --add a new key with an empty list for the object
				writeToFile()
			end if
		end if
		set my theCities to allCities's objectForKey_(countryChoice) as list
	end choseCountry_
	
	on choseCity_(sender)
		if theCities does not contain (cityChoice as text) then
			if cityChoice is not missing value then
				set my theCities to ((allCities's objectForKey_(countryChoice) as list) & cityChoice)
				allCities's setObject_forKey_(theCities, countryChoice)
				writeToFile()
			end if
		end if
	end choseCity_
	
	on writeToFile()
		set myFile to pathToRoot & "Geography.plist"
		if not allCities's writeToFile_atomically_(myFile, true) then
			set messageText to "There was an error writing to file"
			display dialog messageText buttons {"Ok"} default button 1
		end if
	end writeToFile
	
end script

Ric

Nice one, Ric. Allow me to take it a step further and add sorting of entries:

	on applicationWillFinishLaunching_(aNotification)
		set my allCities to NSMutableDictionary's alloc()'s initWithContentsOfFile_(pathToRoot & "Geography.plist")
		if allCities is equal to missing value then
			set my allCities to NSMutableDictionary's dictionaryWithObjects_forKeys_({{"New York", "Los Angeles", "Chicago"}, {"Vancouver", "Ottawa", "Calgary"}}, {"United States", "Canada"})
		end if
		set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
		writeToFile()
	end applicationWillFinishLaunching_
	
	on choseCountry_(sender)
		if (theCountries's containsObject_(countryChoice)) as boolean is false then
			if countryChoice is not missing value then
				tell allCities to setObject_forKey_({}, countryChoice) --add a new entry with an empty list for the object
				set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
				writeToFile()
			end if
		end if
		set my theCities to allCities's objectForKey_(countryChoice)'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
	end choseCountry_
	
	on choseCity_(sender)
		if (theCities's containsObject_(cityChoice)) as boolean is false then
			if cityChoice is not missing value then
				set oldCities to allCities's objectForKey_(countryChoice) -- get existing list
				set oldCities to oldCities's arrayByAddingObject_(cityChoice) -- add new city
				set my theCities to oldCities's sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:") -- sort
				tell allCities to setObject_forKey_(theCities, countryChoice) -- replace existing entry
				writeToFile()
			end if
		end if
	end choseCity_

Many thanks Ric. Hopefully, I could extend this to “Rivers” Combo Box.

Good luck Rounak.

Thanks Shane for the extension of the project – I wanted to do the sorting, but I didn’t really know how to do that. I tried adding the sorting to my code but it didn’t work, I think because the way I implemented “theCountries” and “theCities” they were AS lists instead of NSArrays, so I adopted your code there. I also changed the order of the “if” statements in the choseCities_ method to fix an error that occurred if you clicked out of the countries combo box without choosing or typing anything.
One other error occurred if you had a city name showing in the combo box and then went back to the country combo box and changed the country – the city from the previously chosen country was added to the new country’s list of cities. I fixed that by adding the “set cityChoice to missing value” line in the choseCountry method. So, the code that seems to work well now is :

script CombosAppDelegate
	property parent : class "NSObject"
	property NSMutableDictionary : class "NSMutableDictionary"
	property pathToRoot : POSIX path of ((path to documents folder as string) & "Geography:")
	property theCountries : missing value
	property theCities : missing value
	property countryChoice : missing value
	property cityChoice : missing value
	property allCities : {}
	
	on applicationWillFinishLaunching_(aNotification)
		set my allCities to NSMutableDictionary's alloc()'s initWithContentsOfFile_(pathToRoot & "Country.plist")
		if allCities is equal to missing value then
			set my allCities to NSMutableDictionary's dictionaryWithObjects_forKeys_({{"New York", "Los Angeles", "Chicago"}, {"Vancouver", "Ottawa", "Calgary"}}, {"United States", "Canada"})
		end if
		set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
	end applicationWillFinishLaunching_
	
	on choseCountry_(sender)
		set cityChoice to missing value
		if countryChoice is not missing value then
			if (theCountries's containsObject_(countryChoice)) as boolean is false then
				allCities's setObject_forKey_({}, countryChoice) --add a new entry with an empty list for the object
				set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
			end if
			set my theCities to allCities's objectForKey_(countryChoice)'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
		end if
	end choseCountry_
	
	on choseCity_(sender)
		if cityChoice is not missing value then
			if (theCities's containsObject_(cityChoice)) as boolean is false then
				set oldCities to allCities's objectForKey_(countryChoice) -- get existing list
				set oldCities to oldCities's arrayByAddingObject_(cityChoice) -- add new city
				set my theCities to oldCities's sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:") -- sort
				allCities's setObject_forKey_(theCities, countryChoice) -- replace existing entry
			end if
		end if
	end choseCity_
	
	on applicationShouldTerminateAfterLastWindowClosed_(sender)
		return 1
	end applicationShouldTerminateAfterLastWindowClosed_
	
	on applicationShouldTerminate_(sender)
		set myFile to pathToRoot & "Country.plist"
		if not allCities's writeToFile_atomically_(myFile, true) then
			set messageText to "There was an error writing to Country.plist"
			display dialog messageText buttons {"Ok"} default button 1
		end if
		return 1
	end applicationShouldTerminate_
	
	end script

The other thing worth considering is capitalization. I presume you have Autocompletes turned on.

Try inserting this after “if countryChoice is not missing value then” in choseCountry_(sender):

		set my countryChoice to countryChoice's capitalizedString()

and do similar for choseCity_(sender).

I’m also a it surprised you got away with “set cityChoice to missing value” without a my.

Thanks Shane for the further enhancement.

I’m not sure either why that worked either – in a further extension of the project to include a Rivers combo box, I did get the expected behavior with a river name in the text field when I changed a city name, but the name wasn’t cleared when I changed a country name ( I had “set cityChoice to missing value” and “set riverChoice to missing value” in the choseCountry_ method). So, I put the “my” in those statements, and now it works properly.

Ric

So how are you storing the data? Using city names as keys?

I created a second dictionary with cities as the keys and rivers as the objects.

Hi rdelmar,
I would like to see the updated files.

I added another dictionary, allRivers, with all the cities in the initial allCities dictionary as the keys. I also added another combo box that is bound, like the other 2, to two properties, theRivers and riverChoice. If you stop the program by closing the window, applicationShouldTerminateAfterLastWindowClosed_(sender) will get called which will, in turn, call applicationShouldTerminate_(sender) – that will write out the updated dictionaries to the 2 plists (stopping the program by clicking the stop sign icon will not call these). I also incorporated Shane’s suggestions to sort the lists and to ensure correct capitalization of the entries.

script CombosAppDelegate
	property parent : class "NSObject"
	property NSMutableDictionary : class "NSMutableDictionary"
	property pathToRoot : POSIX path of ((path to documents folder as string) & "Geography:")
	property theCountries : missing value
	property theCities : missing value
	property theRivers : missing value
	property countryChoice : missing value
	property cityChoice : missing value
	property riverChoice : missing value
	property allCities : {}
	property allRiverKeys : {}
	property allRivers : {}
	
	on applicationWillFinishLaunching_(aNotification)
		
		set my allCities to NSMutableDictionary's alloc()'s initWithContentsOfFile_(pathToRoot & "Country.plist")
		if allCities is equal to missing value then
			set my allCities to NSMutableDictionary's dictionaryWithObjects_forKeys_({{"New York", "Los Angeles", "Chicago"}, {"Vancouver", "Ottawa", "Calgary"}}, {"United States", "Canada"})
		end if
		set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
		
		
		set my allRivers to NSMutableDictionary's alloc()'s initWithContentsOfFile_(pathToRoot & "River.plist")
		if allRivers is equal to missing value then
			set allRivers to NSMutableDictionary's dictionary()
			repeat with aKey in (allCities's allKeys() as list) --Extract all the city names into a list
				set allRiverKeys to allRiverKeys & (allCities's objectForKey_(aKey) as list)
			end repeat
			repeat with aKey in allRiverKeys
				allRivers's addObject_forKey_({}, aKey) --creates empty rivers dictionary entries for all cities in the initial allCities dictionary
			end repeat
		end if
		
	end applicationWillFinishLaunching_
	
	on choseCountry_(sender)
		set my cityChoice to missing value
		set my riverChoice to missing value
		if countryChoice is not missing value then
			set my countryChoice to countryChoice's capitalizedString()
			if (theCountries's containsObject_(countryChoice)) as boolean is false then
				allCities's setObject_forKey_({}, countryChoice) --add a new entry with an empty list for the object
				set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
			end if
			set my theCities to allCities's objectForKey_(countryChoice)'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
		end if
	end choseCountry_
	
	on choseCity_(sender)
		set my riverChoice to missing value
		if cityChoice is not missing value then
			set my cityChoice to cityChoice's capitalizedString()
			if (theCities's containsObject_(cityChoice)) as boolean is false then
				set oldCities to allCities's objectForKey_(countryChoice) -- get existing list
				set oldCities to oldCities's arrayByAddingObject_(cityChoice) -- add new city
				set my theCities to oldCities's sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:") -- sort
				allCities's setObject_forKey_(theCities, countryChoice) -- replace existing entry
				allRivers's addObject_forKey_({}, cityChoice)
			end if
			set my theRivers to allRivers's objectForKey_(cityChoice)'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
		end if
	end choseCity_
	
	on choseRiver_(sender)
		if riverChoice is not missing value then
			set my riverChoice to riverChoice's capitalizedString()
			if (theRivers's containsObject_(riverChoice)) as boolean is false then
				set oldRivers to allRivers's objectForKey_(cityChoice)
				set oldRivers to oldRivers's arrayByAddingObject_(riverChoice)
				set my theRivers to oldRivers's sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
				allRivers's setObject_forKey_(theRivers, cityChoice)
			end if
		end if
	end choseRiver_
	
	on applicationShouldTerminateAfterLastWindowClosed_(sender)
		return 1
	end applicationShouldTerminateAfterLastWindowClosed_
	
	on applicationShouldTerminate_(sender)
		set myFile to pathToRoot & "Country.plist"
		if not allCities's writeToFile_atomically_(myFile, true) then
			set messageText to "There was an error writing to Country.plist"
			display dialog messageText buttons {"Ok"} default button 1
		end if
		set myFile to pathToRoot & "River.plist"
		if not allRivers's writeToFile_atomically_(myFile, true) then
			set messageText to "There was an error writing to River.plist"
			display dialog messageText buttons {"Ok"} default button 1
		end if
		return 1
	end applicationShouldTerminate_
	
end script

Ric

Thanks again.
Regarding auto-completion, if I write “C”, I get “Canada” but if I write “c”, I don’t get anything. I thought Shane’s earlier post was to fix this problem. Now, I understand that it was to auto-capitalize the first letter so that “kenya” gets stored as “Kenya”.