0

Xamarin Android với MVVM Light

Overview
Bài viết này giới thiệu về 1 framework phổ biến hay được dùng chung với Xamarin đó là MVVMLight của Laurent Bugnion. Mục đích của bài viết là chia sẻ kinh nghiệm, giúp người đọc có thể tạo 1 app đơn giản có áp dụng binding và navigation của framework MVVMLight theo từng bước một.

Key Points

Screenshot (13).png


Đầu tiên chúng ta cần tạo 1 blank project Xamarin.Android, sau đó cài đặt MVVMLight từ NuGet. Sau khi cài đặt xong, thư mục là tên là **_ViewModel_** sẽ được tự động tạo ra. Thư mục này sẽ chứa các **_ViewModel_** tương ứng với mỗi màn hình trong app, và 1 class là **_ViewModelLocator_**, class này là nơi đăng kí các **_ViewModel_** và **_ScreenKey_** dành cho việc điều hướng trong app.

Cấu trúc project
Screenshot (18).png


Trong framework MVVMLight có sử dụng **_NavigationService_** để map giữa key khai báo trong **_ViewModelLocator_** và Activity app.
nav.Configure(ViewModelLocator.SecondPageKey, typeof(SecondActivity));

Các Activity trong app nên được kế thừa từ **_ActivityBase_** nằm trong namespace **_GalaSoft.MvvmLight.Views_**.

Cốt lõi của MVVMLight là ở phần binding giữa các UI Elements và ViewModels, việc này sẽ được thực hiên trong Activity (Android) hoặc là ViewController (iOS). Reference tới các bindings phải được giữ để tránh việc các bindings bị GC collect, trong ví dụ phía dưới chúng được giữ trong 1 List.

    [Activity(Label = "MVVM LIGHT SAMPLE", MainLauncher = true, Icon = "@drawable/icon")]
    public partial class MainActivity
    {

        private readonly List<Binding> _bindings = new List<Binding>();

        private MainPageViewModel Vm
        {
            get
            {
                return App.Locator.Main;
            }

        }

        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            SetContentView(Resource.Layout.main_page);

            _bindings.Add(this.SetBinding(() => EditMessage.Text, () => Vm.Message));
            _bindings.Add(this.SetBinding(() => Vm.Message, () => TextMessage.Text));

            ButtonNavigate.SetCommand("Click", Vm.GoToSecondPage);

            //Setting click event can be done with this :
            //ButtonNavigate.Click += (sender, e) => { Vm.GoToSecondPage.Execute(null) };
        }
    }

Ngoài ra các properties của các UI Elements trong view sẽ được tách ra và chứa trong 1 partial class của Activity và được nest vào partial class chính của Activity nhờ vào plugin _File_ _Nesting_. Đoạn code dưới nằm trong file **_MainActivity.ui.cs_**.
    public partial class MainActivity : ActivityBase
    {

        private TextView _textMessage;

        public TextView TextMessage
        {
            get
            {
                return _textMessage
                       ?? (_textMessage = FindViewById<TextView>(Resource.Id.text_message));
            }
        }

        private Button _buttonNavigate;

        public Button ButtonNavigate
        {
            get
            {
                return _buttonNavigate
                       ?? (_buttonNavigate = FindViewById<Button>(Resource.Id.button_to_secondpage));
            }
        }

        private EditText _editMessage;

        public EditText EditMessage
        {
            get
            {
                return _editMessage
                       ?? (_editMessage = FindViewById<EditText>(Resource.Id.edit_message));
            }
        }
    }

**MVVMLight App**
App trong bài viết này là 1 app đơn giản, sử dụng cơ chế _Navigate_ có sẵn trong framework MVVMLight để điều hướng giữa 2 màn hình.

Main Page
Screenshot (11).png


Second Page
![Screenshot (12).png](/uploads/d599ae43-1305-40cc-844b-34c0ee47a714.png)
**Make it!**
Trong thư mục ViewModel tạo ra 2 ViewModel cho MainPage và SecondPage là _MainPageViewModel_, _SecondPageViewModel_. Chú ý các class này sẽ phải kế thừa từ **_ViewModelBase_** trong namespace **_GalaSoft.MvvmLight_** để có thể binding được. Đây là base class cho tất cả các ViewModel.

_MainPageViewModel_ :
    public class MainPageViewModel : ViewModelBase
    {
        private INavigationService nav;

        public MainPageViewModel(INavigationService nav)
        {
            this.nav = nav;
        }

        private string _message;

        public string Message
        {
            get
            {
                return _message;
            }

            set
            {
                _message = value;
                RaisePropertyChanged(() => Message);
            }
        }

        private RelayCommand _goToSecondPage;

        /// <summary>
        /// Gets the GoToSecondPage.
        /// </summary>
        public RelayCommand GoToSecondPage
        {
            get
            {
                return _goToSecondPage
                    ?? (_goToSecondPage = new RelayCommand(
                    () =>
                    {
                        nav.NavigateTo(ViewModelLocator.SecondPageKey, Message);
                    }));
            }
        }
    }

RelayCommand _GoToSecondPage_ sẽ được gọi khi người dùng click vào nút Go to second page, nó sẽ thực hiện việc điều hướng sang màn hình thứ 2 của app.

_SecondPageViewModel_ :
    public class SecondPageViewModel : ViewModelBase
    {
        public INavigationService nav;

        public SecondPageViewModel(INavigationService nav)
        {
            this.nav = nav;
            InitTaskList();
        }

        public ObservableCollection<TaskModel> TaskList { get; set; }

        private RelayCommand _addTask;
        public RelayCommand AddTask
        {
            get
            {
                return _addTask ?? (_addTask = new RelayCommand(() =>
                {
                    TaskList.Add(new TaskModel
                    {
                        TaskName = string.Format("To Do #{0}", TaskList.Count + 1),
                        TaskDate = DateTime.UtcNow.ToString()
                    });
                }));
            }
        }

        public void InitTaskList()
        {
            TaskList = new ObservableCollection<TaskModel>()
            {
                new TaskModel
                {
                    TaskName = "To Do #1",
                    TaskDate = DateTime.UtcNow.ToString()
                },
                new TaskModel
                {
                    TaskName = "To Do #2",
                    TaskDate = DateTime.UtcNow.ToString()
                },
                new TaskModel
                {
                    TaskName = "To Do #3",
                    TaskDate = DateTime.UtcNow.ToString()
                },
                new TaskModel
                {
                    TaskName = "To Do #4",
                    TaskDate = DateTime.UtcNow.ToString()
                },
                new TaskModel
                {
                    TaskName = "To Do #5",
                    TaskDate = DateTime.UtcNow.ToString()
                },
                new TaskModel
                {
                    TaskName = "To Do #6",
                    TaskDate = DateTime.UtcNow.ToString()
                },
                new TaskModel
                {
                    TaskName = "To Do #7",
                    TaskDate = DateTime.UtcNow.ToString()
                },
                new TaskModel
                {
                    TaskName = "To Do #8",
                    TaskDate = DateTime.UtcNow.ToString()

                },
                new TaskModel
                {
                    TaskName = "To Do #9",
                    TaskDate = DateTime.UtcNow.ToString()
                },
            };
        }
    }

    public class TaskModel
    {
        public string TaskName { get; set; }
        public string TaskDate { get; set; }
    }

Và cũng cần phải đăng kí các ViewModel trong class ViewModelLocator:
    public class ViewModelLocator
    {
        /// <summary>
        /// The key used by the NavigationService to go to the second page.
        /// </summary>
        public const string SecondPageKey = "SecondPage";

        //Property for access ViewModel from Activity
        public MainPageViewModel Main
        {
            get
            {
                return ServiceLocator.Current.GetInstance<MainPageViewModel>();
            }
        }

        public SecondPageViewModel Second
        {
            get
            {
                return ServiceLocator.Current.GetInstance<SecondPageViewModel>();
            }
        }

        static ViewModelLocator()
        {
            ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
            //Register 2 ViewModels
            SimpleIoc.Default.Register<MainPageViewModel>();
            SimpleIoc.Default.Register<SecondPageViewModel>();
        }
    }

**_Activity và layout cho các Activity đó_**:

_MainActivity_ :
    [Activity(Label = "MVVM LIGHT SAMPLE", MainLauncher = true, Icon = "@drawable/icon")]
    public partial class MainActivity
    {

        private readonly List<Binding> _bindings = new List<Binding>();

        private MainPageViewModel Vm
        {
            get
            {
                return App.Locator.Main;
            }

        }

        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            SetContentView(Resource.Layout.main_page);

            _bindings.Add(this.SetBinding(() => EditMessage.Text, () => Vm.Message));
            _bindings.Add(this.SetBinding(() => Vm.Message, () => TextMessage.Text));

            ButtonNavigate.SetCommand("Click", Vm.GoToSecondPage);

            //Set click event can be done with this :
            //ButtonNavigate.Click += (sender, e) => { Vm.GoToSecondPage.Execute(null) };
        }
    }

_MainActivity.ui_ :
    public partial class MainActivity : ActivityBase
    {

        private TextView _textMessage;

        public TextView TextMessage
        {
            get
            {
                return _textMessage
                       ?? (_textMessage = FindViewById<TextView>(Resource.Id.text_message));
            }
        }

        private Button _buttonNavigate;

        public Button ButtonNavigate
        {
            get
            {
                return _buttonNavigate
                       ?? (_buttonNavigate = FindViewById<Button>(Resource.Id.button_to_secondpage));
            }
        }

        private EditText _editMessage;

        public EditText EditMessage
        {
            get
            {
                return _editMessage
                       ?? (_editMessage = FindViewById<EditText>(Resource.Id.edit_message));
            }
        }
    }

`main_page.axml` :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:text="Press the button to go to Second Page"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18dp"
        android:gravity="center"
        android:layout_marginTop="20dp"
        android:layout_marginBottom="20dp" />
    <Button
        android:text="Go to second page"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button_to_secondpage"
        android:layout_marginLeft="12dp"
        android:layout_marginRight="12dp" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/edit_message"
        android:gravity="center"
        android:layout_marginTop="30dp"
        android:textSize="24dp"
        android:layout_marginLeft="12dp"
        android:layout_marginRight="12dp" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/text_message"
        android:gravity="center"
        android:layout_marginTop="30dp"
        android:textSize="24dp"
        android:layout_marginLeft="12dp"
        android:layout_marginRight="12dp" />
</LinearLayout>

SecondActivity :

    [Activity(Label = "MVVM LIGHT SAMPLE", MainLauncher = false, Icon = "@drawable/icon")]
    public partial class SecondActivity
    {
        private readonly List<Binding> _bindings = new List<Binding>();

        private SecondPageViewModel Vm
        {
            get
            {
                return App.Locator.Second;
            }

        }

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.second_page);
            var nav = (NavigationService)Vm.nav;
            var p = nav.GetAndRemoveParameter<string>(Intent);

            if (string.IsNullOrEmpty(p))
            {
                TextMessage.Text = "No navigation parameter";
            }
            else
            {
                TextMessage.Text = "Navigation parameter: " + p;
            }

            AddButton.Click += (sender, e) =>
            {
                Vm.AddTask.Execute(null);
            };

            TaskListview.Adapter = Vm.TaskList.GetAdapter(GetTaskAdapter);

        }

        private View GetTaskAdapter(int position, TaskModel model, View convertView)
        {
            ViewHolder vh;
            if(convertView == null)
            {
                convertView = LayoutInflater.Inflate(Resource.Layout.item_task_list, null, false);
                vh = new ViewHolder
                {
                    TaskName = convertView.FindViewById<TextView>(Resource.Id.text_task_name),
                    TaskDate = convertView.FindViewById<TextView>(Resource.Id.text_task_date)
                };
                convertView.Tag = vh;
            }

            vh = convertView.Tag as ViewHolder;

            vh.TaskName.Text = model.TaskName;
            vh.TaskDate.Text = model.TaskDate;

            return convertView;
        }

        class ViewHolder : Java.Lang.Object
        {
            public TextView TaskName { get; set; }
            public TextView TaskDate { get; set; }
        }

    }

_SecondActivity.ui_ :
    public partial class SecondActivity : ActivityBase
    {

        private Button _addButton;

        public Button AddButton
        {
            get
            {
                return _addButton
                       ?? (_addButton = FindViewById<Button>(Resource.Id.addButton));
            }
        }

        private ListView _taskListview;

        public ListView TaskListview
        {
            get
            {
                return _taskListview
                       ?? (_taskListview = FindViewById<ListView>(Resource.Id.listView));
            }
        }

        private TextView _textMessage;

        public TextView TextMessage
        {
            get
            {
                return _textMessage
                       ?? (_textMessage = FindViewById<TextView>(Resource.Id.text_message));
            }
        }
    }

`second_page.axml` :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:minWidth="25px"
    android:minHeight="25px">
    <TextView
        android:id="@+id/text_message"
        android:layout_marginTop="10dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal" />
    <Button
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="10dp"
        android:id="@+id/addButton"
        android:text="Add" />
    <ListView
        android:minWidth="25px"
        android:minHeight="25px"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/listView" />
</LinearLayout>

`item_task_list.axml`
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:id="@+id/text_task_name"
        android:layout_height="wrap_content"
        android:layout_width="match_parent" />
    <TextView
        android:id="@+id/text_task_date"
        android:layout_height="wrap_content"
        android:layout_width="match_parent" />
</LinearLayout>

Và cuối cùng chúng ta cần phải tạo 1 static class là App, đây là nơi mà việc map giữa 1 ScreenKey đã khai báo trong **_ViewModelLocaltor_** và 1 Activity trong app được thực hiện. Việc thực hiện điều hướng trong MVVMLight sẽ không trực tiếp như thông thường ta thường làm với cách `StartActivity(typeof(SecondActivity));`, nó sẽ thực hiện gián tiếp qua NavigationService theo ScreenKey đã được map với Activity. Đồng thời việc truy cập đến các property hay **_RelayCommand_** trong ViewModel trong Activity sẽ được thông qua 1 static object của class **_ViewModelLocator_** được khai báo trong class này.
 public static class App
    {
        private static ViewModelLocator _locator;

        public static ViewModelLocator Locator
        {
            get
            {
                if (_locator == null)
                {
                    // Initialize the MVVM Light DispatcherHelper.
                    // This needs to be called on the UI thread.
                    DispatcherHelper.Initialize();

                    // Configure and register the MVVM Light NavigationService
                    var nav = new NavigationService();
                    SimpleIoc.Default.Register<INavigationService>(() => nav);
                    //Map a SecondPageKey which defined in ViewModelLocator with SecondActivity
                    nav.Configure(ViewModelLocator.SecondPageKey, typeof(SecondActivity));

                    // Register the MVVM Light DialogService
                    SimpleIoc.Default.Register<IDialogService, DialogService>();

                    _locator = new ViewModelLocator();
                }

                return _locator;
            }
        }
    }

Đây là 1 demo nhỏ về sự kết hợp giữa Xamarin.Android và MVVMLight, phần tiếp theo sẽ nói rõ hơn về việc binding trong MVVMLight cũng như tại sao không navigate theo cách `StartActivity(typeof(SecondActivity));` trong MVVMLight và những lợi ích của MVVMLight.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí