简体   繁体   中英

ListView SelectedItems binding: why the list is always null

I'm developing a UWP App, with Mvvm Light and Behaviours SDK. I defined a multi selectable ListView:

<ListView
    x:Name="MembersToInviteList"
    IsMultiSelectCheckBoxEnabled="True"
    SelectionMode="Multiple"
    ItemsSource="{Binding Contacts}"
    ItemTemplate="{StaticResource MemberTemplate}">

</ListView>

I'd like, with a button binded to a MVVM-Light RelayCommand , to obtain a list with the selected items:

<Button
    Command="{Binding AddMembersToEvent}"
    CommandParameter="{Binding ElementName=MembersToInviteList, Path=SelectedItems}"
    Content="Ok"/>

The RelayCommand (of MVVM-Light framework):

private RelayCommand<object> _addMembersToEvent;
public RelayCommand<object> AddMembersToEvent
{
    get
    {
        return _addMembersToEvent
            ?? (_addMembersToEvent = new RelayCommand<object>(
               (selectedMembers) =>
               {
                   // Test
                   // selectedMembers is always null!
               }));
    }
}

I put a breakpoint inside the command, and I notice that selectedMembers is always null , although I select various items. By the console output I don't see any binding error or something else.

Also, if I pass as CommandParameter the whole list, and I put a breakpoint inside command's definition, i notice that I can't access to SelectedItems nor SelecteRanges value.

<DataTemplate x:Name="MemberTemplate">

    <Viewbox MaxWidth="250">
        <Grid Width="250"
              Margin="5, 5, 5, 5"
              Background="{StaticResource MyLightGray}"
              BorderBrush="{StaticResource ShadowColor}"
              BorderThickness="0, 0, 0, 1"
              CornerRadius="4"
              Padding="5">

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="1*" />
            </Grid.ColumnDefinitions>

            <Grid Grid.Column="0"
                  Width="45"
                  Height="45"
                  Margin="5,0,5,0"
                  VerticalAlignment="Center"
                  CornerRadius="50">

                <Grid.Background>
                    <ImageBrush AlignmentX="Center"
                                AlignmentY="Center"
                                ImageSource="{Binding Image.Url,
                                                      Converter={StaticResource NullGroupImagePlaceholderConverter}}"
                                Stretch="UniformToFill" />
                </Grid.Background>

            </Grid>

            <TextBlock Grid.Column="1"
                       Margin="3"
                       VerticalAlignment="Center"
                       Foreground="{StaticResource ForegroundTextOverBodyColor}"
                       Style="{StaticResource LightText}"
                       Text="{Binding Alias}" />

        </Grid>
    </Viewbox>

</DataTemplate>

What's the reason? How can I obtain such list?

One of the solutions to pass SelectedItems from ListView in ViewModel (with RelayCommands) is described in igralli's blog.

Pass ListView SelectedItems to ViewModel in Universal apps

Try the following code to get the selected objects from the parameter.

    private RelayCommand<IList<object>> _addMembersToEvent;
    public RelayCommand<IList<object>> AddMembersToEvent
    {
        get
        {
            return _addMembersToEvent
                   ?? (_addMembersToEvent = new RelayCommand<IList<object>>(
                       selectedMembers =>
                       {
                           List<object> membersList = selectedMembers.ToList();
                       }));
        }
    }

I've made a small example for your case without MVVM-Light and it works perfect.

Maybe the problem is within the RelayCommand-class. I've written the following:

public class RelayCommand<T> : ICommand
{
    private readonly Action<T> execute;
    private readonly Predicate<T> canExecute;

    public RelayCommand(Action<T> execute, Predicate<T> canExecute = null )
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
        this.execute = execute;
        this.canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        if (canExecute == null)
            return true;
        return canExecute((T) parameter);
    }

    public void Execute(object parameter)
    {
        execute((T) parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
}

Thanks to Roman's answer I figured out how to solve the issue:

First of all, as Roman suggested:

private RelayCommand<IList<object>> _addMembersToEvent;
public RelayCommand<IList<object>> AddMembersToEvent
{
    get
    {
        return _addMembersToEvent
               ?? (_addMembersToEvent = new RelayCommand<IList<object>>(
                   selectedMembers =>
                   {
                       List<object> membersList = selectedMembers.ToList();
                   }));
    }
}

Then, the XAML:

<Button
    Command="{Binding AddMembersToEvent}"
    CommandParameter="{Binding ElementName=MembersToInviteList, Converter={StaticResource ListViewSelectedItemsConverter}}"
    Content="Ok"/>

The difference here is that I passed the whole list as parameter, not it's SelectedItems property. Then, using an IValueConverter I converted from the ListView object to IList<object> of SelectedMember :

public class ListViewSelectedItemsConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        var listView = value as ListView;
        return listView.SelectedItems;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}

In this way the RelayCommand<IList<object>> got the right list and not a null value.

I can't find a reason but you can easily skirt the issue altogether by having an "IsSelected" property on your Contact object and putting a check box on your template:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid.RowDefinitions>
        <RowDefinition Height="*"></RowDefinition>
        <RowDefinition Height="Auto"></RowDefinition>
        <RowDefinition Height="Auto"></RowDefinition>
    </Grid.RowDefinitions>

    <ListView ItemsSource="{Binding Contacts}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <CheckBox Content="{Binding Name}" IsChecked="{Binding IsSelected, Mode=TwoWay}"></CheckBox>
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

    <TextBlock Grid.Row="1" Text="{Binding SelectedItemsOutput}"></TextBlock>
    <Button Grid.Row="2" Content="What is checked?" Command="{Binding GoCommand}"></Button>
</Grid>

and VMs etc:

public class MainViewModel : INotifyPropertyChanged
{
    private string _selectedItemsOutput;

    public ObservableCollection<Contact> Contacts { get; set; } = new ObservableCollection<Contact> { new Contact() { Id = 1, Name = "Foo" }, new Contact() { Id = 2, Name = "Bar" } };

    public ICommand GoCommand => new RelayCommand(Go);

    public string SelectedItemsOutput
    {
        get { return _selectedItemsOutput; }
        set
        {
            if (value == _selectedItemsOutput) return;
            _selectedItemsOutput = value;
            OnPropertyChanged();
        }
    }

    void Go()
    {
        SelectedItemsOutput = string.Join(", ", Contacts.Where(x => x.IsSelected).Select(x => x.Name));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

public class Contact : INotifyPropertyChanged
{
    private bool _isSelected;

    public int Id { get; set; }
    public string Name { get; set; }

    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (value == _isSelected) return;
            _isSelected = value;
            OnPropertyChanged();
        }
    }

    public override string ToString()
    {
        return Name;
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
        DataContext = new MainViewModel();
    }
}

Just my five cents and might be an absolutely long shot, but you should check this :

If the ItemsSource implements IItemsRangeInfo, the SelectedItems collection is not updated based on selection in the list. Use the SelectedRanges property instead.

I am just guessing based on Tomtom's answer, since he says he did almost exactly as you and got a working solution. Maybe the difference is in this little detail. I haven't checked it myself yet.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM