在wpf中显示所选列表框项的数据

Jok*_*ini 6 c# wpf xaml listbox mvvm

我正在寻求一些帮助.我已经创建了一个非常基本的MVVM设置.我的对象叫做VNode,它有Name,Age,Kids属性.我想要发生的是当用户选择左侧的VNode时,它会在右侧显示更多深度数据作为下图中的场景.我不知道该怎么做.

图1:电流

在此输入图像描述

图2:目标

在此输入图像描述

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

VNode.cs

namespace WpfApplication1
{
    public class VNode
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public int Kids { get; set; }
    }
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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));
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

在此输入图像描述

15e*_*153 7

这里有三个半答案.第一个是良好的通用WPF实践,在ListBox的特定情况下不起作用.第二个是针对ListBox问题的快速而肮脏的解决方法,最后一个是最好的,因为它在后面的代码中什么都不做.最少的代码背后是最好的代码.

第一种方法不需要您在ListBox中显示的任何项目.它们可以是字符串或整数.如果你的项目类型(或类型)是一个类(或类),并且有更多的肉,并且你想让每个实例都知道它是否被选中,我们接下来就会知道.

你需要给你的另一视图模型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.
Run Code Online (Sandbox Code Playgroud)

If System.Windows.Controls.ListBox weren't broken, then in your first ListBox, you would bind SelectedItems to that viewmodel property:

<ListBox 
    Grid.Column="0" 
    Background="AliceBlue" 
    ItemsSource="{Binding VNodes}" 
    SelectedItems="{Binding SelectedVNodes}"
    SelectionMode="Extended">
Run Code Online (Sandbox Code Playgroud)

And the control would be in charge of the content of SelectedVNodes. You could also change SelectedVNodes programmatically, and that would update both lists.

But System.Windows.Controls.ListBox is broken, and you can't bind anything to SelectedItems. The simplest workaround is to handle the ListBox's SelectionChanged event and kludge it in the code behind:

XAML:

<ListBox 
    Grid.Column="0" 
    Background="AliceBlue" 
    ItemsSource="{Binding VNodes}" 
    SelectionMode="Extended"
    SelectionChanged="ListBox_SelectionChanged">
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

Then bind ItemsSource in your second ListBox to SelectedVNodes:

<ListBox 
    Grid.Column="2" 
    Background="LightBlue" 
    ItemsSource="{Binding SelectedVNodes}">
Run Code Online (Sandbox Code Playgroud)

And that should do what you want. If you want to be able to update SelectedVNodes programmatically and have the changes reflected in both lists, you'll have to have your codebehind class handle the PropertyChanged event on the viewmodel (set that up in the codebehind's DataContextChanged event), and the CollectionChanged event on viewmodel.SelectedVNodes -- and remember to set the CollectionChanged handler all over again every time SelectedVNodes changes its own value. It gets ugly.

A better long-term solution would be to write an attachment property for ListBox that replaces SelectedItems and works right. But this kludge will at least get you moving for the time being.

Update

OP提出了第二种方法.我们不是维护选定的项目集合,而是在每个项目上放置一个标志,并且viewmodel具有主项目列表的过滤版本,仅返回所选项目.我在如何将VNode.IsSelected绑定到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");
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

更新2

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

在MainWindow.xaml中,删除SelectionCanged事件(yay!),并设置Style以仅对第一个ListBox中的项进行绑定.在第二个ListBox中,该绑定将产生问题,我将留给其他人解决; 我猜想通过摆弄通知和作业的顺序VNode.IsSelected.set可以解决这个问题,但我可能会非常错误.无论如何,绑定在第二个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>
Run Code Online (Sandbox Code Playgroud)

...我从代码隐藏中删除了事件处理程序方法.但你根本没有添加它,因为你比我更聪明,你开始使用最后一个版本的答案.

In VNode.cs, VNode becomes an ObservableObject so he can advertise his selection status, and he also fires the appropriate notification in IsSelected.set. He still has to fire the change notification for his Parent's SelectedVNodes property, because the second listbox (or any other consumer of SelectedVNodes) needs to know that the set of selected VNodes has changed.

Another way to do that would be to make SelectedVNodes an ObservableCollection again, and have VNode add/remove himself from it when his selected status changes. Then the viewmodel would have to handle CollectionChanged events on that collection, and update the VNode IsSelected properties when they're added to it or removed from it. If you do that, it's very important to keep the if in VNode.IsSelected.set, to prevent infinite recursion.

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");
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Update 3

OP asks about displaying a single selection in a detail pane. I left the old multi-detail pane in place to demonstrate sharing a template.

Version 3

That's pretty simple to do, so I elaborated a bit. You could do this only in the XAML, but I threw in a SelectedVNode property in the viewmodel to demonstrate that as well. It's not used for anything, but if you wanted to throw in a command that operated on the selected item (for example), that's how the view model would know which item the user means.

MainViewModel.cs

//  Add to MainViewModle class
private VNode _selectedVNode = null;
public VNode SelectedVNode
{
    get { return _selectedVNode; }
    set
    {
        if (value != _selectedVNode)
        {
            _selectedVNode = value;
            NotifyPropertyChanged("SelectedVNode");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)