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

时间:2015-12-16 14:17:17

标签: 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

1 个答案:

答案 0 :(得分:5)

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

执行此操作的第一种方法并不需要您在ListBox中显示的任何项目。它们可以是字符串或整数。如果您的项目类型(或类型)是一个类别(或类别),并且有更多的肉,并且您希望每个实例都知道它是否被选中,我们&#39 ;接下来就到了。

您需要为您的视图模型提供另一个名为ObservableCollection<VNode>的{​​{1}}或其他类型的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绑定到该viewmodel属性:

<ListBox 
    Grid.Column="0" 
    Background="AliceBlue" 
    ItemsSource="{Binding VNodes}" 
    SelectedItems="{Binding SelectedVNodes}"
    SelectionMode="Extended">

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

但System.Windows.Controls.ListBox已损坏,您无法将任何内容绑定到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绑定到SelectedVNodes

<ListBox 
    Grid.Column="2" 
    Background="LightBlue" 
    ItemsSource="{Binding SelectedVNodes}">

这应该做你想要的。如果您希望能够以编程方式更新SelectedVNodes并将更改反映在两个列表中,那么您必须让您的代码隐藏类处理viewmodel上的PropertyChanged事件(在codebehind的DataContextChanged事件)和viewmodel.SelectedVNodes上的CollectionChanged事件 - 并且每次SelectedVNodes更改其自己的值时,请记住再次设置CollectionChanged处理程序。它变得丑陋。

更好的长期解决方案是为ListBox编写附件属性,替换SelectedItems并正常工作。但这个kludge至少会让你暂时感动。

更新

这是第二种做法,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");
                }
            }
        }
    }
}

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

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

在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>

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

在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询问在详细信息窗格中显示单个选择。我留下了旧的多细节窗格,以演示共享模板。

Version 3

这很简单,所以我详细说明了一下。你只能在XAML中这样做,但是我在viewmodel中输入了一个SelectedVNode属性来演示它。它不用于任何东西,但是如果你想抛出一个对所选项目进行操作的命令(例如),那么视图模型将如何知道用户意味着哪个项目。

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>