基于WPF、C#和.NET 7,如何通过UserControl编辑ListView选中项
我正在学习C#的MVVM和WPF技术,使用的工具包括MVVM Community Toolkit 8、.NET 7.0、WPF UI(Design)包以及Visual Studio 2022。我尝试在页面中展示一组继承自同一接口IAction的不同类型项的列表,同时希望通过UserControl编辑ListView的选中项。当前直接在页面中添加UserControl并绑定到SelectedItem,UserControl能显示选中项的值,但编辑后切换ListView的选中项时,SelectedItem不再更新。此外,希望根据不同模型类型,通过DataTemplate加载对应的UserControl进行编辑。
问题现象
- 未编辑时选中项1:

- 未编辑时选中项2:

- 编辑项2后再次选中项1,选中项未更新:

我的两个问题
- 如何实现编辑选中项后仍能正常切换选中项?是否需要额外逻辑处理?
- 如何使用ContentControl这类通用控件,根据类型通过DataTemplate显示不同的UserControl?如何绑定Content属性?ContentControl是否是合适的选择?
相关代码
ListPage.xaml
<ui:UiPage x:Class="EditListItem.Views.Pages.ListPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:EditListItem.Views.Pages" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:userControls="clr-namespace:EditListItem.Views.UserControls" Title="ListPage" d:DataContext="{d:DesignInstance local:ListPage, IsDesignTimeCreatable=False}" d:DesignHeight="450" d:DesignWidth="800" d:Background="{DynamicResource ApplicationBackgroundBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}" mc:Ignorable="d"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="9*"></RowDefinition> <RowDefinition Height="1*"></RowDefinition> </Grid.RowDefinitions> <Grid Grid.Row="0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="6*"></ColumnDefinition> <ColumnDefinition Width="4*"></ColumnDefinition> </Grid.ColumnDefinitions> <ListView ItemsSource="{Binding ViewModel.Actions, Mode=TwoWay}" SelectionMode="Single" SelectedItem="{Binding ViewModel.SelectedAction, Mode=TwoWay}"> </ListView> <userControls:TransactionUserControl Grid.Column="1" Transaction="{Binding ViewModel.SelectedAction, Mode=TwoWay}"/> </Grid> <WrapPanel Grid.Row="1"> <Button Content="Deposit" Command="{Binding ViewModel.AddDepositCommand}" /> <Button Content="Withdrawal" Command="{Binding ViewModel.AddWithdrawalCommand}" /> <Label Content="{Binding ViewModel.SelectedAction, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> </WrapPanel> </Grid> </ui:UiPage>
ListPage.xaml.cs
public partial class ListPage : INavigableView<ViewModels.ListViewModel> { public ViewModels.ListViewModel ViewModel { get; } public ListPage(ViewModels.ListViewModel viewModel) { ViewModel = viewModel; InitializeComponent(); } }
ListViewModel.cs
public partial class ListViewModel : ObservableObject, INavigationAware { private bool _isInitialized = false; public ObservableCollection<IAction> Actions { get; set; } = new(); [ObservableProperty] private IAction? _selectedAction; public ListViewModel() { Actions = new() { new Deposit() { Id = "1", Timestamp = "00000000", Amount = 0, Symbol = "Symbol1" }, new Withdrawal() { Id = "2", Timestamp = "00000000", Amount = 0, Symbol = "Symbol2" } }; } [RelayCommand] private async void AddDeposit() { Actions.Add(new Deposit()); } [RelayCommand] private async void AddWithdrawal() { Actions.Add(new Withdrawal()); } }
TransactionUserControl.xaml
<UserControl x:Class="EditListItem.Views.UserControls.TransactionUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:userControls="clr-namespace:EditListItem.Views.UserControls" d:Background="{DynamicResource ApplicationBackgroundBrush}" d:Foreground="{DynamicResource TextFillColorPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="400" > <StackPanel DataContext="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}"> <TextBox Text="{Binding Transaction.Id, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/> <TextBox Text="{Binding Transaction.Timestamp, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/> <TextBox Text="{Binding Transaction.Amount, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/> <TextBox Text="{Binding Transaction.Symbol, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/> </StackPanel> </UserControl>
TransactionUserControl.xaml.cs
public partial class TransactionUserControl { private static DependencyProperty _transactionProperty = DependencyProperty.Register(nameof(Transaction), typeof(IAction), typeof(TransactionUserControl), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public IAction Transaction { get => (IAction)GetValue(_transactionProperty); set => SetValue(_transactionProperty, value); } public TransactionUserControl() { //DataContext = this; InitializeComponent(); } }
模型接口及实现
public interface IAction { public string Id { get; set; } public string Timestamp { get; set; } public double Amount { get; set; } public string Symbol { get; set; } } public record Deposit : IAction { public string Id { get; set; } public string Timestamp { get; set; } public double Amount { get; set; } public string Symbol { get; set; } }
问题解答
1. 编辑后选中项无法切换的解决方法
问题根源在于使用了record类型(Deposit是record),record具有值类型语义,编辑属性时会生成新的实例,导致SelectedAction绑定的对象与Actions集合中的原始对象引用不一致。ListView选中项依赖对象引用匹配,因此无法正确更新绑定。
解决步骤:
- 将
Deposit、Withdrawal从record改为class,并实现INotifyPropertyChanged(利用MVVM Community Toolkit的ObservableObject基类更便捷),确保属性变更时通知UI但不生成新实例:public class Deposit : ObservableObject, IAction { private string _id; public string Id { get => _id; set => SetProperty(ref _id, value); } private string _timestamp; public string Timestamp { get => _timestamp; set => SetProperty(ref _timestamp, value); } private double _amount; public double Amount { get => _amount; set => SetProperty(ref _amount, value); } private string _symbol; public string Symbol { get => _symbol; set => SetProperty(ref _symbol, value); } } - 当前
SelectedAction的TwoWay绑定配置正确(已用[ObservableProperty]实现可观察),无需额外修改。
若坚持使用record,需在编辑后更新Actions集合中的对应实例,但会增加逻辑复杂度,不推荐。
2. 使用ContentControl根据类型加载不同UserControl
ContentControl是完全合适的选择,WPF的DataTemplate机制可自动根据绑定对象的类型匹配对应的模板。
实现步骤:
在页面资源中定义针对不同类型的DataTemplate,每个模板对应专属UserControl:
<ui:UiPage.Resources> <DataTemplate DataType="{x:Type local:Deposit}"> <userControls:DepositUserControl DataContext="{Binding}" /> </DataTemplate> <DataTemplate DataType="{x:Type local:Withdrawal}"> <userControls:WithdrawalUserControl DataContext="{Binding}" /> </DataTemplate> </ui:UiPage.Resources>需提前创建
DepositUserControl和WithdrawalUserControl,分别对应不同类型的编辑界面。替换原有的
TransactionUserControl为ContentControl,绑定Content到SelectedAction:<ContentControl Grid.Column="1" Content="{Binding ViewModel.SelectedAction}" />当
SelectedAction的类型变化时,ContentControl会自动匹配对应的DataTemplate,加载对应的UserControl。简化UserControl的绑定逻辑,直接将DataContext绑定到传入的对象:
例如DepositUserControl.xaml中:<StackPanel> <TextBox Text="{Binding Id, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/> <!-- 其他属性绑定 --> </StackPanel>无需再通过RelativeSource绑定UserControl的依赖属性,ContentControl会自动将
SelectedAction作为UserControl的DataContext。
若不想创建多个UserControl,也可直接在DataTemplate中编写UI元素,根据需求选择即可。
内容的提问来源于stack exchange,提问作者xerxos

