2

I'd like to create the following simple MVP architecture:

  • View classes that are simly the vaadin layout, components, styles. nonfunctional. The views should be tied to the current ViewScope/SessionScope, therefore I use @UIScope of https://github.com/peholmst/vaadin4spring

  • Presenters should have the view injected, register listeners on the view components, handle user input and delegate to the model services

Problem: when I inject the view into the presenter, the view is recreated, thus presenter and view are not in the same scope. So the binding will not work. What can I change to achieve the design described above?

@VaadinComponent
@UIScope
public class LoginView {
    //form fields, buttons
}

@Controller
public class LoginPresenter implements ClickListener {
    @Autowired
    private LoginView view;

    @PostConstruct
    public void bind() {
        view.getLoginButton().addClickListener(this);
    }   

    @Override
    public void buttonClick(ClickEvent event) {
        //validate input and login
    }   
}
membersound
  • 81,582
  • 193
  • 585
  • 1,120

2 Answers2

5

Maybe something like

public class LoginView {

    @Autowired
    public void initPresenter(LoginPresenter loginPresenter) {
        loginPresenter.setLoginView(this);
        loginPresenter.bind();
    }
}

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class LoginPresenter {

    private LoginView loginView;

    public void bind() {
        // ...
    }

    public LoginView getLoginView() {
        return loginView;
    }

    public void setLoginView(LoginView loginView) {
        this.loginView = loginView;
    }

}

Edit

You can decouple adding a configuration interface but adds some complexity, for example

    public interface View {

    }

    public interface Presenter {

        void setView(View view);
        void bind();
    }

    public interface ViewManager {

        void configure(View view);
    }

    public class ViewSupport implements View {

        @Autowired
        private ViewManager viewManager;

        @PostConstruct
        public void init() {
            viewManager.configure(this);
        }
    }



 /**
  * ViewManager that configure Presenters following 
  * the naming convention XXView->XXPresenter
  */  
 public class DefaultViewManager implements ViewManager {


    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public void configure(View view) {
        Presenter p = (Presenter) applicationContext.getBean(getPresenterName(view.getClass()));
        p.setView(view);
        p.bind();

    }

    protected String getPresenterName(Class<?> clazz) {
        return StringUtils.uncapitalize(clazz.getSimpleName()).replace("View", "Presenter");
    }

}
Jose Luis Martin
  • 10,459
  • 1
  • 37
  • 38
  • That's a nice idea. Though I'd prefer not having to mention the presenter at all in my view... – membersound May 12 '14 at 22:46
  • Thanks for the update. But I'd like to have one presenter per view, so that the view is hold as state within the presenter, and changed according to user interactions. So, I'll probably have no other option than your first suggestion. – membersound May 13 '14 at 08:43
  • @membersound Not really, `DefaultViewManager` it's only a example, just change `presenterMap` to an `ApplicationContext` and use applicationContext.getBean(getPressenterName()) to get a prototype instance. – Jose Luis Martin May 13 '14 at 09:56
  • Oh ok, that was the missing piece for me. Using `@Autowired ApplicationContext` works, tyvm! – membersound May 13 '14 at 10:33
  • Just for reference: I just discovered that if I put the `DefaultViewManager ` and the views in the same `@Scope("ui")`, then I can also make use of a `Map presenters`, which will only contain the presenter objects valid for the current ui session. – membersound May 15 '14 at 08:04
1

I have a similar problem in our current project which I solved in the following way for now:

@VaadinUI
public class MainUI extends UI {

    @Autowired
    private MainView mainView;

    @Autowired
    private MainPresenter mainPresenter;

    @Autowired
    private Test1Presenter test1Presenter;

    @Autowired
    private Test2Presenter test2Presenter;

    @Override
    protected void init(final VaadinRequest vaadinRequest) {

        setContent(this.mainView);
    }
}

@Component
@Scope("ui")
@VaadinView(name = "MainView")
public class MainView extends CustomComponent {

    private static final Logger LOGGER = LoggerFactory.getLogger(MainView.class);

    private TabSheet tabSheet;

    @Autowired
    private Test1Tab test1Tab;

    @Autowired
    private Test2Tab test2Tab;

    @PostConstruct
    private void init() {

        LOGGER.debug("MainView - Method init - Test1Tab: " + this.test1Tab);

        this.tabSheet = new TabSheet();
        this.tabSheet.addTab(this.test1Tab);
        this.tabSheet.addTab(this.test2Tab);

        setCompositionRoot(this.tabSheet);
    }

    public TabSheet getTabSheet() {

        return this.tabSheet;
    }

    public Test1Tab getTest1Tab() {

        return this.test1Tab;
    }

    public Test2Tab getTest2Tab() {

        return this.test2Tab;
    }
}

@Controller
@Scope("ui")
// Implementing Serializable was necessary to remove the warning "Storing
// non-serializable bean [...] with name [...] in UI space [...]".
public class MainPresenter implements Serializable {

    @Autowired
    private MainModel model;

    @Autowired
    private MainView view;

    @PostConstruct
    private void init() {

        // ...
    }
}

@Component
@Scope("ui")
@VaadinView(name = "Test1Tab")
public class AlarmsTab extends VerticalLayout {

    private final Button test1Button = new Button("Test 1");

    private final Button test2Button = new Button("Test 2");

    public Test1Tab() {

        setCaption("Test1");
        setMargin(true);
        setSpacing(true);

        addComponent(this.test1Button);
        addComponent(this.test2Button);
    }

    public Button getTest1Button() {

        return this.test1Button;
    }

    public Button getTest2Button() {

        return this.test2Button;
    }
}

@Controller
@Scope("ui")
public class Test1Presenter implements Serializable {

    private static final Logger LOGGER = LoggerFactory.getLogger(Test1Presenter.class);

    @Autowired
    private MainModel model;

    @Autowired
    private Test1Tab view;

    @PostConstruct
    private void init() {

        LOGGER.debug("Test1Presenter - Method init - Test1Tab: " + this.view);

        this.view.getTest1Button().addClickListener(new ClickListener() {

            @Override
            public void buttonClick(ClickEvent event) {

               // ... 
            }
        });

        this.view.getTest2Button().addClickListener(new ClickListener() {

            @Override
            public void buttonClick(ClickEvent event) {

               // ... 
            }
        });
    }
}

This approach works, but I am not really sure if I am correctly using the scopes (views and presenters are annotated with @Scope("ui")).

Any comments on this?

Inactive user
  • 21
  • 1
  • 5
  • Are you really sure that `Test1Tab` is the same object in `MainView` as well as in `Test1Presenter`? – membersound May 28 '14 at 12:51
  • 1
    I added two debug statements to my code snippet above. Both print out the same instance of Test1Tab. When I used @Scope("prototype"), I had the same problem that you described initially. – Inactive user May 28 '14 at 13:15
  • IMO, I think you should not be injecting presenters in your UI. That's not scalable. If you have 100 views in the application (quite normal thing) What would you do? Inject 100 presenters? So if navigation is usually using 4-5, you inject 100 for every user session? I guess presenters should only be instanciated when they're needed, this means, when the View is ready and needs to be bound by the presenter. – frandevel Jan 18 '16 at 21:25