You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于WPF、C#和.NET 7,如何通过UserControl编辑ListView选中项

WPF MVVM 编辑选中项后切换异常及类型匹配控件加载问题

我正在学习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:Item 1 selected, no edit
  • 未编辑时选中项2:Item 2 selected, no edit
  • 编辑项2后再次选中项1,选中项未更新:Again Item 1 selected, but item 2 edited

我的两个问题

  1. 如何实现编辑选中项后仍能正常切换选中项?是否需要额外逻辑处理?
  2. 如何使用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机制可自动根据绑定对象的类型匹配对应的模板。

实现步骤:

  1. 在页面资源中定义针对不同类型的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,分别对应不同类型的编辑界面。

  2. 替换原有的TransactionUserControl为ContentControl,绑定Content到SelectedAction:

    <ContentControl Grid.Column="1" Content="{Binding ViewModel.SelectedAction}" />
    

    当SelectedAction的类型变化时,ContentControl会自动匹配对应的DataTemplate,加载对应的UserControl。

  3. 简化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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 07:57:02