Support for multiple browser tabs and Bookmarking

Hi,

I’m relatively new to Vaadin and am wondering if there is a best practice for building an application that does all these things together:

  • Supports multiple browser tabs
  • Supports bookmarking
  • Supports Back Button/Browser history

Looking over the forum posts, I’ve seen various write ups about multi-tabbing. It seems the consensus is to overwrite getWindow in the main application.

Also wondering, is this the best practice with regards to the URL format for an application?: appname/windowname

Is there a way to support entering an app from a bookmark that uses a familiar format such as appname/windowname?param=value

I’ve reviewed the sampler app, but would like to avoid the way it initializes unnecessary views prior to finally rendering the requested page.
http://demo.vaadin.com/sampler/#Components/Layouts/SplitPanelBasic

I suppose this is a tall order, but am looking for an example that brings all these concepts together.

I’ve tried a small prototype but didn’t get very far. For bookmarking, I tried using the URIHandler and ParameterHandler and added it to each new window that was created in getWindow. However, when opening a new tab like this:

http://localhost::8080/appname/windowname?param=value

the relativeUri and params were available initially but on subsequent calls to getWindow, when the new window was actually getting created, relativeUri and params passed in were no longer available. So they could not then be used to initialize display items in the new window, such as selecting the appropriate row in a table based upon the parameter id passed in.

If you have any guidance or wisdom on these topics from prior experience with Vaadin, I’d really appreciate it. Thanks!

That is correct. You can use Sampler implementation as a reference.

Excellent point.

Sampler first initializes the main view and after that handles #Components/Layouts/SplitPanelBasic. Unfortunately this is a limitation in current implementation of URIFragmentUtility that will hopefully be corrected
in future
.

Now the best bet could be to have empty main view - only have URIFragmentUtility in that and forward everyone to some specific http://mycorp.com/app/#main url that would then show the main view. I think that fact that application does one extra ajax request to forward the user to #main would be invisible to end-user. In fact, Sampler should probably be implemented also this way.

I would suggest to avoid mixing get parameters with uri fragments and design the urls like this:

http://localhost::8080/appname/ - empty screen, forwards to http://localhost::8080/appname/#main
http://localhost::8080/appname/#screen1 - goes directly to screen 1

When user decides to open a second concurrent tab/window, the urls could look like this

http://localhost::8080/appname/window2718/ - empty screen, forwards to http://localhost::8080/appname/#main
http://localhost::8080/appname/window2718/#screen1 - goes directly to screen 1

The problem then is what to do if user bookmarks the above url? I think that our best bet would be to create this requested window2718 in getWindow() method if it doesn’t exist. It would waste one window (original main window is newer accessed), but if the main window is just empty space with URIFragmentUtility it probably doesn’t matter.

Unfortunately there is no example/tutorial that would fulfill all your feature requirements. It would be great if your could summarize your experiences in this thread or even contribute a short wiki page on it.

Joonas,

Thanks for the response and suggestion. When you talk about having an empty view and forwarding to http://mycorp.com/app/#main, can you describe a bit more about how to make that happen? Where/what method (init, getWindow()?) would the forwarding happen in, and how to detect the forwarding should occur?

By forwarding, do you mean doing this, or did you have something else in mind?

emptyWindow.open(new ExternalResource(emptyWindow.getURL().toString()+“#main”));

“Forwarding” to another fragment is actually as easy as calling URIFragmenUtility.setFragment(). Setting the fragment does not require a client-side refresh.

To implement the suggested pattern, you’d do all your view-changing and the FragmentChangedListener:


          // Warning: untested, consider as pseudo-code :-)           
          uriFragmentUtility.addListener(new FragmentChangedListener() {
                public void fragmentChanged(FragmentChangedEvent source) {
                    String fragment = source.getUriFragmentUtility().getFragment();
                    if (fragment == null) {
                        fragment = "main";
                        source.getUriFragmentUtility().setFragment(fragment,false);
                    }
                    showView(fragment);                  
                }
            });  

Best Regards,
Marc

Hi,

As this is fairly common requirement and all but trivial to implement, I decided to try implement a generic helper that would make implementing application with all the requirements easy. It turned out to be not so easy.

Incomplete implementation with use example can be found in
http://dev.vaadin.com/svn/incubator/Navigator/
.

There are currently two problems:

  • When I already have one tab open and would like to open another tab with uri fragment, the fragment is lost and the second tab opens to default view instead of the view I specified. This is probably a bug in Vaadin.
  • The current implementation of UriFragmentUtility does not submit back “no uri fragment given” event and thus we must render default view with default url (without any uri fragment). Unfortunately when I give the fragment, the default view is shown on screen briefly before the selected view appears.

I think that the both problems are fixable and should be fixed.

Before fixing these, I would like to get feedback from the API and use of Navigator helper class. If all the problems are fixed, should we even consider including Navigator in the framework?

Hi Joonas,

Thanks for looking into this further. Personally, I think if it could be made to work by fixing the two issues you mentioned in the core framework without adverse impact to existing apps, that would be preferable.

I also tried a scaled down prototype to try to get this working. I was able to work around that issue by adding the UriFragmentUtility to the new Window.

As I’m not so familiar with all the internals, I tried setting the fragment at various points in the app such as

  • getWindow
  • setting the fragment in handleURI for the Window
  • setting the fragment right away when a new Window is created.

Here’s what I got working:

Here’s what didn’t work:

Not sure if it may help, but here is the code, and the results of testing various approaches are commented throughout.



package com.example.vaadinhistorytest;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;

import com.vaadin.Application;
import com.vaadin.terminal.DownloadStream;
import com.vaadin.terminal.ExternalResource;
import com.vaadin.ui.*;
import com.vaadin.ui.UriFragmentUtility.FragmentChangedEvent;
import com.vaadin.ui.UriFragmentUtility.FragmentChangedListener;

/*
 * CONCLUSIONS:
 * OK TESTED - FIRST TAB OF SESSION IS http://localhost:8080/VaadinHistoryTest AND CLICKED ALL LINKS
 * OK TESTED - FIRST TAB OF SESSION IS http://localhost:8080/VaadinHistoryTest#screen2 AND CLICKED ALL LINKS
 * OK TESTED - FIRST TAB OF SESSION IS http://localhost:8080/VaadinHistoryTest, then open new tab with http://localhost:8080/VaadinHistoryTest#screen2
 * 	-> But click links in first tab, and header links disappear!
 * 
 * NOT WORKING, FIRST TAB OF SESSION IS http://localhost:8080/VaadinHistoryTest#screen2, then open new tab http://localhost:8080/VaadinHistoryTest
 * 	-> But click links in first tab, and header links disappear!	
 * 	-> new tab uri not changing in browser bar, content not set
 * 
 * 
 */

public class VaadinhistorytestApplication extends Application {
	
	public static final String _mainScreen = "main";
	public static final String _screen1 = "screen1";
	public static final String _screen2 = "screen2";
	public static final String _uriHash = "#";
	
	protected Component header = null;
		
	@Override
	public void init() {	
		header = this.buildHeader();
		Window w = new MyAppWindow();
		setMainWindow(w);		
		System.out.println("SET MAIN WINDOW IN init()");
	}
	
	
	public static String buildUriToken(String frag){
		return _uriHash+frag;
	}
	
	
	public Window getWindow(String windowName) {
		System.out.println("getWindow("+windowName+")");
		Window w = super.getWindow(windowName);		
		if (w!=null){			
			String token = ((MyAppWindow)w).getUriFragmentUtility().getFragment();
			System.out.println("-> Found window, token "+token);
			if (token == null){
				/*
				 * Setting the fragment here does the following:
				 * +GOOD: http://localhost:8080/VaadinHistoryTest on second tab will resolve properly to http://localhost:8080/VaadinHistoryTest#main and set content appropriately
				 * 	> NOTE: Without it, http://localhost:8080/VaadinHistoryTest on second tab doesn't get url as http://localhost:8080/VaadinHistoryTest#main and content is empty (assuming default content is not set with window creation) 
				 * -BAD: http://localhost:8080/VaadinHistoryTest#screen2 on second tab will first show main view, then replace it with screen2
				 */
				//((MyAppWindow)w).getUriFragmentUtility().setFragment(buildUriToken(_mainScreen));
				//System.out.println("--> Found window, Forced uriToken to = "+((MyAppWindow)w).getUriFragmentUtility());
			}			
		}
		else {
			w = new MyAppWindow();
			w.setName(windowName);
			System.out.println("-> Created new window "+w.getName());
			addWindow(w);			
		}		
		return w;		
	}
	
	
	private Component buildHeader(){
		final HorizontalLayout hLayout = new HorizontalLayout(); 
		hLayout.setSpacing(true);
		
		URL url = null;
		try {			
			url = new URL(getURL().toString().substring(0, getURL().toString().length()-1));
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			url = getURL();
		}
		
		Link homeLink = new Link("Home",new ExternalResource(url+"#"+_mainScreen));		
		Link screen1Link = new Link("Screen1",new ExternalResource(url+"#"+_screen1));
		Link screen2Link = new Link("Screen2",new ExternalResource(url+"#"+_screen2));
		hLayout.addComponent(homeLink);
		hLayout.addComponent(screen1Link);
		hLayout.addComponent(screen2Link);
		return hLayout;
	}
	
	private Component buildMainScreen(){
		VerticalLayout vLayout = new VerticalLayout();
		Label l = new Label("This is the MAIN content");
		vLayout.addComponent(l);		
		return vLayout;
	}
	
	private Component buildScreen1(){
		VerticalLayout vLayout = new VerticalLayout();
		Label l = new Label("This is the SCREEN 1 content");
		vLayout.addComponent(l);		
		return vLayout;
	}
	
	private Component buildScreen2(){
		VerticalLayout vLayout = new VerticalLayout();
		Label l = new Label("This is the SCREEN 2 content");
		vLayout.addComponent(l);		
		return vLayout;
	}
	
	class MyAppWindow extends Window {		
		VerticalLayout mainView = new VerticalLayout();
		UriFragmentUtility uriUtil = new UriFragmentUtility(); 
				
		MyAppWindow(){
			this.addComponent(header);			
			this.addComponent(mainView);		

			/*
			 * Setting content here causes problems:
			 * -BAD: http://localhost:8080/VaadinHistoryTest#screen2 initially renders with Main content and then replaces it with Screen 1 content.
			 * It only happens with new tabs, not when http://localhost:8080/VaadinHistoryTest#screen2 is the first tab of the session
			 */			
			//setMainContent(buildMainScreen());
			
			uriUtil.addListener(new FragmentChangedListener() {
		        public void fragmentChanged(FragmentChangedEvent source) {
		                String frag = source.getUriFragmentUtility().getFragment();
		                System.out.println("*** In Window-level UriFragmentUtility, fragmentChanged to "+frag);
		                
		                // Actually manipulate the contents of the Window here		                
		                if (frag!=null && !"".equals(frag.trim())){		                	
		                	String[] params = frag.split("/");
		                	String currentScreen = params[0]
;
		                	removeSubwindows();
		                	if (_screen1.equals(currentScreen)){
		                		setMainContent(buildScreen1());
		                	}
		                	else if (_screen2.equals(currentScreen)){
		                		setMainContent(buildScreen2());
		                	}
		                	else{
		                		setMainContent(buildMainScreen());
		                	}
		                }    
		                else {
		                	/*
		                	 * Setting the fragment here will do the following for either first tab of session or second tab:
		                	 * +GOOD: Changes uri frag in browser bar from http://localhost:8080/VaadinHistoryTest to http://localhost:8080/VaadinHistoryTest#%23main
		                	 */
		                	uriUtil.setFragment(buildUriToken(_mainScreen));		                	
		                	/*
		                	 * This is needed so that: (Tested with and without setFragment on getWindow)
		                	 * +GOOD: First tab of session with http://localhost:8080/VaadinHistoryTest will resolve properly to http://localhost:8080/VaadinHistoryTest#main and set content appropriately
		                	 */
		                	setMainContent(buildMainScreen());
		                }
		            }
			});

			/*
			 * Setting the fragment here causes problems:
			 * -BAD: First tab of session, http://localhost:8080/VaadinHistoryTest#screen2 initially renders with main content then gets replaced with Screen2 content
			 * -BAD: IF setFragment IS NOT DONE IN getWindow, http://localhost:8080/VaadinHistoryTest#screen2 as second tab also initially renders with main content then gets replaced with Screen2 content
			 * 
			 * Setting the fragment here resolves a problem:
			 * +GOOD: IF setFragment IS NOT DONE IN getWindow, http://localhost:8080/VaadinHistoryTest on second tab will resolve properly to http://localhost:8080/VaadinHistoryTest#main and set content appropriately
			 * 
			 * 
			 */
			//System.out.println("--- Newly created window uriFrag is "+uriUtil.getFragment());
			//uriUtil.setFragment(buildUriToken(_mainScreen));
			//System.out.println("--- Created window with uri fragment auto-set to main");
			
			
			this.addComponent(uriUtil);
			System.out.println("CREATED A NEW WINDOW");
		}
			
		public UriFragmentUtility getUriFragmentUtility(){
			return this.uriUtil;
		}
		
		
		public void setMainContent(Component mainComponent){			
			this.mainView.removeAllComponents();
			this.mainView.addComponent(mainComponent);
		}
		
		public void removeSubwindows() {
	        Collection<Window> wins = getChildWindows();
	        if (null != wins) {
	            for (Window w : wins) {
	                removeWindow(w);
	            }
	        }
	    }
		
		public DownloadStream handleURI(URL context, String relativeUri){
			System.out.println("~~~ In Window handleURI, path = "+context.getPath()+", relativeUri = "+relativeUri);
			
			String token = uriUtil.getFragment();
			System.out.println("~~~ In Window handleURI, fragment = "+uriUtil.getFragment());
			if (token == null || "".equals(token.trim())) {

				/*
				 *  This does the following:
				 * +GOOD: First tab of session http://localhost:8080/VaadinHistoryTest#screen2 avoids setting main content prior to rendering Screen2 content				 
				 * +GOOD: First tab of session http://localhost:8080/VaadinHistoryTest properly resolves to http://localhost:8080/VaadinHistoryTest#%23main 
				 */
				uriUtil.setFragment(buildUriToken(_mainScreen), false);				
				System.out.println("~~~ Forced uriToken to = "+uriUtil.getFragment());
			}
			return super.handleURI(context, relativeUri);
		}
	}

}


Excellent component. Working well for my application needs.

Hello,

I try to integrate the Navigator in my application. I was wondering why there is in the getWindow() static method in the navigator class a call to open : w.open(new ExternalResource(w.getURL())) ?


    public static Window getWindow(NavigableApplication application,
            String name, Window superGetWindow) {
        if (superGetWindow != null) {
            return superGetWindow;
        }

        Window w = application.createNewWindow();
        w.setName(name);
        ((Application) application).addWindow(w);
        w.open(new ExternalResource(w.getURL()));
        return w;
    }

It seems to put the window name in the browser location bar (which I try to avoid). If I comment the line, the navigation seems to work correctly but I am pretty sure there are side effects I can’t see.
I’ve seen this snippet of code written by Joonas multiple times in this forum so I guess this line is needed but what I can’t figure out is why.

In the Application.getWindow() javadoc, the implementation proposed states : “ensure use of window specific url” … but reading this does not help me.

thanks for you help,

Nicolas


If I remember
correctly, this is the way of ensuring that the window name is kept while refreshing the page. This is needed to make multiple tabs used at the same time behave well. If you can live without this redirect, be sure to check that multitab scenario works well enough for you. It might be that the only side-effect from removing this is that all refreshes would create a new server-side windows.

Hi Joonas,

thanks for your quick answer, I’ll check whether we can live without it.

Nicolas

Hi Joonas,

I reach the point where I’ll look at the Navigator.

I’ve questions (even before starting :wink: :

  1. Is your very last version on the add-on repository (v0.1) ?
  2. Could you provide the usage example of the on-line demo (probably within the jar file) ?
  3. As I’ll try to understand how it works, it might be interesting that I add comments in the code as I find out. May I have access to the source code for a few days?

Without even looking at the code, I’ve already a question about it: in the live demo, is it on purpose that I cannot open a new tab by ctrl-clicking a menu item?
http://jole.virtuallypreinstalled.com/Navigator#Dashboard

Thank you.
John.

  1. Yes. v0.1 is the latest one. Releasing 1.0 has been on my todo for too long, but I am not expecting it to contain any major changes. (And - unfortunately I do not have any schedule for 1.0). Feel free to use 0.1 - it should be better than the version number indicates :)

  2. Here is an use example:
    http://dev.vaadin.com/svn/incubator/Navigator/src/example/
    . And
    a live demo
    of the source code.

  3. Sure. Source code is in incubator.

Thank you Joonas, I’ve started to play with the Navigator this morning. A first reply to your question of this thread is that I like the API in general, and it’s too soon to say that I’ll have no suggestion changes.

From my side, a long serie of questions will arise. Do you prefer that I open a new forum thread for each one?

Here is the first question:

In Navigator.getWindow() method below:


    public static Window getWindow(NavigableApplication application,
            String name, Window superGetWindow) {
        if (superGetWindow != null) {
            return superGetWindow;
        }

        Window w = application.createNewWindow();
        w.setName(name);
        ((Application) application).addWindow(w);
        w.open(new ExternalResource(w.getURL()));
        return w;
    }

My question is:
Why do you call w.open(new ExternalResource(w.getURL())); ??

(I’m sure it’s useful, because you don’t do some checks I had to do in my app, but I don’t understand the reasoning).

Because of this line, when I open a new tab, it adds the window name in the URI.
Steps to reproduce:

I don’t want to have these two random numbers in my URLs. When removing the line w.open(new ExternalResource(w.getURL())); from Navigator.getWindow(), these numbers are not displayed anymore.

Thank you.
John.

Oh… I see that Nicolas has asked the very same question in this thread, a few posts earlier:

http://vaadin.com/forum/-/message_boards/message/67355#_19_message_121692

But I fear that I’m not satisfied with the answer, at least for a component that is an incubation of a part of Vaadin 7 :wink:

My instict tells me that it’s bound to this problem:
http://vaadin.com/forum/-/message_boards/message/49982?_19_delta=10&_19_keywords=&_19_advancedSearch=false&_19_andOperator=true&cur=2#_19_message_61830

but … I definitely cannot reproduce that problem. Maybe was it bound to the loginForm.

So, I need a Navigator with clean urls and no “maybe” dark-side effects :vader:

Navigator “stores” the window identity in any other window than the main window. This seems to be the only way of guaranteeing that the window identity is preserved over reloads and prevent any synchronization exceptions. I would love to drop the ugly “window name” part from the URL if someone figures out a good place to store the window identity…

That said - I am not 100% sure that we need to preserve the window identity. It might be ok just to re-create server-side window each time one presses reload button.

Hey,

is it possible for a future release of the Navigator-Addon, to make the
getOrCreateView(String) Method in the Navigator to be protected so
it could be overwritten for a Spring implementation?

Thanks,

Christoph

Hi Joonas

Very good job with the Navigator add-on. It saved me from the pain of writting mine.
I have two questions however:

  1. How and at what point do you specify the default view for your application. Navigator seems by default to use the first registered as the default page.

  2. Is there a way to force the navigator to use the guiced (proxied) classes instead of the non-managed versions? At the moment I had to modify the navigator source to get this to work.

I use guice along with an annotation scanner to automatically discover views and have no control over the order of discovery.

Regards.
Viktor

Hi Joonas,

In our application, on click of view, we create a wizard. Once i click on the view, i complete the wizard and when i try to again click on the view, it doesn’t do anything.
My requirement is if i again click on view, it should start the wizard again.

I figured out that the issue is if i try to click on the same view again, the fragment value remains the same and the fragmentchange event is not fired.

is there any way we can acheive this?

Hi Joonas,

In standalone mode (as a servlet) your addon works great. Now I am trying to make it work as a Liferay portlet, but I am getting an exception everytime I run it:

Caused by: java.lang.RuntimeException: Internal problem getting window URL, please report
at com.vaadin.ui.Window.getURL(Window.java:914)
at org.vaadin.navigator.Navigator.getWindow(Navigator.java:465)

Is it because the addon is not compatible with portlet mode or am I missing something else?

Great job, thank you.[font=Courier New]

[/font][font=Tahoma]

[/font][font=Courier New]

[/font]