Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I use MVVM and Xamarin.Forms ListView ItemSelected event to to retrieve a bound property of the item selected?

I am using Xamarin.Forms and attempting to use the MVVM architecture.

I have a ContentPage that has a simple List View on it. The List View has its item source property bound to my ViewModel. I am able to populate the list just fine. All of the items display as they should.

When I click on an item from the list, I need to navigate to a different page based on the item selected. This is what is not working. It will ONLY work if I reference my underlying Model directly (which is NOT what I want to do)

I have the ListView.ItemSelected event coded. However, the Item Selected event can't determine what the "display_text" is of the List Item selected. How do I achieve this without having to reference my model directly from my View (Page)?

MainPage Code:

public partial class MainPage : ContentPage
{
    private int intPreJobFormID = 0;
    public MainPage()
    {
        InitializeComponent();
        BindingContext = new MainPageViewModel();

        Label lblHeader = new Label
        {
            Text = "HW Job Assessments",
            FontSize = ViewGlobals.lblHeader_FontSize,
            HorizontalOptions = ViewGlobals.lblHeader_HorizontalOptions,
            FontAttributes = ViewGlobals.lblHeader_FontAttributes,
            TextColor = ViewGlobals.lblHeader_TextColor
        };

        //Create the Main Menu Items List View
        var lvMain = new ListView
        {
            //Pull down to refresh list
            IsPullToRefreshEnabled = true,

            //Define template for displaying each item.
            //Argument of DataTemplate constructor is called for each item. It must return a Cell derivative.
            ItemTemplate = new DataTemplate(() =>
            {
                //Create views with bindings for displaying each property.
                Label lblDisplayText = new Label();
                lblDisplayText.SetBinding(Label.TextProperty, "display_text");
                lblDisplayText.FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label));


                //Return an assembled ViewCell.
                return new ViewCell
                {
                    View = new StackLayout
                    {
                        Padding = new Thickness(20, 5, 0, 0),
                        Orientation = StackOrientation.Horizontal,
                        Children =
                        {
                            new StackLayout
                            {
                                VerticalOptions = LayoutOptions.Center,
                                Spacing = 0,
                                Children =
                                {
                                    lblDisplayText
                                }
                            }
                        }
                    }
                };
            })
        };
        lvMain.SetBinding(ListView.ItemsSourceProperty, "MainMenuItems");
        lvMain.ItemSelected += lvMain_ItemSelected;

    }

    private async void lvMain_ItemSelected(object sender, SelectedItemChangedEventArgs e)
    {
        var lv = (ListView)sender;

        if (e.SelectedItem == null)
        {
            return; //ItemSelected is called on deselection which results in SelectedItem being set to null
        }

        //var item = e.SelectedItem as TableMainMenuItems; //This is what I DON'T want to use because it references my Model directly.
      var item = e.SelectedItem;

        switch (item.display_text) //This is what I need. I can't get this unless I reference my Model "TableMainMenuItems" directly.
        {
            case "Forms List":
                await Navigation.PushAsync(new FormsListPage());
                break;

            case "New Pre-Job":
                await Navigation.PushAsync(new PreJobPage(intPreJobFormID));
                break;
        }

        //Comment out if you want to keep selections
        lv.SelectedItem = null;
    }
}

MainPageViewModel Code:

public class MainPageViewModel
{

    public int intPreJobFormID = 0;
    private DatabaseApp app_database = ViewModelGlobals.AppDB;
    private DatabaseFormData formdata_database = ViewModelGlobals.FormDataDB;
    private IEnumerable<TableMainMenuItems> lstMaineMenuItems;
    private IEnumerable<TableFormData> lstRecentJobs;

    public string DisplayText { get; set; }
    public IEnumerable<TableMainMenuItems> MainMenuItems
    {
        get { return lstMaineMenuItems; }
        set
        {
            lstMaineMenuItems = value;
        }
    }

    public IEnumerable<TableFormData> RecentJobs
    {
        get { return lstRecentJobs; }
        set
        {
            lstRecentJobs = value;
        }
    }

    public MainPageViewModel()
    {
        intPreJobFormID = app_database.GetForm(0, "Pre-Job Assessment").Id;
        MainMenuItems = app_database.GetMainMenuItems();
        RecentJobs = formdata_database.GetFormAnswersForForm(intPreJobFormID).OrderByDescending(o => o.date_modified);

    }

}
like image 702
Nitestar 24 Avatar asked Dec 29 '16 21:12

Nitestar 24


1 Answers

There are 2 ways to retrieve the bound property of the item selected.

I personally prefer to handle the logic in the view because it keeps the code simpler.

1. Handle Logic In The View

cast item as your model type: var item = e.SelectedItem as TableMainMenuItems;

async void lvMain_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
    var listView = (ListView)sender;
    listView.SelectedItem = null; 

    if (e?.SelectedItem is TableMainMenuItems item)
    {
        switch (item.display_text)
        {
            case "Forms List":
                await Navigation.PushAsync(new FormsListPage());
                break;

            case "New Pre-Job":
                await Navigation.PushAsync(new PreJobPage(intPreJobFormID));
                break;
        }
    }
}

2. Handle Logic In The View Model

View

Use a Command<T> to loosely couple the ItemSelected logic between the View and the View Model. Create an event in the View Model that will fire when the View Model has completed the logic.

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        var viewModel = new MainPageViewModel();
        BindingContext = viewModel;
        ...
        viewModel.NavigationRequested += (s,e) => Device.BeginInvokeOnMainThread(async () => await Navigation.PushAsync(e));
    }       

    ...

    void lvMain_ItemSelected(object sender, SelectedItemChangedEventArgs e)
    {
        var listView = (ListView)sender;
        listView.SelectedItem = null;

        var viewModel = (MainPageViewModel)BindingContext;

        viewModel.ListViewItemSelectedCommand?.Invoke(e);
    }
}

ViewModel

public class MainPageViewModel
{
    //...
    Command<SelectedItemChangedEventArgs> _listViewItemSelectedCommand;
    //...
    public event EventHandler<Page> NavigationRequested;
    //...
    public Command<SelectedItemChangedEventArgs> ListViewItemSelectedCommand => _listViewItemSelectedCommand ??
    (_listViewItemSelectedCommand = new Command<SelectedItemChangedEventArgs>(ExecuteListViewItemSelectedCommand));
    //...
    void ExecuteListViewItemSelectedCommand(SelectedItemChangedEventArgs e)
    {
        var item = e as TableMainMenuItems;

        switch (item?.display_text)
        {
            case "Forms List":
                OnNavigationRequested(new FormsListPage());
                break;

            case "New Pre-Job":
                OnNavigationRequested(new PreJobPage(0));
                break;
        }
    }

    void OnNavigationRequested(Page pageToNavigate) => NavigationRequested?.Invoke(this, pageToNavigate);
    //...
}
like image 120
Brandon Minnick Avatar answered Nov 15 '22 10:11

Brandon Minnick