I have the following component and my grid doesn't get refreshed when I receive a Spring event. Why is that?
#Component
#Scope(SCOPE_PROTOTYPE)
public class SearchResultGridView extends Grid<SearchResultModel> implements
ApplicationListener<FetchBySubscriptionNumberAction> {
private static final Logger log = LoggerFactory.getLogger(SearchView.class);
private final SumoCmMemberService sumoCmMemberService;
private List<SearchResultModel> searchResultModels = new ArrayList<>();
private final List<String> columnNames = List.of(
"subscriptionNumber",
"mnr",
"firstName",
"lastName",
"dateOfBirth",
"street",
"houseNumber",
"box",
"postalCode",
"locality",
"active"
);
public SearchResultGridView(SumoCmMemberService sumoCmMemberService) {
super(SearchResultModel.class, false);
this.sumoCmMemberService = sumoCmMemberService;
initGridColumns();
initStyle();
}
private void initGridColumns() {
columnNames.forEach(columnName -> addColumn(columnName).setAutoWidth(true));
}
private void initStyle() {
addThemeVariants(GridVariant.LUMO_NO_BORDER);
setHeightFull();
}
#Override
public void onApplicationEvent(FetchBySubscriptionNumberAction action) {
log.info(action.getSubscriptionNumber());
sumoCmMemberService
.fetchBySubscriptionNumber(action.getSubscriptionNumber())
.doOnError(e -> log.error("error in webflux: " + e.getMessage()))
.subscribe(sumoCmMemberDto -> {
SumoSubscriptionDto sumoSubscriptionDto = sumoCmMemberDto.getSumoSubscription();
SearchResultModel searchResultModel = SearchResultModel
.builder()
.mnr(sumoCmMemberDto.getMnumber())
.firstName(sumoSubscriptionDto.getFirstName())
.lastName(sumoSubscriptionDto.getLastName())
.street(sumoSubscriptionDto.getStreet())
.houseNumber(sumoSubscriptionDto.getHouseNumber())
.box(sumoSubscriptionDto.getBox())
.active(sumoCmMemberDto.isActive())
.build();
searchResultModels = List.of(searchResultModel);
setItems(searchResultModels);
});
}
}
I'm using a Mono from a service from which I subscribe, and I simply want to replace the items.
Note: I dislike the DataProvider api, and I wish not to use it.
My use case is simple; I want to subscribe to a Mono reactive stream and refresh the table.
So why doesn't this work? The simplest use case of the Grid Layout api makes believe that it should. No compiler errors and no runtime errors. Just nothing happens, and the grid doesn't display the data I provide to it. In debug modus, I see that the list is properly populated.
But the grid, nothing displays.
The problem is that there is no ongoing client request, so there is no way for Vaadin to let the browser know something has changed.
What you need to do is to configure #Push to enable two-way communication and then use ui.access() to handle the update:
UI ui = UI.getCurrent();
[...]
.subscribe(sumoCmMemberDto -> {
SumoSubscriptionDto sumoSubscriptionDto = sumoCmMemberDto.getSumoSubscription();
SearchResultModel searchResultModel = SearchResultModel
.builder()
.mnr(sumoCmMemberDto.getMnumber())
.firstName(sumoSubscriptionDto.getFirstName())
.lastName(sumoSubscriptionDto.getLastName())
.street(sumoSubscriptionDto.getStreet())
.houseNumber(sumoSubscriptionDto.getHouseNumber())
.box(sumoSubscriptionDto.getBox())
.active(sumoCmMemberDto.isActive())
.build();
searchResultModels = List.of(searchResultModel);
ui.access(()->setItems(searchResultModels)); // wrap setItems in ui.access()
});
Read more here in the docs: https://vaadin.com/docs/latest/flow/advanced/server-push/#push.access (there are small differences in V14 vs latest, so make sure you read docs for the correct version)
Related
I've written a small Spring Boot/Vaadin application that displays a simple UI to take user input and make a call to another service that takes some time to run. When the task is submitted, I'm displaying a progress dialog that shows a progress bar, a message informing the user what is going on and a close button to allow them to close the dialog when the job completes. I'm using a ListenableFuture to be notified when the task is done.
I can get the dialog to appear with status of "executing" and the progress bar doing its thing, but when the task is done (I have debug statements going to the console to let me know), it's not triggering the logic to update the status message and enable the close button. I can't figure out what I'm doing wrong.
Here's the code:
MainView1.java
#Route("rescheduleWorkOrders1")
#CssImport("./styles/shared-styles.css")
public class MainView1 extends VerticalLayout {
...
private final BackendService service;
public MainView1(BackendService service) {
this.service = service;
configureView();
addSubmitButton();
bindFields();
}
private void addSubmitButton() {
Button submit = new Button("Submit", this::submit);
add(submit);
}
private void submit(ClickEvent<?> event) {
UserData data = binder.getBean();
ListenableFuture<ResponseEntity<String>> future = service.executeTask(data);
ProgressDialog dialog = new ProgressDialog(future);
dialog.open();
}
private void configureView() {
...
}
private void bindFields() {
...
}
}
ProgressDialog.java
public class ProgressDialog extends Dialog {
private final ListenableFuture<ResponseEntity<String>> future;
private ProgressBar bar;
private Paragraph message;
private Button close;
public ProgressDialog(ListenableFuture<ResponseEntity<String>> future) {
super();
this.future = future;
configureView();
this.future.addCallback(new ListenableFutureCallback<>() {
#Override
public void onSuccess(ResponseEntity<String> result) {
message.setText("Task complete. Status: " + result.getStatusCode());
bar.setVisible(false);
close.setEnabled(true);
}
#Override
public void onFailure(Throwable ex) {
message.setText(ex.getMessage());
bar.setVisible(false);
close.setEnabled(true);
}
});
}
private void configureView() {
bar = new ProgressBar();
bar.setIndeterminate(true);
bar.setVisible(true);
message = new Paragraph("Executing task ...");
close = new Button("Close", this::close);
close.setEnabled(false);
add(bar, message, close);
}
private void close(ClickEvent<?> event) {
this.close();
}
}
BackendService.java
#Service
public class BackendService {
#Async
public ListenableFuture<ResponseEntity<String>> executeTask(UserData data) {
...
RestTemplate template = new RestTemplate();
ResponseEntity<String> response = template.postForEntity(uri, entity, String.class);
System.out.println(response);
return AsyncResult.forValue(response);
}
}
Note: I do have #EnableAsync specified in a #Configuration annotated class.
When dealing with asynchronous code in Vaadin you need to:
Use UI#access when updating the UI outside an active request. This acquires a lock on the UI, to prevent it being updated by two threads simultaneously.
Enable server push by adding the #Push annotation to your main layout or view. This allows the server to push updates to the client even if no request is active.
Without the former, you can get ConcurrentModificationExceptions in the best case, and very subtle bugs in the worst.
Without the latter, the changes will be applied (i.e. dialog closed), but the changes will only be sent to the client the next time the client sends a request. I believe this is your main issue.
More information can be found in the documentation.
I have simple Vaadin GUI which I would like connect with my Rest API on localhost:8080:
#Route("hello")
public class EmployeeGui extends VerticalLayout {
private final WebClient webClient = WebClient.create("http://localhost:8080");
public EmployeeGui() {
TextField textEmployee = new TextField("Give id of user");
Button buttonOK = new Button("OK");
Label label = new Label();
buttonOK.addClickListener(buttonClickEvent -> {
this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/employee/{id}")
.build(textEmployee.getValue()))
.retrieve()
.bodyToMono(EmployeeTo.class)
.subscribe(emp -> {
label.setText(emp.getName());
});
});
add(textEmployee,buttonOK, label);
}
}
On localhost:8080 works my backend application which give me REST API to retrive some data from DB.
In text field we can put id of user and then click OK-button. After that in label we set name of user. Unfortunately I got exception (in line label.setText(emp.getName());):
java.lang.IllegalStateException: Cannot access state in VaadinSession or UI without locking the session.
I understood it, but how can I omit this problem? How can I put user Id and then return User attributes to label after OK button was clicked?
When you react to the request from your API, the "request" from Vaadin is
already done. If you want to make that work you would have to enable #Push
(so Vaadin can send changes from the server when they happen) and make sure you
access the UI in a safe way (see "Asynchronous Updates" in the
docs for
details).
Or you actually don't need that to be async and
use the web client in a blocking way (so the client blocks and the label change
happens inside the "request" triggered by the button.
The direct answer to the question is using #Push (Doc 1, Doc 2) to update the ui when the main request has already been responded to (because your webClient.get() is asynchronous). The fix to your problem looks like this:
#Push
#Route("hello")
public class EmployeeGui extends VerticalLayout {
private UI ui;
private final WebClient webClient = WebClient.create("http://localhost:8080");
public EmployeeGui() {
TextField textEmployee = new TextField("Give id of user");
Button buttonOK = new Button("OK");
Label label = new Label();
// keep instance of UI in a field,
// and update it whenever the EmployeeGui is (re-)attached to the page
// (important when using #PreserveOnRefresh or RouterLayout)
addAttachListener(event -> {
this.ui = event.getUI();
});
buttonOK.addClickListener(buttonClickEvent -> {
this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/employee/{id}")
.build(textEmployee.getValue()))
.retrieve()
.bodyToMono(EmployeeTo.class)
.subscribe(emp -> {
// use ui.access to obtain lock on UI, perform updates within
getUI().access(() -> label.setText(emp.getName()));
});
});
add(textEmployee,buttonOK, label);
}
private UI getUI(){
return this.ui;
}
}
But depending on what you want to do with your application, I can recommend using Spring Security to make the user Log in, then you have easy and direct access to the current username.
I'm trying to combine two forms insertion in one using RxJava, RxAndroid and Mosby3, but I can't find a way to make it work.
My structure:
public final class CheckinIntent {
private final CheckinCommand checkinCommand;
private final Bitmap signature;
public CheckinIntent(CheckinCommand checkinCommand, Bitmap signature) {
this.checkinCommand = checkinCommand;
this.signature = signature;
}
public CheckinCommand getCheckinCommand() {
return checkinCommand;
}
public Bitmap getSignature() {
return signature;
}
}
Where I fire my intent (MVI pattern):
final Observable<Bitmap> signatureObservable = Observable.just(BitmapFactory.decodeFile(storage.getFile("signs", booking.getBookingId()).getAbsolutePath()));
final Observable<CheckinCommand> checkinCommandObservable = Observable.just(new CheckinCommand(booking.getBookingId(), booking.getUserId(), booking.getPartnerId(), userDetailsTextView.getText().toString(), "google.com"));
final Observable<CheckinIntent> intentObservable = Observable.zip(signatureObservable, checkinCommandObservable, (image, command) -> new CheckinIntent(command, image));
return saveButtonClickObservable
.flatMap(bla -> intentObservable);
And binding it all together:
#Override
protected void bindIntents() {
Observable<CheckinViewState> checkinViewStateObservable =
intent(CheckinView::sendCheckin)
.flatMap(checkinIntent -> imageRepository.uploadImage(checkinIntent.getSignature())
.flatMap(command -> bookingRepository.doCheckin(command) <------ PROBLEM HERE, HOW CAN I ACCESS THE COMMAND FROM ABOVE ??
.subscribeOn(Schedulers.from(threadExecutor))
.map(CheckinViewState.Success::new)
.cast(CheckinViewState.class)
.startWith(new CheckinViewState.LoadingState())
.onErrorReturn(CheckinViewState.ErrorState::new))
.observeOn(postExecutionThread.getScheduler());
subscribeViewState(checkinViewStateObservable, CheckinView::render);
}
Observable<CnhImageResponse> uploadImage(Bitmap bitmap);
My problem is, my uploadImage returns an internal structure that ends of on a String, but, how can I get the returned string, add it to my command object (setting the returned URL in this object) and continue the flow (sending my command to the cloud) ?
Thanks!
Just flatMap on the observable directly within the first flatMap. In that case you have reference to both, the checkinIntent and command
#Override
protected void bindIntents() {
Observable<CheckinViewState> checkinViewStateObservable =
intent(CheckinView::sendCheckin)
.flatMap(checkinIntent -> {
return imageRepository.uploadImage(checkinIntent.getSignature()
.flatMap(imageResponse -> bookingRepository.doCheckin(command) <-- Now you have access to both, command and CnhImageResponse
})
.subscribeOn(Schedulers.from(threadExecutor))
.map(CheckinViewState.Success::new)
.cast(CheckinViewState.class)
.startWith(new CheckinViewState.LoadingState())
.onErrorReturn(CheckinViewState.ErrorState::new))
.observeOn(postExecutionThread.getScheduler());
subscribeViewState(checkinViewStateObservable, CheckinView::render);
}
Alternative solution: Pass a Pair<CheckinIntent, Command> to the Observable from bookingRepository.doCheckin(...) like this:
#Override
protected void bindIntents() {
Observable<CheckinViewState> checkinViewStateObservable =
intent(CheckinView::sendCheckin)
.flatMap(checkinIntent -> imageRepository.uploadImage(checkinIntent.getSignature()
.map(imageResponse -> Pair.create(checkinIntent, imageResponse))) // Returns a Pair<CheckinIntent, CnhImageResponse>
.flatMap(pair -> bookingRepository.doCheckin(pair.first) <-- Now you can access the pair holding both information
.subscribeOn(Schedulers.from(threadExecutor))
.map(CheckinViewState.Success::new)
.cast(CheckinViewState.class)
.startWith(new CheckinViewState.LoadingState())
.onErrorReturn(CheckinViewState.ErrorState::new))
.observeOn(postExecutionThread.getScheduler());
subscribeViewState(checkinViewStateObservable, CheckinView::render);
}
Just a few other notes:
You almost ever want to prefer switchMap() over flatMap() in MVI. switchMap unsubscribes previous subscription while flatMap doesnt. That means that if you flatMap as you did in your code snipped and if for whatever reason a new checkinIntent is fired while the old one hasn't completed yet (i.e. imageRepository.uploadImage() is still in progress) you end up having two streams that will call CheckinView::render because the first one still continue to work and emit results down through your established observable stream. switchMap() prevents this by unsubscribing the first (uncompleted) intent before starting "switchMaping" the new intent so that you only have 1 stream at the time.
The way you build your CheckinIntent should be moved to the Presenter. This is kind of too much "logic" for a "dump" View. Also Observable.just(BitmapFactory.decodeFile(...)) is running on the main thread. I recommend to use Observable.fromCallable( () -> BitmapFactory.decodeFile(...)) as the later deferres his "work" (bitmap decoding) until this observable is actually subscribed and then you can apply background Schedulers. Observable.just() is basically the same as:
Bitmap bitmap = BitmapFactory.decodeFile(...); // Here is the "hard work" already done, even if observable below is not subscribed at all.
Observable.just(bitmap);
We have AbstractContributionFactorys like these:
final AbstractContributionFactory contributions = new AbstractContributionFactory("org.acme.mainMenu", null) {
#Override
public void createContributionItems(final IServiceLocator serviceLocator,
final IContributionRoot contributionRoot) {
String subMenuId ="org.acme.subMenu";
final MenuManager subMenu = new MenuManager("Sub menu", subMenuId );
contributionRoot.addContributionItem(subMenu, AlwaysEnabledExpression.INSTANCE);
menuService.addContributionFactory(new AbstractContributionFactory("menu:" + subMenuId, null) {
#Override
public void createContributionItems(final IServiceLocator serviceLocator1,
final IContributionRoot additions) {
additions.addContributionItem(new ActionContributionItem(new Action("Sub action") {
}), AlwaysEnabledExpression.INSTANCE);
}
});
}
};
menuService.addContributionFactory(contributions);
This code worked perfectly in Eclipse 3.x, but stopped working in E4. So while searching for the bug we found a lot uncommented code in the E4 framework, as much as two blocks in WorkbenchMenuService.addContributionFactory(...) alone. What I assume produces the bug is:
// // OK, now update any managers that use this uri
// for (Map.Entry<ContributionManager, MenuLocationURI> entry :
// managers.entrySet()) {
// MenuLocationURI mgrURI = entry.getValue();
// if (mgrURI.getScheme().equals(location.getScheme())
// && mgrURI.getPath().equals(location.getPath())) {
// ContributionManager mgr = entry.getKey();
// populateContributionManager(mgr, mgrURI.toString());
// mgr.update(true);
// }
// }
According to the comments on the associated bug a lot of people have the same problem.
Did anyone find a workaround for the bug?
I also wanted to programmatically add some menu items to the main menu of an RCP application and was caught out by this bug.
Instead of using the WorkbenchMenuService.addContributionFactory(...) method I found you can add contibution items using the by extending the ExtensionContributionFactory class (which also has a createContributionItems() method), and then add this using the org.eclipse.ui.menus extension point as a new menuContribution.
I found this solution in this blog by Vogella.
We are working on our Gis application, I am using gwt-openlayers and we're creating Vaadin wrappers.
So I've extended the MapWidget and created the required layers and vector layers and added a DrawFeatureControl:
DrawFeatureOptions drawFeatureOptions = new DrawFeatureOptions();
private DrawFeature drawFeaturePoint = = new DrawFeature(vectorLayer, new PointHandler(), drawFeatureOptions);
and to catch the event:
getWidget().getDrawFeatureOptions().onFeatureAdded(new () {
#Override
public void (com.openlayers.client.feature.VectorFeature vectorFeature) {
Window.alert("Feature Added" + vectorFeature.getFID());
serverRpc.featureAdded(buildVectorFeature(vectorFeature));
}
});
for some reason this is not working; although the following which should be almost the same is working fine:
getWidget().getVectorLayer().addVectorFeatureSelectedListener(new () {
#Override
public void onFeatureSelected(FeatureSelectedEvent eventObject) {
serverRpc.featureSelected(buildVectorFeature(eventObject.getVectorFeature()));
}
});
using (addVectorFeatureAddedListener) on the vector layer will be fired everytime a Feature is added to the VectorLayer, and will not be fired when the DrawFeatureControl is used.
Can someone help me to catch the Features that are drawn using the DrawFeatureControl.
By The way I have a Cluster and BBox stratigies on the MapWidget, I don't know if this changes anything.
Are you aware that Vaadin have their own wrappers for OpenLayers to use OpenLayers in Vaadin ?
https://vaadin.com/directory#addon/openlayers-wrapper
I found it, for some reason
getDrawFeatureOptions().onFeatureAdded
doesn't do the trick, I had to inject the listener in GWT using:
getWidget().getDrawFeaturePoint().eventListeners.addListener(getWidget().getDrawFeaturePoint(), featurePointAddedlistener, EventType.VECTOR_FEATURE_ADDED, new EventHandler() {
#Override
public void onHandle(EventObject eventObject) {
FeatureAddedEvent e = new FeatureAddedEvent(eventObject);
featurePointAddedlistener.onFeatureAdded(e);
}
});