Micah Williamson Micah Williamson - 15 days ago 10
iOS Question

Xamarin ListView TwoWay binding doesn't work on iOS

In our Xamarin application we have a ListView that shows a list of Surveys.If the user selects a Survey they will be taken to that surveys details page. To do that, we use

TwoWay
binding on the
SelectedItem
property, and add a setter to the SelectedItem property in the controller.

Xaml-

<ListView x:Name= "surveyList" ItemsSource= "{Binding Surveys}" SelectedItem= "{Binding SelectedSurvey, Mode=TwoWay}" BackgroundColor= "White" HorizontalOptions= "Fill" SeparatorColor= "Gray" RowHeight= "50" >


C#-

private SurveyListItem _selectedSurvey;
public SurveyListItem SelectedSurvey
{
get { return _selectedSurvey; }
set
{
_selectedSurvey = value;
if (_selectedSurvey == null)
{
NotifyPropertyChanged();
return;
}
OnSurveySelected(_selectedSurvey);
_selectedSurvey = null;
NotifyPropertyChanged();
}
}


This works find on
Android
but does not work on
iOS
. Tapping the item in the list does not set the
_selectedSurvey
or call the setter of
SelectedSurvey
.

I could make a quick fix and change this to some sort of tap gesture but we are using ListView in other places of the application where multi-select is needed, and changing all of that to tap gesture would be a pain.

Any ideas why this would work for
Android
but not
iOS
?

Edit-

Full Xaml-

<?xml version = "1.0" encoding= "utf-8" ?>

< TabbedPage xmlns = "http://xamarin.com/schemas/2014/forms"

xmlns:x= "http://schemas.microsoft.com/winfx/2009/xaml"

x:Class= "MyApp.View.ModuleContentPage"

xmlns:vm= "clr-namespace:MyApp.ViewModel;assembly=MyApp"

xmlns:local= "clr-namespace:MyApp.View;assembly=MyApp"

Title= "Module Content Render" >

< TabbedPage.Children >

< ContentPage Title= "Summary" IsEnabled= "False" >

< ContentPage.Content >

< StackLayout Padding= "20, 20, 20, 0" >

< Label Text= "To Do: Render Module Content Here." ></ Label >

</ StackLayout >

</ ContentPage.Content >

</ ContentPage >

< ContentPage Title= "Related" >

< ContentPage.Content >

< StackLayout Padding= "20, 20, 20, 0" >

< Label Text= "To Do: Render Related Content Here." ></ Label >

</ StackLayout >

</ ContentPage.Content >

</ ContentPage >

< ContentPage Title= "Surveys" IsEnabled= "False" >

< ContentPage.Content >

< StackLayout Padding= "20" >

< ListView x:Name= "surveyList" ItemsSource= "{Binding Surveys}" SelectedItem= "{Binding SelectedSurvey, Mode=TwoWay}" BackgroundColor= "White" HorizontalOptions= "Fill" SeparatorColor= "Gray" RowHeight= "50" >

< ListView.Header >

< StackLayout Padding= "0, 0, 0, 10" VerticalOptions= "Center" >

< Label Text= "Surveys" FontSize= "20" TextColor= "Gray" LineBreakMode= "NoWrap" />

</ StackLayout >

</ ListView.Header >

< ListView.ItemTemplate >

< DataTemplate >

< ViewCell >

< ViewCell.View >

< StackLayout VerticalOptions= "Center" >

< Grid ColumnSpacing= "20" >

< Grid.RowDefinitions >

< RowDefinition Height= "*" />

</ Grid.RowDefinitions >

< Grid.ColumnDefinitions >

< ColumnDefinition Width= "*" />

< ColumnDefinition Width= "50" />

</ Grid.ColumnDefinitions >

< Label Text= "{Binding HydratedSurvey.Name}" FontSize= "12" TextColor= "Black" FontAttributes= "Bold" Grid.Row= "0" Grid.Column= "0" />

< Label Text= "{Binding SurveyInstanceCount}" FontSize= "12" TextColor= "Green" FontAttributes= "Bold" Grid.Row= "0" Grid.Column= "1" />

</ Grid >

</ StackLayout >

</ ViewCell.View >

</ ViewCell >

</ DataTemplate >

</ ListView.ItemTemplate >

</ ListView >

</ StackLayout >

</ ContentPage.Content >

</ ContentPage >

</ TabbedPage.Children >

</ TabbedPage >


Code Behind-

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xamarin.Forms;

namespace MyApp.View
{
public partial class ModuleContentPage : TabbedPage
{
private Page PreviousPage { get; set; }

public ModuleContentPage()
{
InitializeComponent();

// TODO hard coded to disabled the first two tabs and select Surveys. Remove when other tabs are finished
DisableTab(Children[0]);
DisableTab(Children[1]);

//Children[0].IsEnabled = false;
//Children[1].IsEnabled = false;
//PreviousPage = Children[2];
//CurrentPage = PreviousPage;
CurrentPage = Children[2];

CurrentPageChanged += ModuleContentPage_CurrentPageChanged;
PagesChanged += ModuleContentPage_PagesChanged;
}

private void DisableTab(Page page)
{
page.IsEnabled = false;
//page.Unfocus();
page.Opacity = 50.0;
}

private void ModuleContentPage_PagesChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
CurrentPage = Children[2];
}

private void ModuleContentPage_CurrentPageChanged(object sender, EventArgs e)
{
CurrentPage = Children[2];
}
}
}


** View Model-**

using Newtonsoft.Json;
using MyApp.DataModel.TransferObjects;
using MyApp.DataAccess.UoW;
using MyApp.Services;
using MyApp.SQLiteAccess.Repository;
using MyApp.SQLiteAccess.Tables;
using MyApp.ViewModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Xamarin.Forms;

namespace MyApp.ViewModel
{
public class SurveyListItem : ViewModelBase
{
public SurveyDTO HydratedSurvey { get; set; }
private int _surveyInstanceCount;
public int SurveyInstanceCount { get { return _surveyInstanceCount; } set { _surveyInstanceCount = value; NotifyPropertyChanged(); } }
}

public class ModuleContentPageViewModel : ViewModelBase
{
private ModuleHydratedDTO _module;
public ModuleHydratedDTO Module { get { return _module; } set { _module = value; NotifyPropertyChanged(); } }

private ModuleContentHydratedDTO _moduleContent;
public ModuleContentHydratedDTO ModuleContent { get { return _moduleContent; } set { _moduleContent = value; NotifyPropertyChanged(); } }

SqlSurveyInstanceRepository _sqlSurveyInstanceRepository;
SqlSurveyInstanceRepository SqlSurveyInstanceRepository { get { return _sqlSurveyInstanceRepository ?? (_sqlSurveyInstanceRepository = new SqlSurveyInstanceRepository()); } }

private ObservableCollection<SurveyListItem> _surveys;
public ObservableCollection<SurveyListItem> Surveys { get { return _surveys; } set { _surveys = value; NotifyPropertyChanged(); } }

public ModuleContentPageViewModel(ModuleHydratedDTO module, ModuleContentHydratedDTO moduleContent) : base()
{
_module = module;
_moduleContent = moduleContent;
_surveys = GetSurveys();

MessagingCenter.Subscribe<Stores.SurveyStore, SurveyDTO>(this, "UpdateSurveyInstanceCount", (sender, hydratedSurvey) =>
{
SurveyListItem survey = _surveys.FirstOrDefault(s => s.HydratedSurvey.SurveyId == hydratedSurvey.SurveyId);
if (survey != null)
{
survey.SurveyInstanceCount = UoW.SurveyInstances.GetCountForSurveyIdAsync(hydratedSurvey.MasterSurveyId ?? hydratedSurvey.SurveyId, ModuleContent.ModuleContentId).Result;
NotifyPropertyChanged();
}
});
}

private ObservableCollection<SurveyListItem> GetSurveys()
{
ObservableCollection<SurveyListItem> surveyList = new ObservableCollection<SurveyListItem>();
List<SurveyDTO> surveys = UoW.Surveys.GetHydratedSurveysForUser(_module.Module.ModuleId);

if (surveys.Count > 0)
{
foreach (var survey in surveys)
{
SurveyListItem item = new SurveyListItem();
item.HydratedSurvey = survey;
item.SurveyInstanceCount = UoW.SurveyInstances.GetCountForSurveyIdAsync(survey.MasterSurveyId.HasValue ? survey.MasterSurveyId.Value : survey.SurveyId, ModuleContent.ModuleContentId).Result;
surveyList.Add(item);
}
}
return surveyList;
}

private SurveyListItem _selectedSurvey;
public SurveyListItem SelectedSurvey
{
get { return _selectedSurvey; }
set
{
_selectedSurvey = value;
if (_selectedSurvey == null)
{
NotifyPropertyChanged();
return;
}
OnSurveySelected(_selectedSurvey);
_selectedSurvey = null;
NotifyPropertyChanged();
}
}

private void OnSurveySelected(SurveyListItem selectedSurvey)
{
NavigationService.PushAsync(new SurveyInstanceListVM(selectedSurvey.HydratedSurvey, _moduleContent.ModuleContentId), selectedSurvey.HydratedSurvey.Name);
}
}
}


Hope that helps

Edit Edit-

Here is the frankenstiened viewmodel/viewmodelbase combination.

using MyApp.DataModel.TransferObjects;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using MyApp.ViewModelFramework.Spooling;

namespace MyApp.ViewModel
{
public class SurveyListItem
{
public string Name { get { return "foo"; } set { var x = value; } }
private int _surveyInstanceCount;
public int SurveyInstanceCount { get { return _surveyInstanceCount; } set { _surveyInstanceCount = value; } }
}

public class ModuleContentPageViewModel
{
private ObservableCollection<SurveyListItem> _surveys;
public ObservableCollection<SurveyListItem> Surveys { get { return _surveys; } set { _surveys = value; NotifyPropertyChanged(); } }


public ModuleContentPageViewModel(ModuleHydratedDTO module, ModuleContentHydratedDTO moduleContent) : base()
{
_surveys = GetSurveys();
}

private ObservableCollection<SurveyListItem> GetSurveys()
{
ObservableCollection<SurveyListItem> surveyList = new ObservableCollection<SurveyListItem>();
surveyList.Add(new SurveyListItem());
return surveyList;
}

private SurveyListItem _selectedSurvey;
public SurveyListItem SelectedSurvey
{
get { return _selectedSurvey; }
set
{
_selectedSurvey = value;
if (_selectedSurvey == null)
{
NotifyPropertyChanged();
return;
}
_selectedSurvey = null;
NotifyPropertyChanged();
}
}

public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
InvokeNotifyPropertyChanged(propertyName);
}

public event PropertyChangedEventHandler PropertyChanged;

public NotifyPropertyChangedSpooler NotificationSpooler = new NotifyPropertyChangedSpooler();

private NotificationPropertyChangedStateEnum _notificationPropertyChangedState = NotificationPropertyChangedStateEnum.Active;

// Gets and sets change notification spooling and blocking features.
public NotificationPropertyChangedStateEnum NotificationPropertyChangedState
{
get { return _notificationPropertyChangedState; }
set
{
_notificationPropertyChangedState = value;
if (_notificationPropertyChangedState == NotificationPropertyChangedStateEnum.Active && !NotificationSpooler.IsEmpty)
{
NotificationSpooler.Unwind();
}
NotifyPropertyChanged();

}
}


void InvokeNotifyPropertyChanged(string propertyName)
{
switch (NotificationPropertyChangedState)
{
case NotificationPropertyChangedStateEnum.Active:
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
break;
case NotificationPropertyChangedStateEnum.Spooling:
NotificationSpooler.Enqueue(this, propertyName);
break;
case NotificationPropertyChangedStateEnum.Inactive:
break;
}
}
}

public enum NotificationPropertyChangedStateEnum
{
Inactive = 0,
Active = 1,
Spooling = 3
}
}

Answer

After some investigation was found that the page was disabled in xaml and never enabled neither in xaml nor in code behind. For some reason it wasn't problem in Android (which is kind of wrong and should be investigated more why).

Comments