Android Data Binding Library Tutorial

Android provides data binding library that helps in creating apps with less glue code to bind application logic and layouts. With data binding, you don’t need to find views in activities or fragments, set view attributes to properties of data model object and add event handlers to views, all this is taken care of by data binding framework using the data binding component classes and data binding expressions that you define in layouts. Data binding compiler tool generates necessary code to make the binding work using xml layout and data binding component classes defined using annotations.

Table of Contents

Project Setup

To use data binding, you need to enable it by adding below configuration to module build.gradle file.

android { .... dataBinding { enabled = true } }

Data Binding

To use data binding framework, first you need to create data model object, then define your activity layout with data binding elements and expressions. Notice in the below example that layout’s root element is layout and data model object variable is declared using variable element under data element.

To bind a property of data model object to an attribute of view, we need to use @{} expression as shown in the example below.

<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:bind="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="accountSettings" type="zoftino.com.databinding.AccountSetting"/> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" tools:context="zoftino.com.databinding.MainActivity"> <EditText android:id="@+id/account_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="account name" android:text="@{accountSettings.accountName}" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>

After defining the layout, compiler will generate binding classes. For example, if layout name is activity_main.xml, then it will generate ActivityMainBinding class. In the activity, you need to inflate the layout and set variable objects defined in the layout using the generated binding class as shown below.

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMainBinding binding = DataBindingUtil. setContentView(this, R.layout.activity_main); AccountSetting accountSetting = new AccountSetting(); accountSetting.setAccountName("test"); binding.setAccountSettings(accountSetting); }

If you run the above example, it will display data object value in UI widget. But using @{} expression as shown above will just bind the value of the property of data object to the attribute of View. But it won’t refresh the UI widget with the changed value of the property of the object and it won’t populate the data object with user entered or modified value into the UI widget.

Binding and Inverse Binding or Two Way Binding

As shown above, using @{} data binding expression will just display the data in UI, but user changes will not be set to data object. To make the binding work both ways, meaning to make the data binding framework populate the user inputted data to data object, you need to use inverse binding feature of data binding framework. By using @={} data binding expression in xml layout, you can achieve two-way binding.

By default inverse binding works with only few attributes such as AbsListView’s android:selectedItemPosition, CalendarView’s android:date, CompoundButton’s android:checked, RadioGroup’s android:checkedButton, RatingBar’s android:rating, SeekBar’s android:progress and TextView’s android:text attributes, for which there are listeners. Attribute listeners are required to let the data binding framework know when to capture the value from UI widgets and set it to the property of data object.

See the following section to know how to implement two way binding for attributes which don’t have framework provided inverse binding listeners.

<EditText android:id=”@+id/account_name” android:text=”@={accountSettings.accountName}” ....... />

Binding Every Time Data Object’s Values Change

In a scenario where value of a property of data object changes after you create the data object, populate it with data and do the binding, the changed value will not be reflected in UI by implementing the data binding in the ways discussed so far.

To make the data binding framework set the changed values of properties of data object to UI widgets, you need to use data change notification mechanism provided in data binding framework. Data binding framework provides Observable object, observable collection and observable fields using which you can make binding to occur every time values of data object change.

Let’s see how we can use Observable object to implement data change notification so that data changes in data object will be updated in UI.

First, data model object needs to extend BaseObservable, then you need to annotate using Bindable the getters methods of properties for which you want the data change notification to be implemented and finally in the setter methods, you need to call notifyPropertyChanged method passing integer value of the property using BR class as shown below. Do the rest of the binding steps and use data binding expression in layout.

public class AccountSetting extends BaseObservable { private String accountName; @Bindable public String getAccountName() { return accountName; } public void setAccountName(String accountName) { this.accountName = accountName; notifyPropertyChanged(BR.accountName); } }

Binding Event Listeners

To handle View events using data binding library, you need to define a method that handles the event, in xml layout, declare the object that contain the event handler method and using binding expressions you can bind the event handler method to event attribute name.

.... <data> <variable name="eventListener" type="zoftino.com.databinding.SettingsEventListener"/> </data> <Button android:id="@+id/save_b" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Save Settings" android:onClick="@{eventListener.onClickSaveSettings()}"/> . . .

With the above expression, handler method signature should match to the method signature of event listener object, for example for onClick event handling, the handler method you define should return void and take View object as parameter like the method signature of onClick method of View.OnClickListener.

With below expression to bind event handler to event attribute, only event handler method’s return type should match to event listener object’s return type. For onClick event attribute, your handler method should return void and you are free to add any arguments to it.

.... <data> <variable name="eventListener" type="zoftino.com.databinding.SettingsEventListener"/> <variable name="user" type="zoftino.com.databinding.UserInfo"/> </data> <Button android:id="@+id/save_b" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Save Settings" android:onClick="@{() -> eventListener.onClickSaveSettings(user)}"/> . . .

Two Way Binding for RadioGroup RadioButtons

Let’s see how to implement binding of a property of data object to RadioGroup and inverse binding of user selection of one of the radio buttons in radio group to the property of data object.

Below is the data object with one property.

public class AccountSetting{ private String communicationMode; public String getCommunicationMode() { return communicationMode; } public void setCommunicationMode(String communicationMode) { this.communicationMode = communicationMode; } }

Below is the expression to bind value of the property of the object to RadioButtons of RadioGroup.

<RadioGroup ....> <RadioButton .... android:checked="@{accountSettings.communicationMode.equals(@string/email)}" android:text="email"/> <RadioButton .... android:checked="@{accountSettings.communicationMode.equals(@string/sms)}" android:text="SMS" /> </RadioGroup>

To implement inverse binding, we need to implement a method in handler object and bind it to onClick events of RadioButtons. The listener method will set object’s property to user selected RadioButton’s value.

Below is the listener method.

public void onCommunicationMode(String commMode){ accountSetting.setCommunicationMode(commMode); }

Below is the onClick event handling data binding expression.

<RadioGroup ....> <RadioButton .... android:checked="@{accountSettings.communicationMode.equals(@string/email)}" android:onClick="@{() -> eventListener.onCommunicationMode(@string/email)}"/> <RadioButton .... android:checked="@{accountSettings.communicationMode.equals(@string/sms)}" android:onClick="@{() -> eventListener.onCommunicationMode(@string/sms)}" /> </RadioGroup>

Two Way Binding For Spinner

Two way binding works for certain attributes of UI widgets, because framework provides listeners for those attributes and does the reverse binding. But if you need two way binding for attributes that don’t have framework provided listeners, you will have to define BindingAdapter and InverseBindingAdapter and let the framework know when to use InverseBindingAdapter by using InverseBindingListener.

Let’s see how to implement custom binding and reverse binding for spinner. First in the xml layout, we add custom attribute to spinner and use two way binding expression to bind it to a property of data object, as shown below, bind:pmtOpt is custom attribute of spinner.

<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:bind="http://schemas.android.com/apk/res-auto"> <data> <import type="android.view.View" /> <variable name="accountSettings" type="zoftino.com.databinding.AccountSetting"/> <variable name="eventListener" type="zoftino.com.databinding.SettingsEventListener"/> <variable name="spinAdapter" type="android.widget.ArrayAdapter"/> </data> . . . <android.support.v7.widget.AppCompatSpinner android:id="@+id/default_payment_option" . . . . bind:pmtOpt="@={accountSettings.defaultPaymentOption}" app:adapter="@{spinAdapter}"/> </layout>

Then we need to define BindingAdapter to provide behavior for data binding framework to use when binding event for the target attribute occurs.

For our example custom attribute bind:pmtOpt, when binding happens, we want to set default selection of spinner, add item selection listener to spinner and call inverse binding listener in the item selection listener handler. By calling onChange method on InverseBindingListener in OnItemSelectedListener of spinner, we are informing databinding framework to call InverseBindingAdapter.

@BindingAdapter(value = {"bind:pmtOpt", "bind:pmtOptAttrChanged"}, requireAll = false) public static void setPmtOpt(final AppCompatSpinner spinner, final String selectedPmtOpt, final InverseBindingListener changeListener) { spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { changeListener.onChange(); } @Override public void onNothingSelected(AdapterView<?> adapterView) { changeListener.onChange(); } }); spinner.setSelection(getIndexOfItem(spinner, selectedPmtOpt)); }

Then define the inverse binding adapter that captures user selected value of the spinner and returns it to the framework so that it can set the value to corresponding property of the data object.

@InverseBindingAdapter(attribute = "bind:pmtOpt", event = "bind:pmtOptAttrChanged") public static String getPmtOpt(final AppCompatSpinner spinner) { return (String)spinner.getSelectedItem(); }

Android Data Binding Example

Let’s take account setting feature of e-commerce app which uses widgets such as TextView, Checkbox, RadioButton, Spinner, Seekbar, ToggleButton, Switch, RatingBar and Button to show data binding, inverse binding and binding event handlers. The example app loads and saves data to Firebase firestore database.

Activity Layout

<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:bind="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View" /> <variable name="accountSettings" type="zoftino.com.databinding.AccountSetting"/> <variable name="eventListener" type="zoftino.com.databinding.SettingsEventListener"/> <variable name="spinAdapter" type="android.widget.ArrayAdapter"/> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" tools:context="zoftino.com.databinding.MainActivity"> <TextView android:id="@+id/settings" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/settings" android:textAppearance="@style/TextAppearance.AppCompat.Headline" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/account_name_l" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/accountName" app:layout_constraintBaseline_toBaselineOf="@+id/account_name" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/account_name" /> <EditText android:id="@+id/account_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="account name" android:text="@={accountSettings.accountName}" app:layout_constraintLeft_toRightOf="@+id/account_name_l" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/settings" /> <TextView android:id="@+id/notifications_toggle_l" android:layout_marginTop="8dp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/notificationToggle" app:layout_constraintBaseline_toBaselineOf="@+id/notifications" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/notifications" /> <ToggleButton android:id="@+id/notifications" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@={accountSettings.notifications}" android:layout_marginTop="8dp" app:layout_constraintLeft_toRightOf="@+id/notifications_toggle_l" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/account_name" /> <TextView android:id="@+id/notification_types_l" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="@string/notificationType" app:layout_constraintHorizontal_bias="0.199" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/notifications_toggle_l" /> <CheckBox android:id="@+id/deals" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Deals" android:checked="@={accountSettings.deals}" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/promos" app:layout_constraintTop_toBottomOf="@+id/notification_types_l" /> <CheckBox android:id="@+id/promos" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Promos" android:checked="@={accountSettings.promos}" app:layout_constraintLeft_toRightOf="@+id/deals" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/notification_types_l" /> <TextView android:id="@+id/communication_mode_l" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/communicationMode" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/communication_mode" app:layout_constraintTop_toBottomOf="@+id/promos" /> <RadioGroup android:id="@+id/communication_mode" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:orientation="vertical" app:layout_constraintLeft_toRightOf="@+id/communication_mode_l" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/promos"> <RadioButton android:id="@+id/emailOption" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@{accountSettings.communicationMode.equals(@string/email)}" android:onClick="@{() -> eventListener.onCommunicationMode(@string/email)}" android:text="email" /> <RadioButton android:id="@+id/smsOption" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@{accountSettings.communicationMode.equals(@string/sms)}" android:onClick="@{() -> eventListener.onCommunicationMode(@string/sms)}" android:text="SMS" /> </RadioGroup> <TextView android:id="@+id/default_payment_option_l" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/defaultPaymentOption" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/default_payment_option" app:layout_constraintTop_toBottomOf="@+id/communication_mode" /> <android.support.v7.widget.AppCompatSpinner android:id="@+id/default_payment_option" android:layout_width="150dp" android:layout_height="wrap_content" bind:pmtOpt="@={accountSettings.defaultPaymentOption}" app:adapter="@{spinAdapter}" app:layout_constraintLeft_toRightOf="@+id/default_payment_option_l" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/communication_mode" /> <TextView android:id="@+id/price_drop_percent_l" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/priceDropPercent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/price_drop_percent" app:layout_constraintTop_toBottomOf="@+id/default_payment_option" /> <SeekBar android:id="@+id/price_drop_percent" android:layout_width="150dp" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:min="5" android:max="80" android:progress="@={accountSettings.priceDropPercent}" app:layout_constraintLeft_toRightOf="@+id/price_drop_percent_l" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/default_payment_option" /> <TextView android:id="@+id/show_product_reviews_l" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/showProductReviews" app:layout_constraintHorizontal_bias="0.158" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/price_drop_percent_l" /> <RatingBar android:id="@+id/show_product_reviews" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:rating="@={accountSettings.showProductReviews}" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/show_product_reviews_l" /> <TextView android:id="@+id/promo_notification_l" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/promoNotification" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/promo_notification" app:layout_constraintTop_toBottomOf="@+id/show_product_reviews" /> <Switch android:id="@+id/promo_notification" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:checked="@={accountSettings.promoNotification}" app:layout_constraintLeft_toRightOf="@+id/promo_notification_l" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/show_product_reviews" /> <Button android:id="@+id/save_b" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Save Settings" android:onClick="@{(v) -> eventListener.onClickSaveSettings(v)}" style="@style/Widget.AppCompat.Button.Colored" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/promo_notification" /> </android.support.constraint.ConstraintLayout> </layout>

Activity

import android.content.Context; import android.content.SharedPreferences; import android.databinding.DataBindingUtil; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.widget.ArrayAdapter; import android.widget.Toast; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestore; import java.util.UUID; import zoftino.com.databinding.databinding.ActivityMainBinding; public class MainActivity extends AppCompatActivity implements SettingsEventListener{ private static final String TAG = "MainActivity"; private FirebaseFirestore firestoreDB; private String uniqueIdentifier; private ActivityMainBinding binding; private AccountSetting accountSetting; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_main); binding.setEventListener(this); String[] paymentOptions = getResources().getStringArray(R.array.payment_options); binding.setSpinAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item, paymentOptions)); firestoreDB = FirebaseFirestore.getInstance(); loadSettings(); } private void loadSettings(){ firestoreDB.collection("accountConfig").document(getInstallationIdentifier()) .get() .addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() { @Override public void onComplete(@NonNull Task<DocumentSnapshot> task) { if (task.isSuccessful()) { if(task.getResult().exists()) { accountSetting = task.getResult().toObject(AccountSetting.class); if(accountSetting.getCommunicationMode() == null){ accountSetting.setCommunicationMode(""); } }else { accountSetting = new AccountSetting(); } //bind data binding.setAccountSettings(accountSetting); }else { Log.d(TAG, "Error loading settings", task.getException()); } } }); } public void onClickSaveSettings(View v) { accountSetting.setAccountName(accountSetting.getAccountName().toUpperCase()); saveSettings(accountSetting); } public void onCommunicationMode(String commMode){ accountSetting.setCommunicationMode(commMode); } private void saveSettings(AccountSetting accountSetting){ firestoreDB.collection("accountConfig").document(getInstallationIdentifier()) .set(accountSetting) .addOnCompleteListener(new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { if(task.isSuccessful()){ Toast.makeText(MainActivity.this, "settings have been saved", Toast.LENGTH_SHORT).show(); }else{ Toast.makeText(MainActivity.this, "settings could not be saved", Toast.LENGTH_SHORT).show(); } } }); } public synchronized String getInstallationIdentifier() { if (uniqueIdentifier == null) { SharedPreferences sharedPrefs = this.getSharedPreferences( "UNIQUE_ID", Context.MODE_PRIVATE); uniqueIdentifier = sharedPrefs.getString("UNIQUE_ID", null); if (uniqueIdentifier == null) { uniqueIdentifier = UUID.randomUUID().toString(); SharedPreferences.Editor editor = sharedPrefs.edit(); editor.putString("UNIQUE_ID", uniqueIdentifier); editor.commit(); } } return uniqueIdentifier; } }

Data Model

public class AccountSetting extends BaseObservable { private String accountName; private boolean notifications; private boolean deals; private boolean promos; private String communicationMode; private String defaultPaymentOption; private int priceDropPercent; private int showProductReviews; private boolean promoNotification ; @Bindable public String getAccountName() { return accountName; } public void setAccountName(String accountName) { this.accountName = accountName; notifyPropertyChanged(BR.accountName); } public boolean isNotifications() { return notifications; } public void setNotifications(boolean notifications) { this.notifications = notifications; } public boolean isDeals() { return deals; } public void setDeals(boolean deals) { this.deals = deals; } public boolean isPromos() { return promos; } public void setPromos(boolean promos) { this.promos = promos; } public String getCommunicationMode() { return communicationMode; } public void setCommunicationMode(String communicationMode) { this.communicationMode = communicationMode; } @Bindable public String getDefaultPaymentOption() { return defaultPaymentOption; } public void setDefaultPaymentOption(String defaultPaymentOption) { this.defaultPaymentOption = defaultPaymentOption; notifyPropertyChanged(BR.defaultPaymentOption); } public int getPriceDropPercent() { return priceDropPercent; } public void setPriceDropPercent(int priceDropPercent) { this.priceDropPercent = priceDropPercent; } public int getShowProductReviews() { return showProductReviews; } public void setShowProductReviews(int showProductReviews) { this.showProductReviews = showProductReviews; } public boolean isPromoNotification() { return promoNotification; } public void setPromoNotification(boolean promoNotification) { this.promoNotification = promoNotification; } }

Binding Adapter

import android.databinding.BindingAdapter; import android.databinding.InverseBindingAdapter; import android.databinding.InverseBindingListener; import android.support.v7.widget.AppCompatSpinner; import android.view.View; import android.widget.Adapter; import android.widget.AdapterView; public class SettingsBindingAdapter { @BindingAdapter(value = {"bind:pmtOpt", "bind:pmtOptAttrChanged"}, requireAll = false) public static void setPmtOpt(final AppCompatSpinner spinner, final String selectedPmtOpt, final InverseBindingListener changeListener) { spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { changeListener.onChange(); } @Override public void onNothingSelected(AdapterView<?> adapterView) { changeListener.onChange(); } }); spinner.setSelection(getIndexOfItem(spinner, selectedPmtOpt)); } @InverseBindingAdapter(attribute = "bind:pmtOpt", event = "bind:pmtOptAttrChanged") public static String getPmtOpt(final AppCompatSpinner spinner) { return (String)spinner.getSelectedItem(); } private static int getIndexOfItem(AppCompatSpinner spinner, String item){ Adapter a = spinner.getAdapter(); for(int i=0; i<a.getCount(); i++){ if((a.getItem(i)).equals(item)){ return i; } } return 0; } }

Event Handler