See the question and my original answer on StackOverflow

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'
            }
        });