I did a little proof of concept with the back button on Echo2. I don't have it cleaned up, but it (barely) works in FireFox. I thought I'd post it here, clean it up, and post it again later.
I used the following components:
echopointng.command.JavaScriptEval
echopointng.command.JavaScriptInclude
http://www.unfocus.com/Projects/HistoryKeeper
HistoryKeeper is a very simple AJAX history library. I used JavaScriptInclude to include it and my own javascript file. Anytime an action occurs that should be remembered as a history event, a JavaScript function (add) is called. I have 2 buttons (that I plan to hide in the future) that act as forward and back buttons within the Echo application. (This all means it will be up to the application programmer to provide forward/back behaviour). When HistoryKeeper detects a history change event, the appropriate button is clicked (calling getElementById().dispatchEvent())
I'll likely improve it a bit before posting the final code. I might use a different history framework. (unfocus is the simplest one I could find.) For now here are some snippets:
Add to Application.init()
enqueueCommand(new JavaScriptInclude("unfocus/unFocusHistory-p.js"));
enqueueCommand(new JavaScriptInclude("browserhistory.js"));
application.enqueueCommand(new JavaScriptEval("historyManager.add();"));
enqueueCommand(new JavaScriptEval("historyManager.setBackButtonId('c_" + backButton.getRenderId() + "');"));
enqueueCommand(new JavaScriptEval("historyManager.setForwardButtonId('c_" + forwardButton.getRenderId() + "');"));
browserhistory.js:
function clickButton(id)
{
var event = document.createEvent("MouseEvents");
event.initEvent("click", true, true);
document.getElementById(id).dispatchEvent(event);
}
function HistoryManager()
{
var state = 0;
var maxState = 0;
var backButtonId;
var forwardButtonId;
this.add = function()
{
state++;
unFocus.History.addHistory(state);
}
this.setBackButtonId = function(id)
{
backButtonId = id;
}
this.setForwardButtonId = function(id)
{
forwardButtonId= id;
}
this.historyListener = function(historyHash)
{
//Don't do anything if we are just adding a new state.
if(historyHash <= maxState)
{
//TODO: Don't go here on first hit.
if(historyHash < state)
{
clickButton(backButtonId);
}
else if(historyHash > state)
{
//clickButton(forwardButtonId);
}
else alert('invalid state jump');
//TODO: forward, jump around (use a text field)
}
else
{
maxState = historyHash; //TODO: move to add()
}
state = historyHash;
};
unFocus.History.addEventListener('historyChange', this.historyListener);
this.historyListener(unFocus.History.getCurrent());
};
var historyManager = new HistoryManager();
Can it be made to work by plugging into the browsers real back and forward button?
I'm sorry, I don't think I exlpained it very well. It DOES plug into the real browser back button. When you click on the real browser back button, the ActionListener of your own (Echo2) back button will fire.
The need for a back button on your page is something of a hack. I needed some way to fire a Java event and I don't understand the Echo2 codebase well enough to figure out anything else.
I plan to encapsulate all of this into a single Java file to make it easier to integrate.
Excellent. Will it work in IE?
I would suggest you create a component called somehting like HistoryManager. There would only need to be one history manager per app instance added.
Then you could encapsulate the 2 JS files into one and serve them via the rendering peer and its JS sending capability.
This component would not have a visible UI pe se but rather provide a skeleton into which events (lets called them HistoryEvents) could be raised.
The ExternalEventMonitor component in EPNg is a bit like this. No UI but it does raise events.
I haven't tested it in IE. I know the HistoryKeeper JS library works, but I didn't test my own code with IE yet. Since I don't have much code, I'm sure it won't be hard to fix any IE issues that may exist.
About your suggestions:
To be honest and as I said in my original email, I don't know the Echo2 codebase very well. It's very complex. I'll likely do what works and ask others to help me improve it. Until I get a deeper understanding, I am still planning on using small or hidden forward/back components. Although, I'll likely encapsulate as much pluming as possible. Here is how the API may end up:
class HistoryManager { HistoryManager(Application app, AbstractButton backButton, AbstractButton forwardButton) void addHistoryState(HistoryState hs) void back(); void forward(); } interface HistoryState { void undo(); void redo(); }Whenever you change the state of the application, you encapsulate that change (redo) and the reverse of that change (undo) in a HistoryState and add it to the HistoryManager.
I'm working on other things so it's hard to find the time to finish this thing off. It's not much code or very complex.
I have created a HistoryMonitor component in EPNG. Its in the lastest CVS.
Its based on Mikes code above and comes from the unFocus JS toolkit. (The code looks complete if somewhat ugly!)
It works on FireFox but I am having troubles on IE (ah got it working). I have tried to design it so that we can replace the actual history saving mechanism without changing the componentry.
I am not quite happy with the Java design at the moment. I would like some feedback on this.
We start with a HistoryMonitor component. Normally you would have one component per web app instance.
It has HistoryEventListeners which use HistoryEvents as their paremeters.
The key to this is a HistoryUndoRedo interface. It is a simple object that is "added" to the HistoryMonitor when the state of the application changes and is meant to encapsulte the "undo" and "redo" steps required to go back and forward.
At present it has a getStateCounter() which is ever increasing number used to represent a bit of state This ends up as an anchor link in the browser.
What I would like to do eventually is remove this method and have a marker interface only. The "magic" numbers would be managed by the framework and "mapped" back to the actual HistoryUndoRedo objects.
Perhaps another solution would be to have a getEncodedState() mechanism to allow the developer to specify "encoded" state so that if the user bookmarks the page, they may have a chance at "re-instating" the app state when the app first starts next time.
Again I would like to thank Mike for his intial contribution. I hope we can get something useful up and going.
Here is the code from the EPNg test app I have so far. This is still a work in progress and is far from finished or polished.
static class HistoryUndoRedoImpl implements HistoryUndoRedo { private static int globalStateCount = 0; private int stateCounter; private String undoValue; private String redoValue; /** * Constructs a <code>TestXtra.HistoryUndoRedoImpl</code> */ public HistoryUndoRedoImpl(String undoValue, String redoValue) { synchronized (HistoryUndoRedoImpl.class) { globalStateCount++; this.stateCounter = globalStateCount; } this.undoValue = undoValue; this.redoValue = redoValue; } public int getStateCounter() { return stateCounter; } public String getRedoValue() { return redoValue; } public void setRedoValue(String redoValue) { this.redoValue = redoValue; } public String getUndoValue() { return undoValue; } public void setUndoValue(String undoValue) { this.undoValue = undoValue; } public String toString() { return super.toString(); } } public Component testHistoryMonitor() { final HistoryMonitor historyMonitor = new HistoryMonitor(); HistoryUndoRedoImpl undoRedo = new HistoryUndoRedoImpl(null," state of the text field"); historyMonitor.addHistory(undoRedo); final TextField textField1 = new TextField(new StringDocument(),undoRedo.getRedoValue(),30); ButtonEx buttonUpdateState = new ButtonEx("Cause Application State Update to occur!"); buttonUpdateState.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { String currentText = textField1.getText(); String currentTime = new SimpleDateFormat("hh:mm:ss").format(new Date()); currentTime += " " + RandKit.roll(new String[] { "Roses"," Daffodils", "Hydranges", "Strezlixia" }); textField1.setText(currentTime); HistoryUndoRedoImpl undoRedo = new HistoryUndoRedoImpl(currentText,currentTime); historyMonitor.addHistory(undoRedo); } }); historyMonitor.addHistoryEventListener(new HistoryEventListener() { public void onRedo(HistoryEvent historyEvent) { HistoryUndoRedoImpl undoRedo = (HistoryUndoRedoImpl) historyEvent.getUndoRedo(); textField1.setText(undoRedo.getRedoValue()); } public void onUndo(HistoryEvent historyEvent) { HistoryUndoRedoImpl undoRedo = (HistoryUndoRedoImpl) historyEvent.getUndoRedo(); textField1.setText(undoRedo.getUndoValue()); } }); LabelEx explanation = new LabelEx("Each time you click the button, the application state (for the" + " textfield anyway) will be saved and added as a HistoryundoRedo object encapulating" + " that state. The when you press the back and forward button on the browser these" + " values will be retreived and re-instated to undo and redo application state."); explanation.setLineWrap(true); Column cell = new Column(); cell.add(historyMonitor); cell.add(explanation); cell.add(textField1); cell.add(buttonUpdateState); return cell; }Good work! It feels great to have contributed.
About design:
In your the code example HistoryUndoRedo is just a place to store data. I am thinking a better design of HistoryUndoRedo would be as a command pattern. This eliminates the need for (add)HistoryEventListener and makes for a cleaner design (IMO).
Here is the code rewritten to reflect my thoughts (notice how much it shrank):
static class TimeTextUndoRedo implements HistoryUndoRedo { TextField textField; String undoText; public HistoryUndoRedoImpl(TextField aTextField) { testField = aTextField; } public void redo() { //should redo be renamed do or run? undoText = textField.getText(); String currentTime = new SimpleDateFormat("hh:mm:ss").format(new Date()); currentTime += " " + RandKit.roll(new String[] { "Roses"," Daffodils", "Hydranges", "Strezlixia" }); textField.setText(currentTime); } public void undo() { textField.setText(undoText); } } public Component testHistoryMonitor() { final HistoryMonitor historyMonitor = new HistoryMonitor(); final TextField textField1 = new TextField(new StringDocument(),"Original text of the field",30); ButtonEx buttonUpdateState = new ButtonEx("Cause Application State Update to occur!"); buttonUpdateState.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { TimeTextUndoRedo undoRedo = new TimeTextUndoRedo(textField1); undoRedo.redo(); //Should add history automatically call redo? historyMonitor.addHistory(undoRedo); } }); LabelEx explanation = new LabelEx("Each time you click the button, the ACTION to change the" "application state (for the" + " textfield anyway) will be saved and added as a HistoryundoRedo object encapulating" + " that ACTION. The when you press the back and forward button on the browser these" + " THE REVERSE OR RERUN OF THE ACTION WILL RUN."); explanation.setLineWrap(true); Column cell = new Column(); cell.add(historyMonitor); cell.add(explanation); cell.add(textField1); return cell; }So are you saying that using the Command pattern, the HistoryMonitor would be responsible for running the redo() or undo() methods rather than informaing a listener and have it do it.
I dont mids that per se but I figure people might want to know when the a back history event has occur for other reasons and hence the listener.
Or how about a secondary interface called HistoryUndoRedoCommand with the undo() and redo() methods.
Then if the HistoryUndoRedo (which might have to be renamed to say HistoryState) implements HistoryUndoRedoCommand then the code would invoke it directly as well as call any listeners attached. That way the default code pattern would be without listeners but they could be added if required.
What do you think?
interface HistoryState { public String historyHash(); } interface HistoryUndoRedo extends HistoryState { public void undo(); public void redo(); }Looking at this you would get the best of bother worlds! (funny how you see things when you express it as code instead of words)
I like that design. You get the best of both worlds.
It's also closer to GWT's design (but without the undo redo funcationality):
http://code.google.com/webtoolkit/document...nt.History.html
Dojo and Wings appear to only care about the back button, however:
http://dojotoolkit.org/intro_to_dojo_io.html#id6
http://doc.j-wings.org/wings-doc/userguide...04.html#d0e2934
[url=http://doc.j-wings.org/wings-doc/api/org/wings/SFrame.html#setBackButton(org.wings.SButton)]http://doc.j-wings.org/wings-doc/api/org/w....wings.SButton)[/url]
Its in CVS now. The classes are
HistoryMonitor
- the component that owns all this history. Probably on have one per web app since you only have one back and forward button.
HistoryState - a simple interface that generates the "history hash" for use within the browser URL
HistoryUndoRedo - the command pattern extenstion of HistoryState
HistoryEventListener - a listener interface for when history events occur. eg they pressed the back or forward button
HistoryEvent - a wrapper of the HistoryState/HistoryUndoRedo objects to the listener
The code from EPNG test look like this now :
public Component testHistoryMonitor() { DefaultTableColumnModel columnModel = new DefaultTableColumnModel(); columnModel.addColumn(new TableColumnEx(0, "index")); columnModel.addColumn(new TableColumnEx(1, "historyHash")); columnModel.addColumn(new TableColumnEx(2, "toString()")); final DefaultTableModel historyTableModel = new DefaultTableModel(3, 0); final TableEx historyTable = new TableEx(historyTableModel, columnModel); historyTable.setSelectionEnabled(true); historyTable.setBorder(BorderEx.DEFAULT); final LabelEx explanation = new LabelEx("Each time you click the button, the application state (for the" + " textfield anyway) will be saved and added as a HistoryUndoRedo object encapulating" + " that state. Then when you press the back and forward button on the browser these" + " values will be retreived and re-instated to undo and redo application state."); explanation.setLineWrap(true); final Label eventLabel = new LabelEx(); eventLabel.setForeground(Color.BLUE); final HistoryMonitor historyMonitor = new HistoryMonitor(); final TextField textField1 = new TextField(new StringDocument(), "Initial value", 30); ButtonEx buttonUpdateState = new ButtonEx("Cause Application State Update To Occur!"); buttonUpdateState.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { HistoryUndoRedo historyUndoRedo = new HistoryUndoRedo() { private String undoValue = textField1.getText(); private String redoValue = genNewText(); private String genNewText() { String newText = new SimpleDateFormat("hh:mm:ss").format(new Date()); newText += " " + RandKit.roll(new String[] { "Roses", " Daffodils", "Hydranges", "Strezlixia", "ButterCups", "Lillies", "Azalias", "Rodademdrems", "Banksias", "Warratahs" }); return newText; } public String historyHash() { return String.valueOf(hashCode()); } public void redo() { textField1.setText(redoValue); } public void undo() { textField1.setText(undoValue); } public String toString() { StringBuffer buf = new StringBuffer(); buf.append("undo:["); buf.append(String.valueOf(undoValue)); buf.append("] redo:["); buf.append(String.valueOf(redoValue)); buf.append("]"); return buf.toString(); } }; historyUndoRedo.redo(); historyMonitor.addHistory(historyUndoRedo); updateHistoryTable(historyTableModel, historyTable, historyMonitor); } }); historyMonitor.addHistoryEventListener(new HistoryEventListener() { public void onRedo(HistoryEvent historyEvent) { eventLabel.setText("History Event : Redo event occurred"); updateHistoryTable(historyTableModel, historyTable, historyMonitor); } public void onUndo(HistoryEvent historyEvent) { eventLabel.setText("History Event : Undo event occurred"); updateHistoryTable(historyTableModel, historyTable, historyMonitor); } public void onNoHistoryAvailable(HistoryEvent historyEvent) { eventLabel.setText("History Event : No history available in HistoryMonitor"); updateHistoryTable(historyTableModel, historyTable, historyMonitor); } }); Column cell = new Column(); cell.add(historyMonitor); cell.add(explanation); cell.add(textField1); cell.add(buttonUpdateState); cell.add(eventLabel); cell.add(historyTable); return cell; }HistoryMonitor in Echo3
This HistoryMonitor sounds like a really useful component. It would alleviate some confusion if my users could just use the browser back (and forward) button.
From the other posts, it seems that there is some confusion about how to add the component to a project. Was this question resolved for anyone?
And since I'm now out of echo2, is there a version of the component that works in echo3?
Hopefully,
J
Brad (or anyone),
Is the HistoryMonitor component complete and working? How do you plug it into the browsers BACK and FORWARD buttons?
I’m trying to use your HistoryMonitor components to support the BACK and FORWARD functionality offered by the web browser, but I cannot get it to work.
In my ApplicationInstance class, I define the HistoryMonitor object as a property on the class like so..
private HistoryMonitor pHistory = new HistoryMonitor();In my ApplicationInstance class I also have a method where I place the ContentPane in the Window component returned from the init() method. In this method, I add my HistoryUndoRedo implementation to the HistoryMonitor.
/** Sets the ContentPane to the Window component within this application instance. */ public void setContent(final ContentPane aPane) { ContentPane undo = pWindow.getContent(); HistoryUndoRedo ur = new DagHistoryUndoRedo(this, undo, aPane); pHistory.addHistory(ur); pWindow.setContent(aPane); }As the user navigates from screen to screen in my application, the setContent(ContentPane) method is called, which adds to the HistoryMonitor.
I have an implementation of the HistoryEventListener but I'm not sure what the purpose of a listener would be. I say this because the JavaDocs for the HistoryMonitor class states, "If the HistoryState object also implements HistoryUndoRedo, then the appropriate undo() or redo() method will be called when a history event occurs. NOTE : These methods will be called before the HistoryEventListeners are invoked. " From that it would appear as though the HistoryMonitor class will call the HistoryUndoRedo methods. If that is the case, what would one do in an event listener?
Here is my HistoryUndoRedo implementation... (DagApp is my extension of the ApplicationInstance)
public class DagHistoryUndoRedo implements HistoryUndoRedo { private DagApp pApplInstance; private ContentPane pUndoPane; private ContentPane pRedoPane; public DagHistoryUndoRedo(final DagApp aApplInstance, final ContentPane aUndoPane, final ContentPane aRedoPane) { pApplInstance = aApplInstance; pUndoPane = aUndoPane; pRedoPane = aRedoPane; } public void redo() { System.out.println("**** REDO *****"); if (pRedoPane != null) { pApplInstance.setContent(pRedoPane); } } public void undo() { System.out.println("**** UNDO *****"); if (pUndoPane != null) { pApplInstance.setContent(pUndoPane); } } public String historyHash() { return String.valueOf(hashCode()); } }Can anyone help me to get the HistoryMonitor to work when the user clicks the browsers BACK and FORWARD buttons? What am I missing/doing wrong?
Appreciate any assistance you or anyone can provide?
-Doug
Hi Doug, Did you get this
Hi Doug,
Did you get this resolved? I am running into the same issue at the moment and was wondering if I could get some help. I see very few threads on browser history and was wondering how this is typically addressed or is there some kind of best practices?
Thanks!
I had the same issue. I discovered that the HistoryMonitor must be part of the contents (Add this component to the contentpane) to be functional.
Since I added the HistoryMonitor to
myApplicationInstance.getMainwindow().getContentPane()it seems to work...I have the same
I have the same issue...
I've tried adding the historymonitor
if (historyMonitor == null) { historyMonitor = new HistoryMonitor(); } ContentPane undo = getDefaultWindow().getContent(); HistoryUndoRedo ur = new CustomHistoryUndoRedo(this, undo, aPane); historyMonitor.addHistory(ur); aPane.add(historyMonitor); getDefaultWindow().setContent(aPane);nextapp.echo2.app.IllegalChildException: Child "echopointng.HistoryMonitor@9e5f0" illegally added to component "nextapp.echo2.app.ContentPane@1a07791"
How exactly do I plug the HistoryMonitor to the Back and Forward buttons? Thanks a lot!