@SushiHangover's suggestion to control the SelectionMode property and disable/enable the selection of the ListView is a good one. However, I have an alternate solution that will revert the ListView's selected item to the previous item for anyone who might have a similar need.
I will only post snippets of the solution, but they should be complete enough for someone else to learn and implement.
First, I am using FreshMVVM which provides (amongst many things), essentially, syntactic sugar over binding the View to the ViewModel. Also, the PropertyChanged nuget package creates the INotifyPropertyChanged boilerplate code at compile time. That is why you don't see the familiar XF patterns you normally see with that interface. AddINotifyPropertyChanged handles all that.
The solution to my problem is a dedicated, generic ListViewModel that can be bound to any ListView that needs the ability "roll back" a selection changed event. It binds to the Items collection. Additionally the SelectedItem property is bound to the control as well.
The constructor takes a Func which is called to determine if it's ok to move the selection or not.
[AddINotifyPropertyChangedInterface]
public class ListViewModel<T>
{
private Func<bool> _beforeChangeValidator;
private Action _afterChange;
public ListViewModel(Func<bool> beforeChangeValidator, Action afterChange)
{
_beforeChangeValidator = beforeChangeValidator;
_afterChange = afterChange;
_changing = false;
}
public int SelectedIndex { get; set; }
public T SelectedItem { get; set; }
public ObservableCollection<T> Items { get; set; }
private bool _changing;
public Command SelectedItemChanged
{
get
{
return new Command((args) =>
{
if (!_changing)
{
if (_beforeChangeValidator())
{
SelectedIndex = ((SelectedItemChangedEventArgs)args).SelectedItemIndex;
}
}
_changing = false;
});
}
}
public void RevertSelectedItemChanged()
{
_changing = true;
SelectedItem = Items[SelectedIndex];
}
}
And the code in the parent ViewModel has the Func (TagListBeforeChange) that determines if it's ok to move the selection or not. In this case I am checking if the last selected item has been changed, and if it has, prompt the user for what to do.
public override void Init()
{
TagListViewModel = new ListViewModel<Tag>(TagListBeforeChange, null);
}
private bool TagListBeforeChange()
{
if (ActiveTag.HasChanged)
{
var confirmConfig = new ConfirmConfig()
{
Message = "Current tag has changed. Discard changes and continue?",
OkText = "Discard Changes",
CancelText = "Cancel",
OnAction = (result) =>
{
if (result)
{
_mapper.Map(TagListViewModel.SelectedItem, ActiveTag);
}
else
{
TagListViewModel.RevertSelectedItemChanged();
}
}
};
_userDialogs.Confirm(confirmConfig);
return false;
}
_mapper.Map(TagListViewModel.SelectedItem, ActiveTag);
return true;
}
And finally, here is the ListView control declaration...
<ListView ItemsSource="{Binding TagListViewModel.Items}"
SelectedItem="{Binding TagListViewModel.SelectedItem, Mode=TwoWay}">
<ListView.Behaviors>
<behaviors:EventHandlerBehavior EventName="ItemSelected">
<behaviors:InvokeCommandAction Command="{Binding TagListViewModel.SelectedItemChanged}" />
</behaviors:EventHandlerBehavior>
</ListView.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ContentView Padding="8">
<Label Text="{Binding DisplayValue}" />
</ContentView>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
SelectionMode
of the ListView to none upon the first edit of the detail pane (and ghost/dim the listview also to provide a visual clue that the detail pane should be the user's focus now) and use the ListView'sItemTapped
which will still be fired to show a toast/dialog to save/revert the detail pane's contents. After the user's choice, restore the SelectionMode back to single, undo the ghosting, etc... – SushiHangover