Search code examples
c#mvvmuwptemplate10

Can't deselect ListView Item in MVVM UWP


I want to be able to click ListView item, which then takes me to appropriate page. But since there doesn't exists anything like ClickedItem to go along with the ItemClick, I have to use the SelectedItem (to get the object of what the user clicked) and SelectionChanged to capture when it happens (because this is setup in a way that when user clicks, he makes a selection, which triggers this).

Since in MVVM I can't use events, I'm binding what would be events to methods in my ViewModel.

<GridView x:Name="MyGrid" 
    ItemsSource="{x:Bind ViewModel.myList, Mode=OneWay}" 
    VerticalAlignment="Stretch" 
    HorizontalAlignment="Stretch" 
    IsSwipeEnabled="false"
    SelectedItem="{Binding mySelectedItem, Mode=TwoWay}" // Binding makes it easier to bind the whole object
    SelectionChanged="{x:Bind ViewModel.SelectioMade}"
>

I fill up my list in the ViewModel. I'm using Template10 implementation of INotifyPropertyChanged.

private MyListItemClass _mySelectedItem;
public MyListItemClass mySelectedItem{
    get { return _mySelectedItem; }
    set { Set(ref _mySelectedItem, value); }
}

And this simple method pushes me to the next page when user clickes on an item.

public void SelectioMade() {
    if (_mySelectedItem != null) {
        NavigationService.Navigate(typeof(Views.DetailPage), _mySelectedItem.id);
    }
}

This works.

Problem is that a selection is made and it persists. When I hit the back button on the DetailPage, I go back to this list as I left it and the clicked item is still selected. And hence, clicking it again doesn't actually make a selection and trigger the SelectionChanged.

Obvious choice seemed to be to just set mySelectedItem to null when I no longer need the value, but it doesn't work.

public void SelectioMade() {
    if (_mySelectedItem != null) {
        NavigationService.Navigate(typeof(Views.DetailPage), _mySelectedItem.id);
        mySelectedItem = null;
    }
}

I can't seem to be able to set it back to null. If I place a break point on the mySelectedItem = null; it just doesn't do anything. It does trigger the set { Set(ref _mySelectedItem, value); }, but the View doesn't update. Neither the clicked item becomes deselected, nor a TextBlock I bound to one of the mySelectedItem.id properties gets changed (or rather emptied).

I would like to know why doesn't this work and possibly how to fix it. My MVVM may not be perfect, I'm still learning. And while it may not be perfect, I'm not really looking for advice how to properly write MVVM. I want to know why this doesn't work, because in my opinion, it should work just fine.


Solution

  • It seems that GridView doesn't like the SelectedItem property being changed within the SelectionChanged handler (it could result in an infinite loop if guards are not used). You could instead set SelectedItem to null in the OnNavigatedTo handler for that page (or whatever the Template 10 equivalent of that is).

    Also you don't really need to subscribe to the SelectionChanged event since you can detect this in the setter of your mySelectedItem property.

    However, I think it is wrong to handle item clicks by listening for selection changed events because the selection can be changed by other means (up/down arrow key, or tab key, for example). All you want to do is to respond to an item click and obtain the clicked item, right? For this, you can x:Bind the ItemClick event to a method in your view model:

    <GridView ItemClick="{x:Bind ViewModel.ItemClick}" SelectionMode="None" IsItemClickEnabled="True">
    
    public void ItemClick(object sender, ItemClickEventArgs e)
    {
        var item = e.ClickedItem;
    }
    

    If you're uneasy about the ItemClick method signature in your view model, then you can make your own ItemClick behavior to execute a Command exposed in your view model with the command's parameter bound to the clicked item.

    If you're not using behaviors for some reason, then you can make your own attached property instead, something like this:

    public class ViewHelpers
    {
        #region ItemClickCommand
    
        public static readonly DependencyProperty ItemClickCommandProperty =
            DependencyProperty.RegisterAttached("ItemClickCommand", typeof(ICommand), typeof(ViewHelpers), new PropertyMetadata(null, onItemClickCommandPropertyChanged));
    
        public static void SetItemClickCommand(DependencyObject d, ICommand value)
        {
            d.SetValue(ItemClickCommandProperty, value);
        }
    
        public static ICommand GetItemClickCommand(DependencyObject d)
        {
            return (ICommand)d.GetValue(ItemClickCommandProperty);
        }
    
        static void onItemClickCommandPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var listView = d as ListViewBase;
            if (listView == null)
                throw new Exception("Dependency object must be a ListViewBase");
    
            listView.ItemClick -= onItemClick;
            listView.ItemClick += onItemClick;
        }
    
        static void onItemClick(object sender, ItemClickEventArgs e)
        {
            var listView = sender as ListViewBase;
            var command = GetItemClickCommand(listView);
            if (command != null && command.CanExecute(e.ClickedItem))
                command.Execute(e.ClickedItem);
        }
    
        #endregion
    }
    

    XAML doesn't require MVVM patterns to be used, which means there is lots of "missing" functionality that you need to write yourself to make MVVM easier for you (like the above ItemClick attached property). Maybe Template 10 provides some behaviors for you already? I'm not familiar with it.