When are .NET MAUI (Xamarin Forms) pages/custom views destroyed? - xamarin.android

I'm new to Xamarin/.NET MAUI application development. I started to develop a sample .NET MAUI application for Android device.
I'm trying to understand how/when a page and my custom view are destroyed (disposed of). I read some web pages but I can't really understand how things work in .NET MAUI (or Xamarin).
I have three pages: MainPage, SecondPage, TestMapPage.
SecondPage has a button that navigates to TestMapPage. It instantiates a TestMapPage object and passes it to Navigation.PushAsync().
TestMapPage contains a custom view TestMapView, which is rendered by my custom view renderer TestMapViewRenderer. I create a MapView object (from Naxam.Mapbox.Droid) in the renderer and show the map in TestMapPage. The map appears on the emulator and it works fine.
I thought that SecondPage, TestMapPage and TestMapView (and all the objects in TestMapViewRenderer) will be destroyed when I navigate back to MainPage. However, when I set a break point on Dispose() in the renderer and navigate back to SecondPage or MainPage in , it never gets hit.
My questions:
Are the SecondPage, TestMapPage, TestMapView and all the other objects in the view and view renderer like MapboxMap kept somewhere when I go back to MainPage?
When are pages and views destroyed/disposed of?
If those page objects are kept somewhere until the application shuts down, is it normal behaviour?
If not normal behaviour, how do I fix it?
I'm worried about memory leak...
MainPage.xaml.cs
public partial class MainPage : ContentPage
{
// ...
private async void OnGoToSecondPageClicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new SecondPage());
}
}
SecondPage.xaml.cs
public partial class SecondPage : ContentPage
{
// ...
private async void OnMapShowClicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new TestMapPage());
}
}
TestMapPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MapTest"
x:Class="MapTest.TestMapPage">
<StackLayout Margin="5">
<local:TestMapView
x:Name="map"
VerticalOptions="FillAndExpand"
HorizontalOptions="CenterAndExpand"/>
</StackLayout>
</ContentPage>
TestMapView.cs
public class TestMapView : View { }
TestMapViewRenderer.cs
public partial class TestMapViewRenderer : ViewRenderer<TestMapView, Android.Views.View>
{
private MapboxMap map;
public TestMapViewRenderer(Context context) : base(context) {}
protected override void OnElementChanged(ElementChangedEventArgs<TestMapView> e)
{
base.OnElementChanged(e);
// ...
if (Control == null)
{
var mapView = new MapView(Context);
SetNativeControl(mapView);
mapView.GetMapAsync(this);
}
}
public void OnMapReady(MapboxMap map)
{
this.map = map;
this.map.SetStyle(Resources.GetString(Resource.String.mapbox_style_satellite), this);
}
protected override void Dispose(bool disposing)
{
// A breakpoint never hits on this line. Why?
base.Dispose(disposing);
}
// ...
}

as far as I know;
No, on each navigation you create new page instances, so, the page is created and kept in memory by .NET itself, once garbage collection hits a certain threshold, the memory will be free. Until then, the pages are in memory, but not meant to use somewhen in the future.
When GC decides, the Xamarin/MAUI doesn't care about views.
Unfortunately, yes.
It's normal, you can overcome by using what #ToolmakerSteve refers.

Related

Expanding a component in a PopupView - Vaadin

I am using Vaadin 8.5.1 with the Vaading Desiin combination with Spring Boot 2.0.4.
Currently I am trying to add a PopupView at the bottom of the page which opens on button click. In the Popup there is a vertical layout including two components: a HorizontalSplitPanel and a Button. The PopupView should have the width of the current BrowserWindow and one third of the height.
The HorizontalSplitPanel should use all the space in the popup, which is not needed for the button.
What I did:
#SpringComponent
#UIScope
public class View extends VerticalLayout implements Observer {
private final PopupContentView popupContentView;
private PopupView popup;
#Autowired
public View(PopupContentView popupContentView) {
this.popupContentView = popupContentView;
}
#PostConstruct
void init() {
button.addClickListener(clickEvent -> openPopup());
}
private void openPopup() {
if (popup == null) {
setSizeOfPopUp();
// popup will adjust automatically to size of content
popup = new PopupView(null, popupContentView);
popup.addPopupVisibilityListener(event -> {
if (event.isPopupVisible()) {
popupContentView.build(this::submitted);
}
});
popup.setHideOnMouseOut(false);
this.addComponent(popup);
}
popup.setPopupVisible(true);
}
private void setSizeOfPopUp() {
if (popupContentView != null) {
popupContentView.setWidth(Page.getCurrent().getBrowserWindowWidth(), Unit.PIXELS);
popupContentView.setHeight(((float) Page.getCurrent().getBrowserWindowHeight()) / 3, Unit.PIXELS);
}
}
private void submitted() {
// do some stuff
}
#Override
public void update(Observable observable, Object o) {
if (observable instanceof BrowserWindowResizeListenerObservable) {
setSizeOfPopUp();
}
}
}
#Service
public class BrowserWindowResizeListenerObservable extends Observable implements Page.BrowserWindowResizeListener {
#Override
public void browserWindowResized(Page.BrowserWindowResizeEvent browserWindowResizeEvent) {
this.setChanged();
this.notifyObservers();
}
}
#SpringComponent
#UIScope
public class PopupContentView extends VerticalLayout {
private SubmitCallback submitCallback;
private Button submitBtn;
#PostConstruct
void init() {
super.init();
}
void build(#NotNull SubmitCallback) {
removeAllComponents();
this.addComponent(horizontalSplitPanel);
this.addComponent(submitBtn);
this.setExpandRatio(horizontalSplitPanel, 1.0f);
this.submitCallback = callback;
}
private void submit() {
submitCallback.submit(someContent);
}
#FunctionalInterface
public interface SubmitCallback {
void submit(SomeContent someContent);
}
}
As you can see, I have a main view, a view for the content and a listener class.
What I want to happen is that the popup is visible on button click and contains the content view with the panel and the submit button. The panel takes the rest of the space, which is not needed for the button. and the popup is fully filled with content.
What actually happens is that the panel takes the full space of the popup and the button will be shown below the popup.
However, when I resize the window and the resizing event gets fired, everything is fine and the button is no longer below the popup.
It seems to be that the padding and the margin (which are the HTML implementation of the expand ratio in Vaadin) are calculated at an earlier stage and get triggered again when resizing the window. However, I have no clue when and what I need to do, to trigger it.
Does anyone have an idea, how can fix this?
EDIT:
When I have a Tree component or a DateField component in the PopupView and then expand a tree element or change the value of the DateField by selecting a value from the Date popup, the resizing is done correctly and everything is fine.
I think in your case the method of checking Browser window size and calculating target pixel size is too complex for your case. I would recommend just to set the width of the popup to be 100% and height to be 33%, like component.setHeight("33%"), yes you can use percentages for width and height instead of pixels. Then sizing is done by CSS, and it will react faster to browser window sizing without server round trip.

Memory leaks with NavigationController and multiple UIViewController

In my application I manage 4 UIViewController :
First one for handle user login
Second one that display the menu
Third one for displaying the action available depending on the user entry in previous view.
Fourth one that show details infos about previous selected item on third view.
Third and Four are displaying a UIViewTable with item to select that are passed to the next UIViewController to display info.
So I made attention to not using hard link to each other. Parameters are passed when UIViewControlle are instantiate.
Here is the probleme that I'm dealing with :
When I pass from Third to Fourth, and then go back to Fourth, memory is never been released. Worth, when I return to Fourth it take the same amount of memory again.
After 3-4 round-trip the device memory is full. (Another strange behavior here with UIImageView that take 12Mo of memory any picture, no mater the picture size is 12Ko or 120Ko...)
Here is the code that is use to switch between controllers :
//First UIViewController, tagged as Root in storyboard
public partial class LoginController
{
//....some logic code here
//User ask for login by pressed a button , after an echange with a server
//for credential verification, if the server reply "ok" then I call the second
//UIViewController like this
void PushHomeScreen()
{
HomeController homeController = Storyboard.InstantiateViewController("home") as HomeController;
NavigationController.PushViewController(homeController, false);
}
}
public partial class HomeController
{
//....some logic code here
//The view displaying 3 UIButton that root to 2 different UIViewController.
//First one is deadend, and user can only go back. It will be used one time to select a dataset to download and use after.
//The two next use the same UIViewController, only the way that the controller handle the nexts user action is different :
//If user came from the Second then it display screen for retrieving user attendance
//If user came from the Third then it display screen for retrievin user survey.
//So here are the code behind:
private void BtnDownload_TouchUpInside(object sender, EventArgs e)
{
DownloadController downloadTrainingController = Storyboard.InstantiateViewController("downloadTraining") as DownloadController;
NavigationController.PushViewController(downloadTrainingController, false);
}
private void BtnSurvey_TouchUpInside(object sender, EventArgs e)
{
TrainingAndReviewListController trakningController = Storyboard.InstantiateViewController("trakning") as TrainingAndReviewListController;
trakningController.backgroundColor = "violet";
NavigationController.PushViewController(trakningController, false);
}
private void BtnTraining_TouchUpInside(object sender, EventArgs e)
{
TrainingAndReviewListController trakningController = Storyboard.InstantiateViewController("trakning") as TrainingAndReviewListController;
trakningController.backgroundColor = "red";
NavigationController.PushViewController(trakningController, false);
}
}
//I will not show you the DownloadController as it is not pertinent to the current probleme.
public partial class TrainingAndReviewListController
{
//To handle the back action, so user go from this to HomeController
void BtnHome_TouchUpInside(object sender, EventArgs e)
{
NavigationController.PopViewController(false);
}
private void Tablesource_OnRowSelected(object sender, DownloadTableSource.RowSelectedEventArgs e)
{
serviceController = Storyboard.InstantiateViewController("service") as ServiceController;
serviceController.backgroundColor = backgroundColor;
serviceController.training = e.training;
NavigationController.PushViewController(serviceController, false);
}
}
//Next are the final UIViewController on the storyboard. It consume a lot of memory in inspector due to 20 UIViewImage on it who each consume 12Mo of memory.
//When I come back from this Controller to the previous memory are never released, and if I return on it, it consume the same amount extra again
//One solution to prevent this is to instanciate him once in TrainingAndReviewListController as class attribute, and then reuse it. But it is not supposed to be the role
//of the NavigationController?
public partial class ServiceController
{
public string backgroundColor = "red";
public Training training;
//To handle the user action for going to the Home controller, this is not the action that cause memory leaks, altoutgh it will cause the same behavior I think.
void BtnHome_TouchUpInside(object sender, EventArgs e)
{
foreach (UIViewController uiviewcontroller in NavigationController.ViewControllers) {
if (uiviewcontroller.GetType() == typeof(HomeController)) {
NavigationController.PopToViewController(uiviewcontroller as HomeController, false);
}
}
}
void BtnGoBack_TouchUpInside(object sender, EventArgs e)
{
Console.WriteLine(NavigationController.ViewControllers.Count()); //It always log the same count, as if the controller is really be poped when i come back and pop again.
//But profilage show me that memory still used by this instance.
NavigationController?.PopViewController(false);
}
}

VaadinSession attribute and updating session-bound components

I have a Vaadin Navigator with multiple View elements. Each view has a different purpose however some also contain common traits that I have put inside custom components.
One of those custom components is the menu - it is positioned at the top and allows navigation between the different views. I create and add this component inside the constructor of each view (if you are interested in the menu's implementation see the end of this post). Here is a skeleton for each custom view:
class MyViewX implements View {
MenuViewComponent mvc;
public MyViewX() {
mvc = new MenuViewComponent();
addComponent(mvc);
}
#Override
public void enter(ViewChangeEvent event) {
}
}
So far, so good. In order to make things simple I will explain my problem using a simple label and not one of my other custom components but the dependency that I will describe here is the same for those components just like with the label.
Let's say I have a label which sole purpose is to display a greeting with the user's username. In order to do that I use VaadinSession where I store the attribute. This is done by my LoginController, which validates the user by looking into a database and if the user is present, the attribute is set and one of the views is opened automatically. The problem is that VaadinSession.getCurrent().getAttribute("username") returns null when called inside the constructor. This of course makes sense omho because a constructor should not be bound by a session-attribute.
So far I have managed to use the enter() method where there is no problem in retrieving session attributes:
class MyViewX implements View {
MenuViewComponent mvc;
public MyViewX() {
mvc = new MenuViewComponent();
addComponent(mvc);
}
#Override
public void enter(ViewChangeEvent event) {
String username = (String)VaadinSession.getCurrent().getAttribute("username");
Label greeting = new Label("Hello " + username);
addComponent(greeting);
}
}
The issue that comes from this is obvious - whenever I open the view where this label is present, a new label is added so if I re-visit the view 10 times, I will get 10 labels. Even if I move the label to be a class member variable the addComponent(...) is the one that screws things up. Some of my custom components really depend on the username attribute (in order to display user-specific content) hence I also have to place those in the enter(...) method. The addComponent(...) makes a mess out of it. I even tried the dirty way of removing a component and then re-adding it alas! in vain:
class MyViewX implements View {
MenuViewComponent mvc;
Label greeting;
public MyViewX() {
mvc = new MenuViewComponent();
addComponent(mvc);
}
#Override
public void enter(ViewChangeEvent event) {
String username = (String)VaadinSession.getCurrent().getAttribute("username");
greeting = new Label("Hello " + username);
// Remove if present
try { removeComponent(greeting); }
catch(Exception ex) { }
// Add again but with new content
addComponent(greeting);
}
}
but it's still not working. So my question is: what is the simplest way of updating a component that requires session-bound attributes?
The navigation via the menu custom component is omho not the issue here since all components of the menu are loaded in it's constructor. That's why it's also load that component in particular in a view's own constructor. Here is an example of a button in my menu that opens a view:
#SuppressWarnings("serial")
#PreserveOnRefresh
public class MenuViewComponent extends CustomComponent {
public MenuViewComponent(boolean adminMode) {
HorizontalLayout layout = new HorizontalLayout();
Label title = new Label("<h2><b>Vaadin Research Project</b></h2>");
title.setContentMode(ContentMode.HTML);
layout.addComponent(title);
layout.setComponentAlignment(title, Alignment.TOP_LEFT);
Button personalDashboardButton = new Button("Personal dashboard", new Button.ClickListener() {
#Override
public void buttonClick(ClickEvent event) {
getUI().getNavigator().navigateTo(MainController.PERSONALDASHBOARDVIEW);
}
});
personalDashboardButton.setStyleName(BaseTheme.BUTTON_LINK);
layout.addComponent(personalDashboardButton);
layout.setComponentAlignment(personalDashboardButton, Alignment.TOP_CENTER);
// Add other buttons for other views
layout.setSizeUndefined();
layout.setSpacing(true);
setSizeUndefined();
setCompositionRoot(layout);
}
}
PERSONALDASHBOARDVIEW is just one of the many views I have.
It may be worth considering how long should your view instances "live", just as long they're displayed, until the session ends or a mix of the two. With this in mind and depending on what needs to happen when you enter/re-enter a view, you have at least the following 3 options:
1) Recreate the whole view (allowing for early view garbage-collection)
first register a ClassBasedViewProvider (instead of a StaticViewProvider) which does not hold references to the created views:
navigator = new Navigator(this, viewDisplay);
navigator.addProvider(new Navigator.ClassBasedViewProvider(MyView.NAME, MyView.class));
simple view implementation
public class MyView extends VerticalLayout implements View {
public static final String NAME = "myViewName";
#Override
public void enter(ViewChangeListener.ViewChangeEvent event) {
// initialize tables, charts and all the other cool stuff
addComponent(new SweetComponentWithLotsOfStuff());
}
}
2) Keep some already created components and replace others
public class MyView extends VerticalLayout implements View {
private MySweetComponentWithLotsOfStuff mySweetComponentWithLotsOfStuff;
public MyView() {
// initialize only critical stuff here or things that don't change on enter
addComponent(new MyNavigationBar());
}
#Override
public void enter(ViewChangeListener.ViewChangeEvent event) {
// oh, so the user does indeed want to see stuff. great, let's do some cleanup first
removeComponent(mySweetComponentWithLotsOfStuff);
// initialize tables, charts and all the other cool stuff
mySweetComponentWithLotsOfStuff = new SweetComponentWithLotsOfStuff();
// show it
addComponent(mySweetComponentWithLotsOfStuff);
}
}
3) Lazy creating and updating (or not) the content when entering
public class MyView extends VerticalLayout implements View {
private boolean isFirstDisplay = true;
private MySweetComponentWithLotsOfStuff mySweetComponentWithLotsOfStuff;
public MyView() {
// initialize only critical stuff here, as the user may not even see this view
}
#Override
public void enter(ViewChangeListener.ViewChangeEvent event) {
// oh, so the user does indeed want to see stuff
if (isFirstDisplay) {
isFirstDisplay = false;
// lazily initialize tables, charts and all the other cool stuff
mySweetComponentWithLotsOfStuff = new SweetComponentWithLotsOfStuff();
addComponent(mySweetComponentWithLotsOfStuff);
} else {
// maybe trigger component updates, or simply don't do anything
mySweetComponentWithLotsOfStuff.updateWhateverIsRequired();
}
}
}
I'm sure (and curious) that there may be other options, but I've mainly used a variation of 1) using spring with prototype views and component tabs.

How to open custom dialog box / popup using Xamarin.Forms?

I am newbie to Xamarin.Forms and stuck with a situation where I want to open up a popup box with my control details [e.g. View Employee Details] on click of parent page.
How can I open custom dialog box / popup using Xamarin.Forms?
Any example code will be appreciated?
Thanks in advance!
If you still want to have your popup's code in its own Page you can set up some custom renderers along the following logic.
1. A ModalPage & corresponding renderer
public class ModalPage : ContentPage { }
public class ModalPageRenderer : PageRenderer {
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
this.View.BackgroundColor = UIColor.Clear;
this.ModalPresentationStyle = UIModalPresentationStyle.OverCurrentContext;
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
SetElementSize (new Size (View.Bounds.Width, View.Bounds.Height));
}
}
2. HostPage
public class ModalHostPage : ContentPage, IModalHost
{
#region IModalHost implementation
public Task DisplayPageModal(Page page)
{
var displayEvent = DisplayPageModalRequested;
Task completion = null;
if (displayEvent != null)
{
var eventArgs = new DisplayPageModalRequestedEventArgs(page);
displayEvent(this, eventArgs);
completion = eventArgs.DisplayingPageTask;
}
// If there is no task, just create a new completed one
return completion ?? Task.FromResult<object>(null);
}
#endregion
public event EventHandler<DisplayPageModalRequestedEventArgs> DisplayPageModalRequested;
public sealed class DisplayPageModalRequestedEventArgs : EventArgs
{
public Task DisplayingPageTask { get; set;}
public Page PageToDisplay { get; }
public DisplayPageModalRequestedEventArgs(Page modalPage)
{
PageToDisplay = modalPage;
}
}
}
3. HostPage renderer
public class ModalHostPageRenderer: PageRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if(e.OldElement as ModalHostPage != null)
{
var hostPage = (ModalHostPage)e.OldElement;
hostPage.DisplayPageModalRequested -= OnDisplayPageModalRequested;
}
if (e.NewElement as ModalHostPage != null)
{
var hostPage = (ModalHostPage)e.NewElement;
hostPage.DisplayPageModalRequested += OnDisplayPageModalRequested;
}
}
void OnDisplayPageModalRequested(object sender, ModalHostPage.DisplayPageModalRequestedEventArgs e)
{
e.PageToDisplay.Parent = this.Element;
var renderer = RendererFactory.GetRenderer (e.PageToDisplay);
e.DisplayingPageTask = this.PresentViewControllerAsync(renderer.ViewController, true);
}
}
Then it is as simple as calling
await ModalHost.DisplayPageModal(new PopUpPage());
from your host page or in this particular case from the ViewModel behind.
What Pete said about PushModalAsync / PopModalAsync still remains valid for this solution too (which in my opinion is not a disadvantage), but your popup would appear with transparent background.
The main advantage of this approach, in my opinion, is that you can have your popup XAML/code definition separate from the host page and reuse it on any other page where you wish to show that popup.
The general purpose of what you are trying to achieve can be accomplished by using the PushModalAsync and PopModalAsync methods of Xamarin.Forms Navigation object.
The chances are that this is good enough for what you are needing - However - this isn't truely modal. I will explain after a small code snippet:-
StackLayout objStackLayout = new StackLayout()
{
};
//
Button cmdButton_LaunchModalPage = new Button();
cmdButton_LaunchModalPage.Text = "Launch Modal Window";
objStackLayout.Children.Add(cmdButton_LaunchModalPage);
//
cmdButton_LaunchModalPage.Clicked += (async (o2, e2) =>
{
ContentPage objModalPage = new ContentPage();
objModalPage.Content = await CreatePageContent_Page2();
//
await Navigation.PushModalAsync(objModalPage);
//
// Code will get executed immediately here before the page is dismissed above.
});
//
return objStackLayout;
private async Task<StackLayout> CreatePageContent_Page2()
{
StackLayout objStackLayout = new StackLayout()
{
};
//
Button cmdButton_CloseModalPage = new Button();
cmdButton_CloseModalPage.Text = "Close";
objStackLayout.Children.Add(cmdButton_CloseModalPage);
//
cmdButton_CloseModalPage.Clicked += ((o2, e2) =>
{
this.Navigation.PopModalAsync();
});
//
return objStackLayout;
}
The problem with the above is that the
await Navigation.PushModalAsync(objModalPage);
will immediately return after the animation.
Although you can't interact with the previous page, as we are displaying a new NavigationPage with a Close button shown - the parent Navigation Page is still executing behind the scenes in parallel.
So if you had any timers or anything executing these still would get called unless you stopped those.
You could also use the TaskCompletionSource approach as outlined in the following post also How can I await modal form dismissal using Xamarin.Forms?.
Note - that although you can now await the 2nd page displaying and then when that page is dismissed allowing code execution to continue on the next line - this is still not truely a modal form. Again timers or anything executing still will get called on the parent page.
Update 1:-
To have the content appear over the top of existing content then simply include it on the current page, however make this section of content invisible until you need it.
If you use an outer container such like a Grid that supports multiple child controls in the same cell, then you will be able to achieve what you want.
You will also want to use something like a filled Box with transparency that will cover the entire page also, to control the visible, see through section, that surrounds your inner content section.
I followed above approach and found it impossible to run on iOS 7.
I found this library BTProgressHUD which you can modify and use.
I Use its methods by Dependency service.
Actual library for popups.
https://github.com/nicwise/BTProgressHUD
Following example uses BTProgressHUD library internally.
https://github.com/xximjasonxx/ScorePredictForms

BlackBerry back to screen detection

HI, I try to detect reterning to screen after closing another screen,
should work when returning from my application screens, but also returning from device camera
after shooting video. In overriden method onExposed() I'm able to detect this situation,
but it's called too many times, and also called when dialog was shown (alert).
Is there better way to detect return to screen?
protected void onExposed() {
// return to screen detected
MainApp.addLog("onExposed");
}
returning from device camera after
shooting video
Check the Application.activate()
The system invokes this method when it
brings this application to the
foreground. By default, this method
does nothing. Override this method to
perform additional processing when
being brought to the foreground.
If you override the Screen.onUiEngineAttached(boolean) method, you can be notified when the screen is attached or detached from the UI --- basically when it's pushed or popped from the screen stack.
I had to do a similar thing and found it's very confusing because onExposed() can be called multiple times in uncertain timing.
To detect returning from screen B in screen A (main screen), I used screen B's onUiEngineAttached(false) which is called when it is popped.
To use callback:
public interface Ievent {
public void backFromScreenBEvent();
}
Screen A:
public class ScreenA extends MainScreen implements Ievent
{
private ScreenB screenB;
// constructor
public ScreenA()
{
screenB = new ScreenB(this); // pass over Ievent
// ....
}
public void backFromScreenBEvent()
{
// screen B is returning, do something
}
Screen B:
public final class ScreenB extends MainScreen
{
private Ievent event;
// constructor
public ScreenB(final Ievent event)
{
this.event = event;
// ...
}
protected void onUiEngineAttached(boolean attached) {
super.onUiEngineAttached(attached);
if (!attached) {
event.backFromScreenBEvent(); // notify event
}
}

Resources