Adding tabs in Tabsheet showing different istances of same SpringUI

Hello,
I’m using Vaadin8 + Spring Boot and i’m facing the following problem:
I have a main view, that contains a menu in the top, and a tabsheet (empty at the startup). Each menu options, will add a new tab in the tabsheet, showing a component (Imagine like a browser, with the possibility to switch from a view to another using tabs).
The problem is that since each component that I need to show in the tab, is a Spring injected component, I have no chance to open this component in more than one tab.
if I click on the same menu item more than one time, it will not add a second tab in my TabSheet, cause the same istance has already beed attached.
To explain with minimal code:

MainLayout.java

[code]
@SpringUI
public class MainLayout extends UI {
VerticalLayout verticalLayout = new VerticalLayout();
MenuBar menu = new MenuBar();
TabSheet tabSheet = new TabSheet();

@Autowired
ComponentA componentA;

@Autowired
ComponentB componentB;

@Override
protected void init(VaadinRequest vaadinRequest) {
createMenu();
tabSheet.setSizeFull();
verticalLayout.addComponents(menu, tabSheet);
setContent(verticalLayout);
}

private void createMenu() {
MenuBar.MenuItem mainMenu = menu.addItem(“Components”, null);
mainMenu.addItem(“ComponentA”, p → {
tabSheet.addTab(componentA, “ComponentA”).setClosable(true);
tabSheet.setSelectedTab(componentA);
});
mainMenu.addItem(“ComponentB”, p → {
tabSheet.addTab(componentB, “ComponentB”).setClosable(true);
tabSheet.setSelectedTab(componentB);
});
}
}
[/code]ComponentA.java (similat to ComponentB.java)[code]
@SpringUI
public class PatientUI extends VerticalLayout {

@Autowired
ComponentAService service;

@Autowired
EventBus.UIEventBus eventBus;


@PostConstruct
public void init() {
   //initialize and do something with this view
}

}
[/code]Now, since ComponentA and ComponentB, needs autowired components like a service injected or the eventBus, i cannot create them with the “new” operator in the MainLayout class, something like:

mainMenu.addItem("ComponentA", p -> {
tabSheet.addTab(new ComponentA(), "ComponentA").setClosable(true);
});

or I can, if i pass the service and all the other components needed in the constructor of ComponentA. But this would mean that in the MainLayout class I have to autowire all the objects needed by all the components and then passing them in each constructor during the addTab operation. It doesn’t seem a really clean solution and also I don’t know if I’m in the right way with this, or if there is another better approach.

Hi,
what about using navigator with SpringViewProvider and a custom ViewDisplay that adds views as tabsheet component?

ComponentA and ComponentB will be annotated as @SpringView and implement
com.vaadin.navigator.View, so they will have ViewScope.

@SpringView 
public class View1 extends Composite implements View { ... }

@SpringView(name="anotherView") 
public class View2 extends Composite implements View { ... }

ViewDisplay implementation will addTabs to the tabsheet

public class MyUI extends UI {
   @Autowired
   SpringViewProvider viewProvider;
   
    protected void init(VaadinRequest vaadinRequest) {
        ...
        Navigator navigator = new Navigator(this, view -> {
                TabSheet.Tab tab = tabSheet.addTab(view.getViewComponent(), view.toString());
                tab.setClosable(true);                  
                tabSheet.setSelectedTab(tab);
         });
         navigator.addProvider(viewProvider);
        ...
    }


    private MenuBar createMenu() {
        MenuBar.MenuItem mainMenu = menu.addItem("Components", null);
        mainMenu.addItem("ComponentA", p -> { getNavigator().navigateTo("componentA"); });
        mainMenu.addItem("ComponentB", p -> { getNavigator().navigateTo("anotherView"); });
     }
}

However in this way you should also manage the start state and tabs could also be openend directly (eg http://localhost/#!/componentA)

HTH
Marco

ComponentA and ComponentB should be annotated with @SpringComponent, not with @SpringUI

Ciao Marco, and thank you for your reply.
It works! I mean I can add many tabs that show the same SpringComponent bean UI.
But there is a problem…imagine if one of the view has some UI injected, like the topPanel field in this example:

[code]
@SpringView(name=“anotherView”)
public class View2 extends Composite implements View {

@Autowired
EventBus.UIEventBus eventBus;

@Autowired
AService service;

@Autowired
MyTopPanel topPanel;

}
[/code]And MyTopPanel is like:

@SpringComponent
@UIScope
public class MyTopPanel extends VerticalLayout {

@Autowired
EventBus.UIEventBus eventBus;

So MyTopPanel is a part of View2. More generally, if a complex view is composed by more Ui or Views, is there a way to make them as prototype and not singleton? Because using your solution, I have yes more than one istance for the ComponentA or ComponentB views, but all these instances use the same injected istances (MyTopPanel as the previous code example).

Thanks again for your reply

Hi,
you can mark MyTopPanel (and all components that should share the same lifecycle of the view) as @ViewScope insted of @UIScope

HTH
Marco

Hi Thanks for all

Thank you Marco! It works as expected!

When you say:
However in this way you should also manage the start state and tabs could also be openend directly (eg http://localhost/#!/componentA)

What do you mean with start state?
And yes, the tabs can be opened directly, using a link, but this maybe can cause a problem, if I need to pass a variable in the constructor of one View, that depends by other calculations inside the Main layout, this should not be open directly with a link. Is there a way to forbid the direct opening?

I think I understood what you mean with the intiial state.
The navigator during its initialization will try to navigate to a default view, with name “” (empty string).
I managed this, using a View, that does nothing, and that has as name an empty string. And in the navigator, i skip to add the tab, if a view has a name = “”.
To make this i had created a TabbeableView interface that extends View and has one method that return the caption to put in the tab of the TabSheet.
In code

TabbeableView.java public interface TabbeableView extends View { String getTabCaption(); }
InitialView.java (it’s the default view that does anything)

[code]
@SpringView(name = InitialView.VIEW_NAME)
public class InitialView extends HorizontalLayout implements TabbeableView {
public static final String VIEW_NAME = “”;

@Override public String getTabCaption() { return VIEW_NAME; }

}
[/code]And in the MainLayout class, where i instanciate the Navigator I do:

Navigator navigator = new Navigator(this, view -> { String caption = ((TabbeableView) view).getTabCaption(); if (!caption.isEmpty()) { TabSheet.Tab tab = tabSheet.addTab(view.getViewComponent(), caption); tab.setClosable(true); tabSheet.setSelectedTab(tab); } }); navigator.addProvider(viewProvider); ... In this way I don’t have the annoying exception " java.lang.IllegalArgumentException: Trying to navigate to an unknown state ‘’ and an error view provider not present"

But the issue that i can’t understand how to resolve now is another. Using your solution of Navigator with SpringViewProvider works fine, until i want to pass some parameters to the tab that i’m going to open.
Example, imagine if in one view, let’s say “ComponentA” are shown information about a user, together with his address, and clicking on a button “show map” in the same view, i want to attacch a new tab, showing the google map geolocation of the user address. This new view, MapPanel, it’s a SpringView like the others, but need to have the info about the user and the adress to show details on the screen. Is there a way to do this?

Hi, sorry for late response.
One way to pass data to the view could be using URI fragment (https://vaadin.com/docs/-/part/framework/advanced/advanced-navigator.html#advanced.navigator.urifragment); for example from “ComponentA” view you can navigate to “map/userId=21&foo=bar” and in the
enter
method of the MapPanel you can get the parameter from the ViewChangeEvent (using getParameterMap(), getParameterMap(separator) or getParameters()).

If you want to pass a POJO you could use VaadinRequest.(get|set)Attribute.

HTH
Marco

I need to pass POJO, and I don’t know if the VaadinRequest can be the best way.
What do you think about extending the Navigator class, and changin the method navigateTo() so to return the View to the user?
Something like this:[code]
public class MyNavigator extends Navigator {
public MyNavigator(UI ui, ViewDisplay display) {
super(ui, display);
}

public View navigateTo(String navigationState) {
//same code of the navigateTo() method of parent class


View viewWithLongestName = null;


return viewWithLongestName ;
}
[/code]
Anyway, to return in topic, your solution with Navigator and SpringView is a good solution for having multiple istances of the same component opened in multiple tabs of the tabsheet.
The problem that I have with this, is that each tab(view) has its own url, and it is accessible from the url directly. While in most case this is an advantage, in my specific case, i would like to hide the paths of each view, and have only a direct link to the main UI with the tabsheets without tabs opened. Is it possible to do? Or I need to use another solution without navigator and view?
Thanks again for your help!

Hi,
what about calling
navigator.getCurrentView()
just after
navigateTo()
?
Maybe this could be simpler rather than extending Navigator (haven’t tried it).

In your case Navigator is maybe not the best solution after all.
To avoid direct link you should implement a custom NavigationStateManager for Navigator (did it time ago for a custom event based navigator).
Maybe it could be simpler using SpringViewProvider (or even spring context directly) to get new component instances and add them in tabsheet.
Something like this

  {
   ...
   Button b = new Button("Map", e -> openTab("map", (MapView mapView) -> { mapView.setAddress(...) }));
   ...
   }

    private <TAB extends Component> void openTab(String tabComponent, Consumer<TAB> customizer) {
        View view  = viewProvider.getView(tabComponent);
        TAB component = (TAB)view.getViewComponent();
        customizer.accept(component);
        TabSheet.Tab tab = tabSheet.addTab(component, view.toString());
        tab.setClosable(true);
        tabSheet.setSelectedTab(tab);
    }

HTH
Marco

Excellent solution! It works!
I just made a little change, to have the name of the view as caption of the tab, instead of the class full name.
Thank you for your precious help Marco!