Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Display selected listbox items' data in wpf

I'm in search of some help. I've created a very basic MVVM setup. My object is called VNode which has the properties Name,Age,Kids. What I want to happen is when the user selects VNodes on the left, it displays their more in depth data on the right as scene in the image below. I'm not sure how to go about doing this.

image 1: Current

enter image description here

Image 2: Goal

enter image description here

If you don't feel like using the code below to recreate the window you can grab the project solution files from 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));
            }
        }
    }
}

UPDATED For the sake of example, how about demonstrating if the user just selects a single item in the ListBox on the right, it then displays that selected items more in-depth data on the right as shown in the image below?

enter image description here

like image 355
JokerMartini Avatar asked Dec 16 '15 14:12

JokerMartini


1 Answers

There are three and a half answers here. Number one is good general WPF practice that doesn't work in the specific case of ListBox. The second one is a quick and dirty workaround for the problem with ListBox, and the last is the best, because it does nothing in code behind. Least code behind is best code behind.

The first way to do this doesn't require anything of the items you're displaying in the ListBox. They could be strings or integers. If your item type (or types) is a class (or are classes) with a little more meat to it, and you'd like to have each instance know whether it's been selected or not, we'll get to that next.

You need to give your view model another ObservableCollection<VNode> called SelectedVNodes or some such.

    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.

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

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

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);
    }
}

Then bind ItemsSource in your second ListBox to SelectedVNodes:

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

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

Here's a second way of doing it, which OP suggested. Instead of maintaining a selected item collection, we put a flag on each item, and the viewmodel has a filtered version of the main item list that returns only selected items. I'm drawing a blank on how to bind VNode.IsSelected to the IsSelected property on ListBoxItem, so I just did that in the code behind.

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 (partial):

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

Update 2

And here, finally, is how you do it with binding (thanks to OP for figuring out for me how to bind data item properties to ListBoxItem properties -- I should be able to accept his comment as an answer!):

In MainWindow.xaml, get rid of the SelectionCanged event (yay!), and set a Style to do the binding only on the items in the first ListBox. In the second ListBox, that binding will create problems which I'll leave to somebody else to resolve; I have a guess that it might be fixable by fiddling with the order of notifications and assignments in VNode.IsSelected.set, but I could be wildly wrong about that. Anyway the binding serves no purpose in the second ListBox so there's no reason to have it there.

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

...and I removed the event handler method from the codebehind. But you didn't add it at all, because you're smarter than me and you started with this last version of the answer.

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");
                }
            }
        }
    }
}

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");
        }
    }
}

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>
like image 158
15ee8f99-57ff-4f92-890c-b56153 Avatar answered Nov 03 '22 07:11

15ee8f99-57ff-4f92-890c-b56153