c# - 在WPF中显示所选列表框项目的数据

标签 c# wpf xaml mvvm listbox

我正在寻找帮助。我已经创建了一个非常基本的MVVM设置。我的对象称为VNode,其属性为Name,Age,Kids。我想发生的是,当用户在左侧选择VNode时,它将在右侧显示其深度数据,如下图所示。我不确定该怎么做。

图片1:当前

enter image description here

图片2:目标

enter image description here

如果您不想使用下面的代码来重新创建窗口,则可以从此处获取项目解决方案文件:DropboxFiles

VNode.cs

namespace WpfApplication1
{
    public class VNode
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public int Kids { get; set; }
    }
}

MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="8" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <ListBox Grid.Column="0" Background="AliceBlue" ItemsSource="{Binding VNodes}" SelectionMode="Extended">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <WrapPanel>
                        <TextBlock Text="Name: " />
                        <TextBlock Text="{Binding Name}" FontWeight="Bold" />
                    </WrapPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />

        <ListBox Grid.Column="2" Background="LightBlue" ItemsSource="{Binding VNodes}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <WrapPanel>
                        <TextBlock Text="{Binding Name}" FontWeight="Bold" />
                        <TextBlock Text=":" FontWeight="Bold" />
                        <TextBlock Text=" age:"/>
                        <TextBlock Text="{Binding Age}" FontWeight="Bold" />
                        <TextBlock Text=" kids:"/>
                        <TextBlock Text="{Binding Kids}" FontWeight="Bold" />
                    </WrapPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

    </Grid>
</Window>

MainViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfApplication1
{
    public class MainViewModel : ObservableObject
    {
        private ObservableCollection<VNode> _vnodes;
        public ObservableCollection<VNode> VNodes
        {
            get { return _vnodes; }
            set
            {
                _vnodes = value;
                NotifyPropertyChanged("VNodes");
            }
        }

        Random r = new Random();

        public MainViewModel()
        {
            //hard coded data for testing
            VNodes = new ObservableCollection<VNode>();
            List<string> names = new List<string>() { "Tammy", "Doug", "Jeff", "Greg", "Kris", "Mike", "Joey", "Leslie", "Emily","Tom" };
            List<int> ages = new List<int>() { 32, 24, 42, 57, 17, 73, 12, 8, 29, 31 };

            for (int i = 0; i < 10; i++)
            {
                VNode item = new VNode();

                int x = r.Next(0,9);
                item.Name = names[x];
                item.Age = ages[x];
                item.Kids = r.Next(1, 5);
                VNodes.Add(item);
            }
        }
    }
}

ObservableObject.cs
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WpfApplication1
{
    public class ObservableObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

更新了
举例来说,如何演示用户是否只是在右侧的列表框中选择一个项目,然后在右侧显示所选项目的更深入数据,如下图所示?

enter image description here

最佳答案

这里有三个半答案。第一是良好的WPF常规做法,在ListBox的特定情况下不起作用。第二个是针对ListBox问题的快速而肮脏的解决方法,最后一个是最佳方法,因为它在后面的代码中不执行任何操作。 最少的代码是最好的代码。

执行此操作的第一种方法不需要在ListBox中显示任何项目。它们可以是字符串或整数。如果您的项目类型是一个类(或多个类),并且具有更多的特色,并且您想让每个实例知道是否已选择它,我们将继续进行下去。

您需要为 View 模型提供另一个称为ObservableCollection<VNode>SelectedVNodes或类似的名称。

    private ObservableCollection<VNode> _selectedvnodes;
    public ObservableCollection<VNode> SelectedVNodes
    {
        get { return _selectedvnodes; }
        set
        {
            _selectedvnodes = value;
            NotifyPropertyChanged("SelectedVNodes");
        }
    }

    public MainViewModel()
    {
        VNodes = new ObservableCollection<VNode>();
        SelectedVNodes = new ObservableCollection<VNode>();

        // ...etc., just as you have it now.

如果System.Windows.Controls.ListBox没有损坏,则在第一个ListBox中,您需要将SelectedItems绑定(bind)到该viewmodel属性:
<ListBox 
    Grid.Column="0" 
    Background="AliceBlue" 
    ItemsSource="{Binding VNodes}" 
    SelectedItems="{Binding SelectedVNodes}"
    SelectionMode="Extended">

该控件将负责SelectedVNodes的内容。您还可以通过编程方式更改SelectedVNodes,这将更新两个列表。

但是System.Windows.Controls.ListBox损坏了,您不能将任何东西绑定(bind)到SelectedItems。最简单的解决方法是处理ListBox的SelectionChanged事件,并在后面的代码中对其进行混淆:

XAML:
<ListBox 
    Grid.Column="0" 
    Background="AliceBlue" 
    ItemsSource="{Binding VNodes}" 
    SelectionMode="Extended"
    SelectionChanged="ListBox_SelectionChanged">

C#:
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ListBox lb = sender as ListBox;
    MainViewModel vm = DataContext as MainViewModel;
    vm.SelectedVNodes.Clear();
    foreach (VNode item in lb.SelectedItems)
    {
        vm.SelectedVNodes.Add(item);
    }
}

然后将第二个ListBox中的ItemsSource绑定(bind)到SelectedVNodes:
<ListBox 
    Grid.Column="2" 
    Background="LightBlue" 
    ItemsSource="{Binding SelectedVNodes}">

那应该做您想要的。如果您希望能够以编程方式更新SelectedVNodes并将更改反射(reflect)在两个列表中,则必须让您的代码隐藏类处理 View 模型上的PropertyChanged事件(在代码隐藏的DataContextChanged事件中进行设置)和CollectionChanged事件在viewmodel.SelectedVNodes上-并记住每次SelectedVNodes更改其自身值时都要重新设置CollectionChanged处理程序。它变得丑陋。

更好的长期解决方案是为ListBox编写一个附件属性,该属性替换SelectedItems并可以正常使用。但是这种纠缠至少暂时会让您动起来。

更新

OP建议,这是第二种方法。我们没有维护选定的项目集合,而是在每个项目上都放置了一个标志,并且 View 模型具有主项目列表的过滤版本,该版本仅返回选定的项目。我在如何将VNode.IsSelected绑定(bind)到ListBoxItem的IsSelected属性上画了一个空白,所以我只是在后面的代码中做到了。

VNode.cs:
using System;
namespace WpfApplication1
{
    public class VNode
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public int Kids { get; set; }

        //  A more beautiful way to do this would be to write an IVNodeParent
        //  interface with a single method that its children would call 
        //  when their IsSelected property changed -- thus parents would 
        //  implement that, and they could name their "selected children" 
        //  collection properties anything they like. 
        public ObservableObject Parent { get; set; }

        private bool _isSelected = false;
        public bool IsSelected
        {
            get { return _isSelected; }
            set
            {
                if (value != _isSelected)
                {
                    _isSelected = value;
                    if (null == Parent)
                    {
                        throw new NullReferenceException("VNode.Parent must not be null");
                    }
                    Parent.NotifyPropertyChanged("SelectedVNodes");
                }
            }
        }
    }
}

MainViewModel.cs:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfApplication1
{
    public class MainViewModel : ObservableObject
    {
        private ObservableCollection<VNode> _vnodes;
        public ObservableCollection<VNode> VNodes
        {
            get { return _vnodes; }
            set
            {
                _vnodes = value;
                NotifyPropertyChanged("VNodes");
                NotifyPropertyChanged("SelectedVNodes");
            }
        }

        public IEnumerable<VNode> SelectedVNodes
        {
            get { return _vnodes.Where(vn => vn.IsSelected); }
        }

        Random r = new Random();

        public MainViewModel()
        {
            //hard coded data for testing
            VNodes = new ObservableCollection<VNode>();

            List<string> names = new List<string>() { "Tammy", "Doug", "Jeff", "Greg", "Kris", "Mike", "Joey", "Leslie", "Emily","Tom" };
            List<int> ages = new List<int>() { 32, 24, 42, 57, 17, 73, 12, 8, 29, 31 };

            for (int i = 0; i < 10; i++)
            {
                VNode item = new VNode();

                int x = r.Next(0,9);
                item.Name = names[x];
                item.Age = ages[x];
                item.Kids = r.Next(1, 5);
                item.Parent = this;
                VNodes.Add(item);
            }
        }
    }
}

MainWindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            foreach (VNode item in e.RemovedItems)
            {
                item.IsSelected = false;
            }
            foreach (VNode item in e.AddedItems)
            {
                item.IsSelected = true;
            }
        }
    }
}

MainWindow.xaml(部分):
    <ListBox 
        Grid.Column="0" 
        Background="AliceBlue" 
        ItemsSource="{Binding VNodes}" 
        SelectionMode="Extended"
        SelectionChanged="ListBox_SelectionChanged">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <WrapPanel>
                    <TextBlock Text="Name: " />
                    <TextBlock Text="{Binding Name}" FontWeight="Bold" />
                </WrapPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

    <GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />

    <ListBox Grid.Column="2" Background="LightBlue" ItemsSource="{Binding SelectedVNodes}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <WrapPanel>
                    <TextBlock Text="{Binding Name}" FontWeight="Bold" />
                    <TextBlock Text=":" FontWeight="Bold" />
                    <TextBlock Text=" age:"/>
                    <TextBlock Text="{Binding Age}" FontWeight="Bold" />
                    <TextBlock Text=" kids:"/>
                    <TextBlock Text="{Binding Kids}" FontWeight="Bold" />
                </WrapPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

更新2

最后,这里是您如何使用绑定(bind)的方法(感谢OP帮我弄清楚如何将数据项属性绑定(bind)到ListBoxItem属性-我应该能够接受他的评论作为答案!):

在MainWindow.xaml中,摆脱SelectionCanged事件(是!),并设置一个Style,使其仅对第一个ListBox中的项目进行绑定(bind)。在第二个ListBox中,该绑定(bind)将产生一些问题,我将留给其他人解决。我猜想,通过摆弄VNode.IsSelected.set中的通知和分配顺序可以解决此问题,但我对此可能大错特错。无论如何,绑定(bind)在第二个ListBox中没有任何作用,因此没有理由将其放在那里。
    <ListBox 
        Grid.Column="0" 
        Background="AliceBlue" 
        ItemsSource="{Binding VNodes}" 
        SelectionMode="Extended"
        >
        <ListBox.Resources>
            <Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
                <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
            </Style>
        </ListBox.Resources>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <WrapPanel>
                    <TextBlock Text="Name: " />
                    <TextBlock Text="{Binding Name}" FontWeight="Bold" />
                </WrapPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

...然后我从后面的代码中删除了事件处理程序方法。但是您根本没有添加它,因为您比我聪明,并且从答案的最后一个版本开始。

在VNode.cs中,VNode变为ObservableObject,因此他可以公布其选择状态,并且还可以在IsSelected.set中触发相应的通知。他仍然必须为其父项的SelectedVNodes属性触发更改通知,因为第二个列表框(或SelectedVNodes的任何其他使用方)需要知道所选VNode的集合已更改。

另一种方法是将SelectedVNodes再次设置为ObservableCollection,并在其选定状态更改时让VNode在其中添加/删除自己。然后,viewmodel必须处理该集合上的CollectionChanged事件,并在将VNode IsSelected属性添加到其中或从中删除时更新它们。如果这样做,将if保留在VNode.IsSelected.set中非常重要,以防止无限递归。
using System;
namespace WpfApplication1
{
    public class VNode : ObservableObject
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public int Kids { get; set; }

        public ObservableObject Parent { get; set; }

        private bool _isSelected = false;
        public bool IsSelected
        {
            get { return _isSelected; }
            set
            {
                if (value != _isSelected)
                {
                    _isSelected = value;
                    if (null == Parent)
                    {
                        throw new NullReferenceException("VNode.Parent must not be null");
                    }
                    Parent.NotifyPropertyChanged("SelectedVNodes");
                    NotifyPropertyChanged("IsSelected");
                }
            }
        }
    }
}

更新3

OP询问有关在详细信息 Pane 中显示单个选择的问题。我将旧的多细节 Pane 留在原处,以演示共享模板。

Version 3

这很简单,因此我做了一些详细说明。您只能在XAML中执行此操作,但是我在viewmodel中添加了SelectedVNode属性以进行演示。它没有任何用处,但是,如果您想抛出一个对选定项目进行操作的命令(例如), View 模型将通过这种方式知道用户所指的项目。

MainViewModel.cs
//  Add to MainViewModle class
private VNode _selectedVNode = null;
public VNode SelectedVNode
{
    get { return _selectedVNode; }
    set
    {
        if (value != _selectedVNode)
        {
            _selectedVNode = value;
            NotifyPropertyChanged("SelectedVNode");
        }
    }
}

MainWindow.xaml:
<Window x:Class="WpfApplication1.MainWindow"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <SolidColorBrush x:Key="ListBackgroundBrush" Color="Ivory" />

        <DataTemplate x:Key="VNodeCardTemplate">
            <Grid>
                <Border 
                    x:Name="BackgroundBorder"
                    BorderThickness="1"
                    BorderBrush="Silver"
                    CornerRadius="16,6,6,6"
                    Background="White"
                    Padding="6"
                    Margin="4,4,8,8"
                    >
                    <Border.Effect>
                        <DropShadowEffect BlurRadius="2" Opacity="0.25" ShadowDepth="4" />
                    </Border.Effect>
                    <Grid
                        x:Name="ContentGrid"
                        >
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" />
                            <!-- Each gets half of what's left -->
                            <ColumnDefinition Width="0.5*" />
                            <ColumnDefinition Width="0.5*" />
                        </Grid.ColumnDefinitions>

                        <Border
                            Grid.Row="0" Grid.RowSpan="3"
                            VerticalAlignment="Top"
                            Grid.Column="0"
                            BorderBrush="{Binding Path=BorderBrush, ElementName=BackgroundBorder}"
                            BorderThickness="1"
                            CornerRadius="9,4,4,4"
                            Margin="2,2,6,2"
                            Padding="4"
                            >
                            <StackPanel Orientation="Vertical">
                                <StackPanel.Effect>
                                    <DropShadowEffect BlurRadius="2" Opacity="0.25" ShadowDepth="2" />
                                </StackPanel.Effect>
                                <Ellipse
                                    Width="16" Height="16"
                                    Fill="DarkOliveGreen"
                                    Margin="0,0,0,2"
                                    HorizontalAlignment="Center"
                                    />
                                <Border
                                    CornerRadius="6,6,2,2"
                                    Background="DarkOliveGreen"
                                    Width="36"
                                    Height="18"
                                    Margin="0"
                                    />
                            </StackPanel>
                        </Border>

                        <TextBlock Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding Name}" FontWeight="Bold" />
                        <Separator Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Background="{Binding Path=BorderBrush, ElementName=BackgroundBorder}" Margin="0,3,0,3" />
                        <!-- 
                        Mode=OneWay on Run.Text because bindings on that property should default to that, but don't. 
                        And if you bind TwoWay to a property without a setter, it throws an exception. 
                        -->
                        <TextBlock Grid.Row="2" Grid.Column="1"><Bold>Age:</Bold> <Run Text="{Binding Age, Mode=OneWay}" /></TextBlock>
                        <TextBlock Grid.Row="2" Grid.Column="2"><Bold>Kids:</Bold> <Run Text="{Binding Kids, Mode=OneWay}" /></TextBlock>
                    </Grid>
                </Border>
            </Grid>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding}" Value="{x:Null}">
                    <Setter TargetName="ContentGrid" Property="Visibility" Value="Hidden" />
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>

        <Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
            <!-- I think this should be the default, but it isn't.  -->
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
        </Style>
    </Window.Resources>

    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="8" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="0.5*" />
            <RowDefinition Height="0.5*" />
        </Grid.RowDefinitions>

        <ListBox 
            x:Name="VNodeMasterList"
            Grid.Column="0" 
            Grid.Row="0"
            Grid.RowSpan="2" 
            Background="{StaticResource ListBackgroundBrush}" 
            ItemsSource="{Binding VNodes}" 
            SelectionMode="Extended"
            SelectedItem="{Binding SelectedVNode}"
            >
            <ListBox.Resources>
                <Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
                    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
                </Style>
            </ListBox.Resources>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <WrapPanel>
                        <TextBlock Text="Name: " />
                        <TextBlock Text="{Binding Name}" FontWeight="Bold" />
                    </WrapPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <GridSplitter Grid.Column="1" Grid.RowSpan="2" Grid.Row="0" Width="5" HorizontalAlignment="Stretch" />

        <Border
            Grid.Column="2" 
            Grid.Row="0"
            Background="{StaticResource ListBackgroundBrush}" 
            >
            <ContentControl
                Content="{Binding ElementName=VNodeMasterList, Path=SelectedItem}"
                ContentTemplate="{StaticResource VNodeCardTemplate}"
                />
        </Border>

        <ListBox 
            Grid.Column="2" 
            Grid.Row="1"
            Background="{StaticResource ListBackgroundBrush}" 
            ItemsSource="{Binding SelectedVNodes}"
            ItemTemplate="{StaticResource VNodeCardTemplate}"
            />

    </Grid>
</Window>

关于c# - 在WPF中显示所选列表框项目的数据,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34314339/

相关文章:

c# - 在 C# 中将数据表转换为 JSON

c# - 如何设置ListView的ItemsSource?

c# - 将 VLC 播放器嵌入到 WPF 应用程序中

.net - 在ListView中同步IsSelected和SelectedItem

xaml - 一旦选中,如何防止RadioButton被取消选中?

c# - 在运行时为动态创建的对象动态创建绑定(bind)

c# - 创建新数据库还是使用现有的 ASP.NET Identity 数据库?

c# - 使用 Reflection.Emit 调用基类方法

c# - NavigationService.导航错误

c# - 如何在 C# WPF 中以重复模式运行 MediaPlayer?