WinUI 3 ListView Programmatic Item Selection In MVVM
This comprehensive guide tackles the challenge of programmatically selecting items in a WinUI 3 ListView
within the Model-View-ViewModel (MVVM) architectural pattern. We'll explore a robust and maintainable solution, focusing on leveraging data binding and commands to achieve seamless integration between your UI and application logic. The goal is to manage ListView
item selection directly from your ViewModel, keeping your code clean, testable, and adhering to MVVM principles. We'll delve into the intricacies of working with observable collections and ensuring that your UI accurately reflects the selected items based on your application's state. This approach ensures that user interactions and programmatic updates are synchronized, providing a consistent and predictable user experience.
In WinUI 3 applications employing the MVVM pattern, directly manipulating UI elements from the ViewModel is discouraged. This principle promotes separation of concerns, enhancing testability and maintainability. However, scenarios often arise where programmatic selection of ListView
items is necessary. For instance, you might need to:
- Restore selection state upon application launch.
- Select items based on external events or data changes.
- Implement features like "select all" or "clear selection."
Directly accessing the ListView
and setting its SelectedItem
or SelectedItems
properties from the ViewModel violates MVVM principles. Therefore, a mechanism is needed to indirectly control the selection state through data binding and commands. This involves creating a binding between a property in the ViewModel and the selected items in the ListView
. When the ViewModel property changes, the ListView
selection should update accordingly. Furthermore, commands can be used to trigger selection changes based on user actions or other application logic, maintaining the separation of concerns. This approach ensures that the UI remains a reflection of the ViewModel's state, rather than the ViewModel directly manipulating the UI.
Before diving into the solution, ensure you have the following:
- Basic Understanding of MVVM: Familiarity with the Model-View-ViewModel architectural pattern is crucial. Understand the roles of Models, Views, and ViewModels, and how they interact. This includes data binding, commanding, and the separation of concerns.
- WinUI 3 Development Environment: Set up your development environment with the necessary tools and SDKs for WinUI 3 development. This typically involves installing Visual Studio with the WinUI 3 project template.
- C# and XAML Knowledge: A solid grasp of C# for ViewModel logic and XAML for UI design is essential. You should be comfortable with data binding syntax, control properties, and event handling in XAML.
- Observable Collections: Understand how
ObservableCollection<T>
works and its role in notifying the UI of changes. This is crucial for dynamic updates in theListView
as items are added, removed, or modified.
With these prerequisites in place, you'll be well-equipped to implement programmatic item selection in your WinUI 3 ListView
while adhering to MVVM best practices. We will be using these concepts extensively throughout the solution.
To implement programmatic selection in a WinUI 3 ListView
using the MVVM pattern, we'll follow these steps:
1. Define the Model
First, create a simple model class representing the data displayed in the ListView
. For example:
public class Party
{
public string Name { get; set; }
public int Id { get; set; }
}
This Party
class has two properties: Name
and Id
. This model will be used to represent each item in the ListView
. You can extend this model with additional properties as needed for your application. The key is to have a clear representation of the data that your ListView
will display. This model will be used in the ObservableCollection
within the ViewModel.
2. Create the ViewModel
Next, construct a ViewModel containing an ObservableCollection<T>
for the list items and another ObservableCollection<T>
to hold the selected items.
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.UI.Xaml.Data;
public class MainViewModel
{
public ObservableCollection<Party> Partys { get; set; } = new ObservableCollection<Party>();
public ObservableCollection<Party> SelectedPartys { get; set; } = new ObservableCollection<Party>();
public MainViewModel()
{
// Initialize with sample data
Partys.Add(new Party { Id = 1, Name = "Party A" });
Partys.Add(new Party { Id = 2, Name = "Party B" });
Partys.Add(new Party { Id = 3, Name = "Party C" });
}
}
Here, Partys
is the main collection that the ListView
will bind to, and SelectedPartys
is the collection that will hold the selected items. The constructor initializes Partys
with some sample data. It's crucial to use ObservableCollection<T>
because it automatically notifies the UI of changes, ensuring that the ListView
updates whenever items are added or removed. SelectedPartys
will be the key to programmatically controlling the ListView
's selection.
3. Implement Selection Logic in ViewModel
Now, add logic to the ViewModel to handle item selection. We'll subscribe to the SelectionChanged
event of the ListView
indirectly by observing changes in the SelectedPartys
collection. We also need a way to programmatically select items, so we'll add a method for that. This method will add or remove items from the SelectedPartys
collection, which will in turn update the ListView
's selection. This is a key aspect of the MVVM pattern – modifying the model (in this case, the SelectedPartys
collection) rather than directly manipulating the view.
// In MainViewModel.cs
public void SelectParty(Party party)
{
if (party == null) return;
if (SelectedPartys.Contains(party))
{
SelectedPartys.Remove(party);
}
else
{
SelectedPartys.Add(party);
}
}
public bool IsPartySelected(Party party)
{
return SelectedPartys.Contains(party);
}
The SelectParty
method adds or removes a Party
from the SelectedPartys
collection, effectively toggling its selection state. The IsPartySelected
method checks if a party is currently selected. These methods are essential for programmatically controlling the selection state of items in the ListView
. We'll use these methods in conjunction with data binding and commands to link the UI with the ViewModel's logic.
4. Bind the ListView to the ViewModel
In the XAML for your View, bind the ListView
's ItemsSource
to the Partys
collection in the ViewModel. The ListView
's SelectedItems
property cannot be directly bound to an ObservableCollection
, so we will utilize a Behavior
to synchronize the SelectedPartys
collection in the ViewModel with the ListView
's selection. Data binding is a cornerstone of MVVM, allowing the UI to reflect the ViewModel's state automatically.
<Page
x:Class="WinUIApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WinUIApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
DataContext="{local:MainViewModel}"
>
<Grid>
<ListView
x:Name="LV_Partys"
ItemsSource="{Binding Partys}"
SelectionMode="Multiple"
IsMultiSelectCheckBoxEnabled="True">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Party">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Page
In this XAML, the ListView
's ItemsSource
is bound to the Partys
collection in the MainViewModel
. The SelectionMode
is set to Multiple
to allow multiple item selections, and IsMultiSelectCheckBoxEnabled
is set to True
to display checkboxes for selection. The ItemTemplate
defines how each Party
object is displayed in the ListView
, in this case, using a TextBlock
to show the Name
property. This setup establishes the visual representation of the data in the ListView
and lays the groundwork for synchronizing selections with the ViewModel.
5. Create a Behavior to Sync SelectedItems
Since ListView.SelectedItems
is not a dependency property that supports two-way binding, we need to create a Behavior
to synchronize the ListView
's selected items with the SelectedPartys
collection in the ViewModel. A Behavior
allows us to add functionality to existing XAML controls without subclassing them. This is particularly useful in MVVM scenarios where we want to encapsulate UI-related logic and keep our ViewModels clean.
Create a new class named ListViewSelectedItemsBehavior
:
using Microsoft.Xaml.Interactivity;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
public class ListViewSelectedItemsBehavior : Behavior<ListView>
{
public ObservableCollection<object> SelectedItems
{
get { return (ObservableCollection<object>)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(ObservableCollection<object>),
typeof(ListViewSelectedItemsBehavior), new PropertyMetadata(null, OnSelectedItemsChanged));
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
if (SelectedItems != null)
{
// Initialize ListView selection based on ViewModel's SelectedItems
foreach (var item in SelectedItems.ToList())
{
if (!AssociatedObject.SelectedItems.Contains(item))
{
AssociatedObject.SelectedItems.Add(item);
}
}
SelectedItems.CollectionChanged += SelectedItems_CollectionChanged;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
if (SelectedItems != null)
{
SelectedItems.CollectionChanged -= SelectedItems_CollectionChanged;
}
}
private void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (SelectedItems == null) return;
// Add newly selected items to SelectedItems
foreach (var item in e.AddedItems)
{
if (!SelectedItems.Contains(item))
{
SelectedItems.Add(item);
}
}
// Remove deselected items from SelectedItems
foreach (var item in e.RemovedItems)
{
SelectedItems.Remove(item);
}
}
private void SelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (AssociatedObject == null) return;
if (e.Action == NotifyCollectionChangedAction.Add)
{
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
if (!AssociatedObject.SelectedItems.Contains(item))
{
AssociatedObject.SelectedItems.Add(item);
}
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
AssociatedObject.SelectedItems.Remove(item);
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
AssociatedObject.SelectedItems.Clear();
if(SelectedItems != null)
{
foreach (var item in SelectedItems.ToList())
{
AssociatedObject.SelectedItems.Add(item);
}
}
}
}
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var behavior = (ListViewSelectedItemsBehavior)d;
if (behavior.AssociatedObject != null)
{
var oldCollection = e.OldValue as ObservableCollection<object>;
if (oldCollection != null)
{
oldCollection.CollectionChanged -= behavior.SelectedItems_CollectionChanged;
}
var newCollection = e.NewValue as ObservableCollection<object>;
if (newCollection != null)
{
newCollection.CollectionChanged += behavior.SelectedItems_CollectionChanged;
behavior.AssociatedObject.SelectedItems.Clear();
foreach (var item in newCollection.ToList())
{
if (!behavior.AssociatedObject.SelectedItems.Contains(item))
{
behavior.AssociatedObject.SelectedItems.Add(item);
}
}
}
}
}
}
This ListViewSelectedItemsBehavior
class uses the Microsoft.Xaml.Interactivity
library to attach to a ListView
. It defines a SelectedItems
dependency property of type ObservableCollection<object>
, which will be bound to the SelectedPartys
collection in the ViewModel. The OnAttached
method subscribes to the ListView
's SelectionChanged
event and the SelectedItems
collection's CollectionChanged
event. The AssociatedObject_SelectionChanged
method keeps the SelectedItems
collection in sync with the ListView
's selection. The SelectedItems_CollectionChanged
method keeps the ListView
's selection in sync with the SelectedItems
collection. The OnSelectedItemsChanged
method handles the scenario when the SelectedItems
property itself is changed (e.g., a new collection is assigned). This behavior effectively bridges the gap between the ListView
's selection mechanism and the ViewModel's data, enabling two-way synchronization.
6. Attach the Behavior to the ListView
In your XAML, add a reference to the Microsoft.Xaml.Interactivity
namespace and attach the ListViewSelectedItemsBehavior
to your ListView
. This is done using XAML's attached properties feature. By attaching the behavior, we enable the synchronization logic defined in the ListViewSelectedItemsBehavior
class.
First, add the namespace:
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:behaviors="using:WinUIApp"
Then, attach the behavior to the ListView
:
<ListView
x:Name="LV_Partys"
ItemsSource="{Binding Partys}"
SelectionMode="Multiple"
IsMultiSelectCheckBoxEnabled="True">
<interactivity:Interaction.Behaviors>
<behaviors:ListViewSelectedItemsBehavior SelectedItems="{Binding SelectedPartys, Mode=TwoWay}" />
</interactivity:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Party">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Here, we've added the interactivity
namespace and the behaviors
namespace (assuming your ListViewSelectedItemsBehavior
is in the WinUIApp
namespace). We then use Interaction.Behaviors
to attach the ListViewSelectedItemsBehavior
to the ListView
. The SelectedItems
property of the behavior is bound to the SelectedPartys
collection in the ViewModel with Mode=TwoWay
, ensuring that changes in either the ListView
's selection or the SelectedPartys
collection are reflected in the other. This completes the synchronization setup, allowing the ViewModel to programmatically control the ListView
's selection.
7. Programmatically Select Items
Now, you can programmatically select items by adding them to the SelectedPartys
collection in your ViewModel. For example:
// In MainViewModel.cs
public void SelectPartyById(int id)
{
var partyToSelect = Partys.FirstOrDefault(p => p.Id == id);
if (partyToSelect != null)
{
SelectParty(partyToSelect);
}
}
This SelectPartyById
method finds a Party
in the Partys
collection by its Id
and then calls the SelectParty
method to add it to the SelectedPartys
collection. This will trigger the ListViewSelectedItemsBehavior
to update the ListView
's selection. You can call this method from anywhere in your ViewModel to programmatically select an item. This is a powerful way to control the UI from your ViewModel without directly manipulating UI elements. For example, you might call this method in response to a button click, a timer event, or data arriving from a web service.
8. Test the Implementation
Run your application and verify that:
- Items added to
SelectedPartys
in the ViewModel are automatically selected in theListView
. - Items selected or deselected in the
ListView
are reflected in theSelectedPartys
collection. - The
SelectPartyById
method correctly selects items in theListView
.
Thorough testing is essential to ensure that your implementation works correctly and that the synchronization between the ViewModel and the ListView
is seamless. Test various scenarios, including selecting and deselecting items manually, programmatically selecting items, and handling edge cases such as empty collections or invalid IDs.
Here's a complete example demonstrating the implementation:
// Party.cs
public class Party
{
public string Name { get; set; }
public int Id { get; set; }
}
// MainViewModel.cs
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.UI.Xaml.Data;
public class MainViewModel
{
public ObservableCollection<Party> Partys { get; set; } = new ObservableCollection<Party>();
public ObservableCollection<Party> SelectedPartys { get; set; } = new ObservableCollection<Party>();
public MainViewModel()
{
// Initialize with sample data
Partys.Add(new Party { Id = 1, Name = "Party A" });
Partys.Add(new Party { Id = 2, Name = "Party B" });
Partys.Add(new Party { Id = 3, Name = "Party C" });
}
public void SelectParty(Party party)
{
if (party == null) return;
if (SelectedPartys.Contains(party))
{
SelectedPartys.Remove(party);
}
else
{
SelectedPartys.Add(party);
}
}
public bool IsPartySelected(Party party)
{
return SelectedPartys.Contains(party);
}
public void SelectPartyById(int id)
{
var partyToSelect = Partys.FirstOrDefault(p => p.Id == id);
if (partyToSelect != null)
{
SelectParty(partyToSelect);
}
}
}
// ListViewSelectedItemsBehavior.cs
using Microsoft.Xaml.Interactivity;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
public class ListViewSelectedItemsBehavior : Behavior<ListView>
{
public ObservableCollection<object> SelectedItems
{
get { return (ObservableCollection<object>)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(ObservableCollection<object>),
typeof(ListViewSelectedItemsBehavior), new PropertyMetadata(null, OnSelectedItemsChanged));
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
if (SelectedItems != null)
{
foreach (var item in SelectedItems.ToList())
{
if (!AssociatedObject.SelectedItems.Contains(item))
{
AssociatedObject.SelectedItems.Add(item);
}
}
SelectedItems.CollectionChanged += SelectedItems_CollectionChanged;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
if (SelectedItems != null)
{
SelectedItems.CollectionChanged -= SelectedItems_CollectionChanged;
}
}
private void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (SelectedItems == null) return;
foreach (var item in e.AddedItems)
{
if (!SelectedItems.Contains(item))
{
SelectedItems.Add(item);
}
}
foreach (var item in e.RemovedItems)
{
SelectedItems.Remove(item);
}
}
private void SelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (AssociatedObject == null) return;
if (e.Action == NotifyCollectionChangedAction.Add)
{
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
if (!AssociatedObject.SelectedItems.Contains(item))
{
AssociatedObject.SelectedItems.Add(item);
}
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
AssociatedObject.SelectedItems.Remove(item);
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
AssociatedObject.SelectedItems.Clear();
if(SelectedItems != null)
{
foreach (var item in SelectedItems.ToList())
{
AssociatedObject.SelectedItems.Add(item);
}
}
}
}
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var behavior = (ListViewSelectedItemsBehavior)d;
if (behavior.AssociatedObject != null)
{
var oldCollection = e.OldValue as ObservableCollection<object>;
if (oldCollection != null)
{
oldCollection.CollectionChanged -= behavior.SelectedItems_CollectionChanged;
}
var newCollection = e.NewValue as ObservableCollection<object>;
if (newCollection != null)
{
newCollection.CollectionChanged += behavior.SelectedItems_CollectionChanged;
behavior.AssociatedObject.SelectedItems.Clear();
foreach (var item in newCollection.ToList())
{
if (!behavior.AssociatedObject.SelectedItems.Contains(item))
{
behavior.AssociatedObject.SelectedItems.Add(item);
}
}
}
}
}
}
// MainPage.xaml
<Page
x:Class="WinUIApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WinUIApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:behaviors="using:WinUIApp"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
DataContext="{local:MainViewModel}"
>
<Grid>
<StackPanel>
<ListView
x:Name="LV_Partys"
ItemsSource="{Binding Partys}"
SelectionMode="Multiple"
IsMultiSelectCheckBoxEnabled="True">
<interactivity:Interaction.Behaviors>
<behaviors:ListViewSelectedItemsBehavior SelectedItems="{Binding SelectedPartys, Mode=TwoWay}" />
</interactivity:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Party">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Content="Select Party B" Click="SelectPartyB_Click"/>
</StackPanel>
</Grid>
</Page>
// MainPage.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace WinUIApp
{
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
private void SelectPartyB_Click(object sender, RoutedEventArgs e)
{
if (DataContext is MainViewModel viewModel)
{
viewModel.SelectPartyById(2);
}
}
}
}
This complete example demonstrates how to programmatically select items in a WinUI 3 ListView
using the MVVM pattern. It includes the Party
model, the MainViewModel
, the ListViewSelectedItemsBehavior
, and the XAML for the MainPage
. The SelectPartyB_Click
event handler in MainPage.xaml.cs
demonstrates how to call the SelectPartyById
method in the ViewModel to programmatically select an item. This working example provides a solid foundation for implementing programmatic selection in your own WinUI 3 applications.
This article demonstrated how to programmatically select items in a WinUI 3 ListView
using the MVVM pattern. By leveraging data binding and a custom Behavior
, we achieved seamless synchronization between the ListView
's selection and the ViewModel's SelectedPartys
collection. This approach ensures that your UI accurately reflects the application's state while adhering to MVVM principles. The key takeaways are the use of ObservableCollection<T>
for data binding, the creation of a Behavior
to handle the synchronization of selected items, and the importance of modifying the ViewModel's data rather than directly manipulating the UI. This pattern is highly effective for building maintainable and testable WinUI 3 applications.
Here are some potential enhancements to consider:
- Command Implementation: Use commands to trigger selection changes from the View, further decoupling the View and ViewModel.
- Custom Selection Logic: Implement more complex selection logic in the ViewModel, such as selecting items based on multiple criteria or handling edge cases more gracefully.
- Unit Testing: Write unit tests for your ViewModel to ensure that the selection logic works correctly and that the
SelectedPartys
collection is updated as expected.
By incorporating these enhancements, you can further refine your implementation and create even more robust and maintainable WinUI 3 applications.
This approach enhances testability, maintainability, and scalability of your WinUI 3 applications. By adhering to MVVM principles, you create a clear separation of concerns, making your code easier to understand, modify, and test. This is crucial for building complex applications that need to evolve over time.