Skip to content
Robert Silverton edited this page Jul 3, 2014 · 4 revisions

View this tutorial's changes on GitHub.

If you're working with the tutorial repo, open a Git Shell in the CoreEditor-HelloWorld-Example-as folder and run the following command:

git checkout -f step-9

Otherwise, follow the instructions below.


Note: This tutorial requires you to add a reference to CoreAppEx in the ActionScript Build Path for your HelloWorldExtension project.


About Editors

In your StringListContext you’ve got a fairly complex little mini-application. It has a view and controller (the View and Context), and the model is tied up in the Context’s dataProvider property. What if we wanted to be able to save the DOM of our Context, and reload it at a later date?

What we are starting to talk about here are Editors. At a code level, Editors are really no different from the VisualContexts you’ve seen so far. Instead of implementing the IVisualContext interface they implement IEditorContext (which itself extends IVisualContext). It’s this interface that informs the app that this Context can save/load data.

Despite these similarities, at a higher level, Editors differ from VisualContexts in a few important ways:

  • Multiple instances of an Editor can exist at any one time. Whereas IVisualContexts only ever have one.
  • The View associated with an Editor is placed by the IGlobalViewContainer in the larger TabNavigator on the left of the screen, and typically has access to a larger share of the available screen space.

Before we update our StringListContext to implement IEditorContext, lets first have a quick look at this interface so we can see what we’re adding.

package core.editor.contexts
{
	import flash.events.IEventDispatcher;
	
	import core.appEx.core.contexts.IVisualContext;
	import core.app.entities.URI;
	
	[Event( type="flash.events.Event", name="change" )];
	
	public interface IEditorContext extends IEventDispatcher, IVisualContext
	{
		function get uri():URI;
		function set uri( value:URI ):void;
		function save():void;
		function publish():void;
		function load():void;
		function set changed(value:Boolean):void;
		function get changed():Boolean;
		function set isNewFile(value:Boolean):void;
		function get isNewFile():Boolean;
		function enable():void;
		function disable():void;
	}
}

The first change is that the Context must now be an EventDispatcher. This is so it can dispatch a CHANGE event as specified in the [Event] metadata.

We’ve got 2 new properties that are accessible via getters/setters. The first is ‘uri’. A URI is a class stolen from Adobe’s core lib, and in many ways it is simply a wrapped up String, but with a load of handy methods for manipulating the underlying path. When specifying file locations within CoreApp you must use URIs. You can’t get away with just using strings. The uri property on this Editor essentially links it with its data on disk.

We’ve also got a getter/setter for a ‘changed’ property. This value should be set to true when something in the editor’s data changes. This allows the application to detect that this Editor is a candidate for being (re)saved. In many applications, a file that has been changed and is able to be saved has a * next to its filename. Otherwise the application blocks you from using the Save command. CoreEditor also adheres to this convention.

We’ve also got a pair of enable/disable methods. These are automatically called by the framework when gaining/losing focus respectively. We can ignore these for now, as our Context doesn’t do anything sufficiently CPU intensive to warrant being disabled when losing focus.

Finally we’ve got the important save()/load() pair of methods. How these are implemented is explained in further detail below.


Adding Your Own Editor

Let’s update out StringListContext to implement the IEditorContext.

package helloWorld.contexts
{
	import flash.display.DisplayObject;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	
	import core.app.CoreApp;
	import core.app.entities.URI;
	import core.app.operations.ReadFileAndDeserializeOperation;
	import core.appEx.core.contexts.IOperationManagerContext;
	import core.appEx.events.OperationManagerEvent;
	import core.appEx.managers.OperationManager;
	import core.appEx.operations.SerializeAndWriteFileOperation;
	import core.data.ArrayCollection;
	import core.editor.CoreEditor;
	import core.editor.contexts.IEditorContext;
	
	import helloWorld.ui.views.StringListView;

	[Event( type="flash.events.Event", name="change" )]
	
	public class StringListContext extends EventDispatcher implements IEditorContext, IOperationManagerContext
	{
		private var _view			:StringListView;
		
		private var _dataProvider		:ArrayCollection;
		private var _operationManager		:OperationManager;
		
		private var _uri			:URI;
		private var _changed			:Boolean = false;
		protected var _isNewFile		:Boolean = false;
		
		public function StringListContext()
		{
			_view = new StringListView();
			
			_operationManager = new OperationManager();
			_operationManager.addEventListener(OperationManagerEvent.CHANGE, changeOperationManagerHandler);
			_dataProvider = new ArrayCollection();
			
			_view.dataProvider = _dataProvider;
		}
		
		public function get view():DisplayObject
		{
			return _view;
		}
		
		public function dispose():void
		{
			_operationManager.removeEventListener(OperationManagerEvent.CHANGE, changeOperationManagerHandler);
			_operationManager.dispose();
		}
		
		public function enable():void
		{
			// Do nothing
		}
		
		public function disable():void
		{
			// Do nothing
		}
		
		public function publish():void
		{
			// Do nothing
		}
		
		public function save():void
		{
			var serializeOperation:SerializeAndWriteFileOperation = new SerializeAndWriteFileOperation( _dataProvider, _uri, CoreApp.fileSystemProvider );
			CoreEditor.operationManager.addOperation(serializeOperation);
			changed = false;
		}
		
		public function load():void
		{
			var deserializeOperation:ReadFileAndDeserializeOperation = new ReadFileAndDeserializeOperation( _uri, CoreApp.fileSystemProvider );
			deserializeOperation.addEventListener(Event.COMPLETE, deserializeCompleteHandler);
			CoreEditor.operationManager.addOperation(deserializeOperation); 
		}
		
		private function deserializeCompleteHandler( event:Event ):void
		{
			var deserializeOperation:ReadFileAndDeserializeOperation = ReadFileAndDeserializeOperation(event.target);
			
			// Handle case where the file on disk is empty. This occurs when we're opening a newly created file.
			if ( deserializeOperation.getResult() == null )
			{
				_dataProvider = new ArrayCollection();
			}
			else
			{
				_dataProvider = ArrayCollection( deserializeOperation.getResult() );
			}
			_view.dataProvider = _dataProvider;
			changed = false;
		}
		
		public function set uri( value:URI ):void
		{
			_uri = value;
		}
		
		public function get uri():URI { return _uri; }
		
		public function set changed( value:Boolean ):void
		{
			if ( value == _changed ) return;
			_changed = value;
			dispatchEvent( new Event( Event.CHANGE) );
		}
		
		public function get changed():Boolean { return _changed; }
		
		private function changeOperationManagerHandler( event:OperationManagerEvent ):void
		{
			changed = true;
		}
		
		public function set isNewFile( value:Boolean ):void
		{
			_isNewFile = value;
		}

		public function get isNewFile():Boolean { return _isNewFile; } 
				
		public function get dataProvider():ArrayCollection { return _dataProvider; }
		
		public function get operationManager():OperationManager { return _operationManager; }
	}
}

Hopefully the implementation of the get/set uri and get/set changed are fairly self-explanatory. The only real trick is to make sure a CHANGE event is dispatched whenever ‘changed’ is set to ‘true’. This allows the framework to detect when your Editor is eligible for a (re)save.

You’ll notice that we’re adding an OperationManagerEvent.CHANGE event listener to our operationManager. The handler simply sets the Context’s ‘changed’ parameter to true. This ensures that whenever any code performs changes to our Context via its OperationManager we automatically get the changed property set to true. This might seem a bit cheap, but when you think about it pretty much any code that changes a Context's history via its OperationManager is generally changing the underlying DOM.

First lets look at the save() method. We’re using a new Operation here. It’s a fairly complex operation that makes use of the Serializer class (which we won’t go into here, in short it serializes any DOM into XML and vice versa).

The SerializeAndWriteFileOperation takes 3 parameters, the item you want to serialize, the location (uri) of where you want to save it, and finally a reference to a FileSystemProvider. We won’t talk about the FileSystemProvider at length here, but briefly, it is the class that abstracts away the data input/output layer of an application, and you can grab a reference to it on the CoreEditor API.

Next we add this Operation to the ‘global’ OperationManager on the CoreApp API. We haven’t used this instance of an OperationManager on the API before. Why are we using this, and not the one on our StringListContext?


The Global OperationManager

This requires a bit of explanation, as it’s something that only really becomes apparent after using undoable Operations for a while. In order to maintain a history on a Context, all operations added to it must be undo-able. Adding a non-undoable Operation to a history will clear any operations before it. This is a subtle concept to grasp, but it’s important. If an Operation is not undoable, then it is logically impossible for the preceding operations to ever have their undo() method called.

Sometimes this is unavoidable – or the operation in question is just particularly difficult to convert into being undo-able. This behaviour manifests itself in many other applications. You know it when you see an alert box with something along the lines of ‘Are you sure wish to perform X?. This Operation is not undoable’.

Saving bytes to a file system isn’t an undoable operation. So rather than adding our SerializeAndWriteFileOperation to our local OperationManager, we simply add it to the global one. The operation will still be executed in the same fashion, and we get to keep our local undo history.

You may also be wondering why we add it to an OperationManager at all. Why not just call the operation's execute() method directly and be done with it? While it's perfectly valid to do this, you'll miss out on some benefits. Namely, if added to either the global OperationManager, or any OperationManager on any Context, there is some code contributed by the CoreApp_Extensions project that will disable further user interaction and show the progress of the operation at the bottom of the screen.


Loading Data

Coming back to the load() method, loading the file is a tiny bit more involved. Serializing and Deserializing are asynchronous Operations. So we add a COMPLETE event listener to the operation before we hand it over to the global OperationManager. Once completed, we check to see if the read file operation's result is null. This is a valid result for the operation to return, as the file it is reading may be a completely empty file on disk just created by the IDE. In this case we make sure we initialise a fresh ArrayCollection.

However if the result is not null we cast it to a ArrayCollection. We are safe to cast this value as a DataSet, because if the returned object wasn’t of this type, then there would be a fairly serious bug in the Serializer.


And there we have it. Our Context is now capable of saving and retrieving its state from the file system. However if you build and run the application now, you’ll see some slightly strange behaviour. Chances are your StringListView will still appear on the right side. This is because the application’s preferences have been saved with your Context as a IVisualContext, rather than a IEditorContext. If you close this panel then close the application, your preferences will be resaved and you won’t see this again.

You’ll also notice that you can’t open your Context from the Window menu anymore. This is because it no longer behaves like a IVisualContext, and so hasn’t been treated as such. If we want to see our Context again we’re going to have to create our own file type…

Note: Trying to save a file at this stage will cause a runtime error. Proceed to the next tutorial in order to enable saving.


< Previous | Next >