Michael Chourdakis
Michael Chourdakis

Reputation: 11178

WinUI 3 ListView Selected Items

I'm using a WinUI3 ListView like this to load a list of files:

    <ListView ItemTemplate="{StaticResource Template2}"
     ItemsSource="{x:Bind Files,Mode=OneWay}"  
    IsItemClickEnabled="True" x:Name="List2" SelectionMode="Multiple" />
     <DataTemplate x:Key="Template2" x:DataType="local:FileItem">
            <Grid>
             .... TextBlocks that bind to properties in FileItem
             </Grid>
    </DataTemplate>

Now, how do I configure my DataTemplate in order for some (or all) of the items to be selected by default?

Is there a special value in the data template I should use?

Upvotes: 1

Views: 656

Answers (3)

David Hernandez
David Hernandez

Reputation: 24

You can always add the objects to the lists SelectedItems Collection.

Name your list x:Name=ListName

Then apply logic after loading your view to add certain objects of that list to the SelectedItems.

<ListView
    x:Name="ListName"
    Grid.Row="1"
    IsItemClickEnabled="True"
    ItemTemplate="{StaticResource Template2}"
    ItemsSource="{x:Bind ViewModel.Files, Mode=OneWay}"
    SelectionMode="Multiple" />

Then do

public ClassName()
{
    this.InitializeComponent();

    // apply your own logic to add if condition is met
    // this would select all values
    foreach (var x in ListName.Items)
    {
        ListName.SelectedItems.Add(x);
    }
}

Upvotes: -1

Simon Mourier
Simon Mourier

Reputation: 139187

Here is another answer of course still based on ListViewItem but slightly different, I believe a bit simpler to implement across different WinUI3 supported languages.

The Xaml:

<ListView ItemsSource="{x:Bind MyItems}" SelectionMode="Multiple">
    <ListView.ItemTemplate>
        <DataTemplate x:DataType="local:MyItem">
            <ListViewItem IsSelected="{x:Bind IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                <TextBlock Text="{x:Bind Name}" />
            </ListViewItem>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

And if you have a data class like this in C#

public class MyItem : INotifyPropertyChanged
{
    private bool _isSelected;
    public event PropertyChangedEventHandler PropertyChanged;

    public string Name { get; set; }
    public bool IsSelected
    {
        get => _isSelected;
        set
        {
            if (_isSelected == value)
                return;

            _isSelected = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
        }
    }
}

Or this in C++ (with WinRT), some code ommited for brevity the full project is here https://github.com/smourier/WinUI3Cpp

[bindable]
[default_interface]
runtimeclass FileSystemItem : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
{
  String Name { get; };
  Boolean IsSelected;
}

struct FileSystemItem : FileSystemItemT<FileSystemItem>
  {
    hstring Name() { return _name; }

    bool IsSelected() const { return _isSelected; }
    void IsSelected(bool const& selected){
      if (selected == _isSelected)
        return;
      
      _isSelected = selected;
      RaisePropertyChanged(L"IsSelected");
    }

    event_token PropertyChanged(PropertyChangedEventHandler const& handler) { return _propertyChanged.add(handler); }
    void PropertyChanged(event_token token) { _propertyChanged.remove(token); }

  private:
    event<PropertyChangedEventHandler> _propertyChanged;
    hstring _name;
    bool _isSelected;

    void RaisePropertyChanged(hstring propertyName)
    {
      _propertyChanged(*this, PropertyChangedEventArgs(propertyName));
    }
  };
}

Then the trick is to select initial items in a deferred way, so like this in C#

public sealed partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        MyItems.Add(new MyItem { Name = "Bob" });
        MyItems.Add(new MyItem { Name = "Alice" });
        MyItems.Add(new MyItem { Name = "Carl" });
        MyItems.Add(new MyItem { Name = "Donald" });

        // select items at initialization
        DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
        {
            MyItems[1].IsSelected = true; // select items 1 & 3
            MyItems[3].IsSelected = true;
        });
    }

    public ObservableCollection<MyItem> MyItems { get; } = [];
}

And for example like this in C++

    auto source{ single_threaded_observable_vector<TreeViewSample::FileSystemItem>() };
    for (auto const& child : someSource())
    {
        source.Append(child);
    }
    myListView().ItemsSource(source);

    // select items at initialization
    DispatcherQueue().TryEnqueue(
        Microsoft::UI::Dispatching::DispatcherQueuePriority::Low,
        [source]()
        {
            for (auto const& child : source)
            {
                child.IsSelected(child.Name().c_str()[0] == 'T'); // select all child with name that starts with 'T'
            }
        });

Upvotes: 2

Andrew KeepCoding
Andrew KeepCoding

Reputation: 13666

You can use the ListViewItem in your ItemTemplate.

Let's say your FileItem looks like this:

public partial class FileItem : ObservableObject
{
    [ObservableProperty]
    private string _name = string.Empty;

    [ObservableProperty]
    private bool _isSelected;
}

and your ViewModel:

public partial class  MainPageViewModel : ObservableObject
{
    [ObservableProperty]
    private ObservableCollection<FileItem> _files = new()
    {
        new FileItem { Name = "File1" },
        new FileItem { Name = "File2", IsSelected = true, },
        new FileItem { Name = "File3"},
    };
}

and in XAML:

<Page.Resources>
    <DataTemplate
        x:Key="Template2"
        x:DataType="local:FileItem">
        <ListViewItem IsSelected="{x:Bind IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
            <TextBlock Text="{x:Bind Name, Mode=OneWay}" />
        </ListViewItem>
    </DataTemplate>
</Page.Resources>
<ListView
    Grid.Row="1"
    IsItemClickEnabled="True"
    ItemTemplate="{StaticResource Template2}"
    ItemsSource="{x:Bind ViewModel.Files, Mode=OneWay}"
    SelectionMode="Multiple" />

But unfortunately, this won't work because the ListViewItem resets its IsSelected property to false when it's loaded. I'm not sure if it's a bug or it's by design.

As a workaround, you can do the binding after each ListViewItem is loaded:

<Page.Resources>
    <DataTemplate
        x:Key="Template2"
        x:DataType="local:FileItem">
        <ListViewItem Loaded="ListViewItem_Loaded">
            <TextBlock Text="{x:Bind Name, Mode=OneWay}" />
        </ListViewItem>
    </DataTemplate>
</Page.Resources>

then in code-behind:

private void ListViewItem_Loaded(object sender, RoutedEventArgs e)
{
    if (sender is not ListViewItem listViewItem)
    {
        return;
    }

    listViewItem.SetBinding(
        ListViewItem.IsSelectedProperty,
        new Binding
        {
            Path = new PropertyPath("IsSelected"),
            Mode = BindingMode.TwoWay,
            Source = listViewItem.DataContext,
            UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
        });
}

BTW, I'm using the CommunityToolkit.Mvvm NuGet package for the ViewModels but I hope this gives you an idea how to fix your issue.

Upvotes: 4

Related Questions