Edit WPF listview item and commit changes - c#

Updated
Link to code: Dropbox files
I have a WPF listview populated with a data which is bound through itemsSource. The item bound is a class object called User which contains a handful of properties such as Name, Age, Mail and Info.
What I want to do is be able to double click any listview item and popup an edit dialog which will allow me to change the Mail and Info of that individual. Those changes however will only be committed when i hit OK otherwise they are ignored.
The problem or place where I'm stuck at is, how do i populate the Edit Dialog with the selected listview item's info/mail?
How do i commit those changes back to the item and update the main listview? The rest of the code regarding the popup dialog and whatnot is all implemented already.
The Main Dialog:
XAML - Main Dialog
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="240" Width="350"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView Name="lvDataBinding" Grid.Row="0" Background="LightBlue">
<ListView.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="Name: " />
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text=", " />
<TextBlock Text="Age: " />
<TextBlock Text="{Binding Age}" FontWeight="Bold" />
<TextBlock Text=" (" />
<TextBlock Text="{Binding Mail}" TextDecorations="Underline" Foreground="Blue" Cursor="Hand" />
<TextBlock Text=")" />
</WrapPanel>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<EventSetter Event="MouseDoubleClick" Handler="listViewItem_MouseDoubleClick" />
</Style>
</ListView.ItemContainerStyle>
</ListView>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Label Content="Selected:" HorizontalAlignment="Left"/>
<Label Content="{Binding SelectedItem.Info, ElementName=lvDataBinding}" HorizontalAlignment="Left" Width="140"/>
<!--<Label Content="{Binding ElementName=lvDataBinding, Path=SelectedItem.Name}" HorizontalAlignment="Left" Margin="72,172,0,0" Width="140"/>-->
</StackPanel>
</Grid>
</Window>
CS - Main Dialog
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<User> items = new List<User>();
items.Add(new User() { Name = "John Doe", Age = 42, Mail = "john#doe-family.com", Info = "A basketball player"});
items.Add(new User() { Name = "Jane Doe", Age = 39, Mail = "jane#doe-family.com", Info = "A soccer player" });
items.Add(new User() { Name = "Sammy Doe", Age = 13, Mail = "sammy.doe#gmail.com", Info = "A hockey player" });
lvDataBinding.ItemsSource = items;
}
private void listViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
ListViewItem item = sender as ListViewItem;
object obj = item.Content;
//User user = sender as User;
Console.WriteLine(item);
// popup window
Window editDialog = new EditWindow();
editDialog.Owner = this;
editDialog.ShowDialog();
if (editDialog.DialogResult.HasValue && editDialog.DialogResult.Value)
{
Console.WriteLine("User pressed OK");
;
}
else
{
Console.WriteLine("User pressed Cancel");
}
}
}
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Mail { get; set; }
public string Info { get; set; }
}
}
The Edit Dialog:
XAML - Edit Dialog
<Window x:Class="WpfApplication1.EditWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="EditWindow" Height="140" Width="200"
WindowStartupLocation="CenterScreen">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="Email: " Grid.Row="0" Grid.Column="0"/>
<TextBox Background="AliceBlue" Grid.Row="0" Grid.Column="1" AcceptsReturn="True" TextWrapping="Wrap" DockPanel.Dock="Right"/>
<Label Content="Info: " Grid.Row="1" Grid.Column="0"/>
<TextBox Background="AliceBlue" Grid.Row="1" Grid.Column="1" AcceptsReturn="True" TextWrapping="Wrap" DockPanel.Dock="Right"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Button Content="Cancel" MinWidth="50" Height="25" Click="ButtonCancelClick"/>
<Button Content="OK" MinWidth="50" Height="25" Click="ButtonOkClick"/>
</StackPanel>
</Grid>
</Window>
CS - Edit Dialog
using System.Windows;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for EditWindow.xaml
/// </summary>
public partial class EditWindow : Window
{
public EditWindow()
{
InitializeComponent();
}
private void ButtonOkClick(object sender, RoutedEventArgs e)
{
DialogResult = true;
this.Close();
}
private void ButtonCancelClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
this.Close();
}
}
}

There are many things to change in order to get this working.
Implement INotifyPropertyChanged on your User class
You cannot have two way binding in wPF without it. Just google and read till you understand how it works.
Create a public CurrentItem property on your EditWindow
(listViewItem_MouseDoubleClick event)
Create a new instance of User class (myUser) and copy all property values from item.Content (you first need to typecast this as a user object)
Set the new CurrentItem of editDialog.CurrentItem = myUser
if the user presses ok copy all properties of editDialog.CurrentItem back to item.Content object (remember this should be already typecasted to User)
Create all Bindins on EditWindow pointing to CurrentItem object.
Hope that helps.

Related

Cannot change the VirtualizationMode attached property on an ItemsControl after Measure is called on the ItemsHost panel

When i try to set VirtualizationMode on my ListView to Recycling I get the error from the title:
Cannot change the VirtualizationMode attached property on an
ItemsControl after Measure is called on the ItemsHost panel.
I am trying to set the attached property programmatically but when I try to define the VirtualizationMode in XAML designer throws the same error from the title. Anyone had similar problems like this?
My view in XAML is:
<Window x:Class="FinalVirtualizationApp.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:FinalVirtualizationApp"
mc:Ignorable="d"
Title="MainWindow" Height="900" Width="800"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
TextElement.Foreground="{DynamicResource MaterialDesignBody}"
TextElement.FontWeight="Regular"
TextElement.FontSize="13"
TextOptions.TextFormattingMode="Ideal"
TextOptions.TextRenderingMode="Auto"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<GroupBox Grid.Row="0" Header="UI virtualization options">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox Content="UI virtualization" VerticalAlignment="Center" IsChecked="{Binding IsUIVirtualization}"/>
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Content="Container Recycling:" VerticalAlignment="Center"/>
<ComboBox Grid.Column="1" VerticalAlignment="Center" SelectedValue="{Binding ContainerRecyclingType}" SelectedValuePath="Content">
<ComboBoxItem>Recycling</ComboBoxItem>
<ComboBoxItem>Standard</ComboBoxItem>
</ComboBox>
</Grid>
<CheckBox Content="Deferred scrolling" Grid.Row="1" VerticalAlignment="Center" IsChecked="{Binding IsDeferredScrolling}">
</CheckBox>
<Grid Grid.Row="1" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="74*"/>
<ColumnDefinition Width="253*"/>
</Grid.ColumnDefinitions>
<Label Content="Scroll unit" VerticalAlignment="Center" Margin="0,0,0,1"/>
<ComboBox Grid.Column="1" VerticalAlignment="Center" SelectedValue="{Binding ScrollUnitType}" SelectedValuePath="Content" Grid.ColumnSpan="2" Margin="0,2,0,3">
<ComboBoxItem>Item</ComboBoxItem>
<ComboBoxItem>Pixel</ComboBoxItem>
</ComboBox>
</Grid>
</Grid>
</GroupBox>
<GroupBox Grid.Row="1" Header="Data virtualization">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<GroupBox Header="ItemsProvider">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="Number of Items: "/>
<TextBox Width="60" Text="{Binding NumberOfItems}"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Label Content="Fetch delay(ms): "/>
<TextBox Width="60" Text="{Binding FetchDelay}"/>
</StackPanel>
</Grid>
</GroupBox>
<GroupBox Grid.Row="1" Header="Collection">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,2,0,0">
<TextBlock Text="Type:" Margin="5" TextAlignment="Right" VerticalAlignment="Center"/>
<RadioButton x:Name="rbNormal" GroupName="rbGroup" Margin="5" Content="List(T)" VerticalAlignment="Center" Command="{Binding CollectionTypeChangeCommand}" CommandParameter="List"/>
<RadioButton x:Name="rbVirtualizing" GroupName="rbGroup" Margin="5" Content="VirtualizingList(T)" VerticalAlignment="Center" Command="{Binding CollectionTypeChangeCommand}" CommandParameter="VirtualizingList"/>
<RadioButton x:Name="rbAsync" GroupName="rbGroup" Margin="5" Content="AsyncVirtualizingList(T)" VerticalAlignment="Center" Command="{Binding CollectionTypeChangeCommand}" CommandParameter="AsyncVirtualizingList"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,2,0,0">
<TextBlock Text="Page size:" Margin="5" TextAlignment="Right" VerticalAlignment="Center"/>
<TextBox x:Name="tbPageSize" Margin="5" Text="{Binding PageSize}" Width="60" VerticalAlignment="Center"/>
<TextBlock Text="Page timeout (s):" Margin="5" TextAlignment="Right" VerticalAlignment="Center"/>
<TextBox x:Name="tbPageTimeout" Margin="5" Text="{Binding PageTimeout}" Width="60" VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
</GroupBox>
</Grid>
</GroupBox>
<StackPanel Orientation="Horizontal" Grid.Row="2">
<TextBlock Text="Memory Usage:" Margin="5" VerticalAlignment="Center"/>
<TextBlock x:Name="tbMemory" Margin="5" Width="80" Text="{Binding MemoryUsage}" VerticalAlignment="Center"/>
<Button Content="Refresh" Margin="5" Width="100" VerticalAlignment="Center" Command="{Binding RefreshCommand}"/>
</StackPanel>
<ListView Grid.Row="3" Name="lvItems">
<ListView.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding DeviceName}">
<StackPanel>
</StackPanel>
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Window>
And i try to set the VirtualizationMode here:
public void SetUIVirtualizationOptions()
{
listView.SetValue(VirtualizingStackPanel.IsVirtualizingProperty, IsUIVirtualization);
listView.SetValue(ScrollViewer.IsDeferredScrollingEnabledProperty, IsDeferredScrolling);
if(ContainerRecyclingType == "Recycling")
listView.SetValue(VirtualizingStackPanel.VirtualizationModeProperty, VirtualizationMode.Recycling);
else
listView.SetValue(VirtualizingStackPanel.VirtualizationModeProperty, VirtualizationMode.Standard);
if (ScrollUnitType == "Item")
listView.SetValue(VirtualizingPanel.ScrollUnitProperty, ScrollUnit.Item);
else
listView.SetValue(VirtualizingPanel.ScrollUnitProperty, ScrollUnit.Pixel);
}
Edit: My Window code behind where i pass the listview to viewmodel is:
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel(lvItems);
}
}
And the viewmodel part of the code (minus getters and setters for less space taken):
public class MainWindowViewModel : BindableBase
{
#region private fields
private ListView listView;
private bool isUIVirtualization;
private bool isDeferredScrolling;
private string containerRecyclingType;
private string scrollUnitType;
private int numberOfItems;
private int fetchDelay;
private string collectionType;
private int pageSize;
private int pageTimeout;
private string memoryUsage;
private DemoItemProvider itemsProvider;
#endregion
#region commands
public RelayCommand<string> CollectionTypeChangeCommand { get; set; }
public RelayCommand RefreshCommand { get; set; }
public RelayCommand<string> ContainerRecyclingTypeChangeCommand { get; set; }
public RelayCommand<string> ScrollUnitTypeChangeCommand { get; set; }
#endregion
public MainWindowViewModel(ListView lvItems)
{
this.listView = lvItems;
PageSize = 100;
PageTimeout = 30;
NumberOfItems = 1000000;
FetchDelay = 1000;
CollectionTypeChangeCommand = new RelayCommand<string>(CollectionTypeChangeFunc);
RefreshCommand = new RelayCommand(RefreshFunc);
ContainerRecyclingTypeChangeCommand = new RelayCommand<string>(ContainerRecyclingTypeChangeFunc);
ScrollUnitTypeChangeCommand = new RelayCommand<string>(ScrollUnitTypeChangeFunc);
// use a timer to periodically update the memory usage
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = new TimeSpan(0, 0, 1);
timer.Tick += timer_Tick;
timer.Start();
}
private void timer_Tick(object sender, EventArgs e)
{
MemoryUsage = string.Format("{0:0.00} MB", GC.GetTotalMemory(true) / 1024.0 / 1024.0);
}
#region command methods
public void CollectionTypeChangeFunc(string type)
{
CollectionType = type;
}
public void RefreshFunc()
{
SetUIVirtualizationOptions();
itemsProvider = new DemoItemProvider(NumberOfItems, FetchDelay);
if (collectionType == "List")
{
listView.ItemsSource = new List<DataItem>(itemsProvider.FetchRange(0, itemsProvider.FetchCount()));
}
else if (collectionType == "VirtualizingList")
{
listView.ItemsSource = new VirtualizingCollection<DataItem>(itemsProvider, pageSize);
}
else if (collectionType == "AsyncVirtualizingList")
{
listView.ItemsSource = new AsyncVirtualizingCollection<DataItem>(itemsProvider, pageSize, pageTimeout * 1000);
}
}
public void ContainerRecyclingTypeChangeFunc(string type)
{
ContainerRecyclingType = type;
}
public void ScrollUnitTypeChangeFunc(string type)
{
ScrollUnitType = type;
}
public void SetUIVirtualizationOptions()
{
listView.SetValue(VirtualizingStackPanel.IsVirtualizingProperty, IsUIVirtualization);
listView.SetValue(ScrollViewer.IsDeferredScrollingEnabledProperty, IsDeferredScrolling);
if(ContainerRecyclingType == "Recycling")
listView.SetValue(VirtualizingStackPanel.VirtualizationModeProperty, VirtualizationMode.Recycling);
else
listView.SetValue(VirtualizingStackPanel.VirtualizationModeProperty, VirtualizationMode.Standard);
if (ScrollUnitType == "Item")
listView.SetValue(VirtualizingPanel.ScrollUnitProperty, ScrollUnit.Item);
else
listView.SetValue(VirtualizingPanel.ScrollUnitProperty, ScrollUnit.Pixel);
}
#endregion
}
The code source for VirtualizingStackPanel indicates that this property can only be set before initialization of the panel:
/// <summary>
/// Attached property for use on the ItemsControl that is the host for the items being
/// presented by this panel. Use this property to modify the virtualization mode.
///
/// Note that this property can only be set before the panel has been initialized
/// </summary>
public static readonly DependencyProperty VirtualizationModeProperty =
DependencyProperty.RegisterAttached("VirtualizationMode", typeof(VirtualizationMode), typeof(VirtualizingStackPanel),
new FrameworkPropertyMetadata(VirtualizationMode.Standard));
You can indeed check this behavior later in the file:
//
// Set up info on first measure
//
if (HasMeasured)
{
VirtualizationMode oldVirtualizationMode = InRecyclingMode ? VirtualizationMode.Recycling : VirtualizationMode.Standard;
if (oldVirtualizationMode != virtualizationMode)
{
throw new InvalidOperationException(SR.Get(SRID.CantSwitchVirtualizationModePostMeasure));
}
}
else
{
HasMeasured = true;
}
and there is no way (according to source code) to set this HasMeasured property back to False unless you destroy and recreate the ListView.
It is so as the message says:
Your not allowed to _change the VirtualizationMode attached property
on an ItemsControl after Measure is called on the ItemsHost panel.
This means, that if ListView is already shown virtualizing mechanism being used and you are not allowed to change it.
If you set Visibility of ListView in XAML to Collapsed and set it only later to the Visible, then you can set the VirtualizationMode in code behind to wished value, but only once(so you can't change it after ListView become visible)!
private void Button_Click(object sender, RoutedEventArgs e)
{
listView.SetValue(VirtualizingStackPanel.VirtualizationModeProperty, VirtualizationMode.Recycling);
listView.Visibility = Visibility.Visible;
}
XAML:
<ListView x:Name="listView" ... Visibility="Collapsed">

Binding JSON JArray to Listview in UWP Issue

In visual studio universal windows app, I am trying to bind a JArray to a Listview. It seems to compile fine and the quantity of rows populate into the Listview but no data fills the rows. What am I doing wrong here or is this even possible?
using Newtonsoft.Json.Linq;
XAML
<ListView Grid.Row="0" x:Name="lstJobData">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Path=job_id}" FontSize="14" Foreground="White"/>
<TextBlock Grid.Row="1" Text="{Binding Path=job_number}" FontSize="14" Foreground="White"/>
<TextBlock Grid.Row="2" Text="{Binding Path=ship_type}" FontSize="14" Foreground="White"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
JArray from Service layer (converted to Jarray from Json string)
{[
{ "job_id": 399783, "job_number": "XYZ-J111111", "ship_type": "c" },
{ "job_id": 445672, "job_number": "XYZ-J222222", "ship_type": "p" },
{ "job_id": 896543, "job_number": "XYZ-J333333", "ship_type": "f" }
]}
XAML (Code behind)
private async void Button1_Click(object sender, RoutedEventArgs e)
{
MyServices.Service1Client client = new MyServices.Service1Client();
var itemSource = await client.GetDTAsync();
JArray result = JArray.Parse(itemSource);
lstJobData.ItemsSource = result;
}
When we set the JArray to the ItemsSource property of the ListView, we can not use the path in the XAML. The path means Property-path, it will point to the property of the item. Also there is no job_id property in the item.
If you want to use path in XAML, we should be able to create a Job class. And in the Job, we should be able to define the job_id, job_number and
ship_type properties.
Then we should be able to add the Job to the ObservableCollection of the Job class.
For example:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button HorizontalAlignment="Right" VerticalAlignment="Top" Click="Button1_Click" >click</Button>
<ListView VerticalAlignment="Bottom" HorizontalAlignment="Center" Grid.Row="0" x:Name="lstJobData">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Path=job_id}" FontSize="14" Foreground="Red" />
<TextBlock Grid.Row="1" Text="{Binding Path=job_number}" FontSize="14" Foreground="Red" />
<TextBlock Grid.Row="2" Text="{Binding Path=ship_type}" FontSize="14" Foreground="Red" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
The code behind:
public sealed partial class MainPage : Page
{
private ObservableCollection<Job> Jobs;
public MainPage()
{
this.InitializeComponent();
Jobs = new ObservableCollection<Job>();
}
private void Button1_Click(object sender, RoutedEventArgs e)
{
string newJson = #"
[
{'job_id':399783,'job_number':'XYZ-J111111','ship_type':'c'},
{'job_id':445672,'job_number':'XYZ-J222222','ship_type':'p'},
{'job_id':896543,'job_number':'XYZ-J333333','ship_type':'f'}
]";
JArray result = JArray.Parse(newJson);
for (int i = 0; i < result.Count; i++)
{
Jobs.Add(new Job() { job_id = (string)result[i]["job_id"], job_number = (string)result[i]["job_number"], ship_type = (string)result[i]["ship_type"] });
}
lstJobData.ItemsSource = Jobs;
}
}
internal class Job
{
public string job_id { get; set; }
public string job_number { get; set; }
public string ship_type { get; set; }
}

MenuItem binding datacontext in listview

I have a ListView with a DataTemplate, inside this templates there are several textblocks and a button. The button has a contextmenu with fixed items. The binding of the listviewitems works fine, but binding a property to the contextmenu does not seem to work.
<ListView x:Name="lv_clients" Margin="0 22 0 0" SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid Grid.Column="0" Grid.RowSpan="2" Background="{Binding StateColor}">
</Grid>
<TextBlock Grid.Column="1" Text="{Binding DisplayString}" Foreground="Black" Height="20" FontWeight="Bold" Padding="2,2,0,0" />
<Button Click="Button_ListItem_Click" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Right" VerticalAlignment="Top">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="Anrufen" Name="mn_call" Click="mn_call_Click" DataContext={Binding Number} />
</ContextMenu>
</Button.ContextMenu> ...</Button>
<StackPanel Grid.Column="1" Grid.Row="1">
<TextBlock Text="{Binding State}" Height="20" Padding="2,2,0,0"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstEntry.KDKGRS}" Height="20" FontWeight="Bold" Padding="2,2,2,0" HorizontalAlignment="Left" Foreground="{Binding FirstEntry.ConvertedKGFARBE}" />
<TextBlock Text="{Binding FirstEntry.ADNAMI}" Height="20" Padding="0,2,0,0" HorizontalAlignment="Left" />
</StackPanel>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I have removed some unnecessary bits of the code (style and columndefinitions) for readability.
The important part is the MenuItem inside the Button. My underlying class has a public string property Number. This should be passed to the menu item. But the DataContext of the MenuItem is always null inside the click event.
I've read something about the contextmenu not beeing part of the visual tree, but I can't wrap my head around it. Could somebody explain the issue with the binding?
Edit Code for the underlying class:
Again removed some unnecessary code for the question
public class PhoneClient
{
public String Name { get; set; }
public String Number { get; set; }
public String Extension { get; set; }
public String DisplayString
{
get
{
return String.IsNullOrEmpty(Name) ? Number : String.Format("{0} ({1})", Name, Extension);
}
}
}
And the binding of the ListBox:
List<PhoneClient> clients = new List<PhoneClient>();
clients = load(); //returns active Clients
lv_clients.ItemsSource = clients;
Bridging the gap
<ContextMenu Tag="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Anrufen" Click="mn_call_Click" Name="mn_call"
DataContext="{Binding Tag.Number, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
</ContextMenu>
Handler
private void mn_call_Click(object sender, RoutedEventArgs e)
{
MenuItem currentMenuItem = (MenuItem)sender;
string number = (string)currentMenuItem.DataContext;
// Do Stuff
}
EDIT
MainWindow.xaml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid Margin="10">
<ListView x:Name="lv_clients" Margin="0 22 0 0" SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Background="LightGray" Width="100">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Background="LightGreen">
</Grid>
<TextBlock Text="{Binding DisplayString}" Foreground="Black" Height="20" FontWeight="Bold" Padding="2,2,0,0" />
<Button Click="Button_ListItem_Click" Grid.Row="0" HorizontalAlignment="Right" VerticalAlignment="Top">
<Button.ContextMenu>
<ContextMenu Tag="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Show" Click="mn_call_Click" Name="mn_call" DataContext="{Binding Tag.Number, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
</ContextMenu>
</Button.ContextMenu> ...
</Button>
<StackPanel Grid.Row="1">
<TextBlock Text="{Binding State}" Height="20" Padding="2,2,0,0"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Foo" Height="20" FontWeight="Bold" Padding="2,2,2,0" HorizontalAlignment="Left" Foreground="Blue" />
<TextBlock Text="Bar" Height="20" Padding="0,2,0,0" HorizontalAlignment="Left" />
</StackPanel>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Collections.Generic;
namespace WpfApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<PhoneClient> clients = new List<PhoneClient>();
clients.Add(new PhoneClient() { Name = "Kumar", Number = "0101010", Extension = "555", State = "New York" });
clients.Add(new PhoneClient() { Name = "Shanaya", Number = "1010101", Extension = "555", State = "New Jersey" });
clients.Add(new PhoneClient() { Name = "Billy Bob", Number = "6543210", Extension = "555", State = "Single" });
lv_clients.ItemsSource = clients;
}
public class PhoneClient
{
public String Name { get; set; }
public String Number { get; set; }
public String Extension { get; set; }
public String State { get; set; }
public String DisplayString
{
get
{
return String.IsNullOrEmpty(Name) ? Number : String.Format("{0} ({1})", Name, Extension);
}
}
}
private void mn_call_Click(object sender, RoutedEventArgs e)
{
MenuItem currentMenuItem = (MenuItem)sender;
string number = (string)currentMenuItem.DataContext;
MessageBox.Show("Number " + number);
}
private void Button_ListItem_Click(object sender, RoutedEventArgs e)
{
Button currentButton = (Button)sender;
PhoneClient data = (PhoneClient)currentButton.DataContext;
MessageBox.Show(data.Name + " tapped");
}
}
}

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
Image 2: Goal
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?
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.
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>

How to show data from selected item in listbox wpf using mvvm?

I am just doing some practice on WPF & MVVM forms
So far, I have just completed the normal Listbox with data loaded from an array, that when the index changes, it loads other data into textblocks on the form
I would like to change this example however. I want the user to select a field in the listbox, then click a button to display all the other data
So, the form is (usercontrol):
And when someone selects, say Cena, they will see the data on the right filled out.
My code for the .xaml is:
<UserControl x:Class="SuperstarsRoster.RosterView"
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:local="clr-namespace:SuperstarsRoster"
xmlns:WpfToolkit="http://schemas.microsoft.com/wpf/2008/toolkit"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="600">
<UserControl.DataContext>
<local:RosterViewModel/>
</UserControl.DataContext>
<Grid x:Name="LayoutRoot" Background="White" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!--SelectedItem="{Binding SelectedPerson}"-->
<ListBox Grid.Column="0" Margin="0"
ItemsSource="{Binding RosterList}"
DisplayMemberPath="Name"
Name="lstRoster"
Height="250"
VerticalAlignment="Top"/>
<Button Content="Exit" Height="23" HorizontalAlignment="Left" VerticalAlignment="Bottom" Name="btnExit" Width="150" Command="{Binding ButtonCommand}" CommandParameter="Hai" />
<Grid x:Name="PersonDetails" Grid.Column="1" DataContext="{Binding SelectedPerson}" Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="150"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="20"/>
<RowDefinition Height="20"/>
<RowDefinition Height="20"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.ColumnSpan="2" Text="Person Details" FontSize="15"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Name"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Name,Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Brand"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Brand,Mode=TwoWay}"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Type"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding Type,Mode=TwoWay}"/>
<Button Grid.Row="4" Content="Choose" Height="23" HorizontalAlignment="Left" VerticalAlignment="Bottom" Name="btnChoose" Width="90" Command="{Binding ChooseCommand}" CommandParameter="{Binding ElementName=lstRoster,Path=SelectedPerson}" />
</Grid>
</Grid>
my problem is the Show button:
<Button Grid.Row="4" Content="Choose" Height="23" HorizontalAlignment="Left" VerticalAlignment="Bottom" Name="btnChoose" Width="90" Command="{Binding ChooseCommand}" CommandParameter="{Binding ElementName=lstRoster,Path=SelectedPerson}" />
I don't know what to do here. I don't know if the CommandParameters should be empty, or have something in it.
My ViewModel code is:
#region Show Button
private ICommand m_ShowCommand;
public ICommand ShowCommand
{
get
{
return m_ShowCommand;
}
set
{
m_ShowCommand = value;
}
}
public void Selected(object obj)
{
RaisePropertyChanged("SelectedPerson");
}
#endregion
I have a feeling my Selected is the incorrect piece of code. On the ViewModel code there is also:
#region Roster Details
public ObservableCollection<Roster> rosterList;
public Roster selectedPerson;
public RosterViewModel()
{
ButtonCommand = new RelayCommand(new Action<object>(ShowMessage));
ShowCommand = new RelayCommand(new Action<object>(Selected));
rosterList = new ObservableCollection<Roster>()
{
new Roster(){Name="Batista", Brand="Smackdown!", Type="Heel"},
new Roster(){Name="The Rock", Brand="RAW", Type="Face"},
new Roster(){Name="John Cena", Brand="RAW", Type="Face"},
new Roster(){Name="Randy Orton", Brand="Smackdown!", Type="Heel"}
};
RaisePropertyChanged("SelectedPerson");
}
#endregion
With the ShowCommand being the button event, but I don't know what to put here.
Ideally, user selects Cena, clicks show, and the textblocks will fill with the data from the array.
My RelayCommand class (the default for all button clicks) looks like so:
class RelayCommand : ICommand
{
private Action<object> _action;
public RelayCommand(Action<object> action)
{
_action = action;
}
#region ICommand Members
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_action(parameter);
}
#endregion
Should there be something more here? Or in the ViewModel?
I'm using MVVM, so there is no code in my code behind class.
The Exit button works, that's why I know the commands will work.
EDIT
I changed some of the code now. Firstly, the show button was sitting in a grid that had a datacontext setting. That was taken out. Next, both buttons share the same command (ButtonCommand), but both have different parameters. Exit is "Exit", and choose is "Hai"
In the ViewModel code, the following was added and changed
public string Name { get; set; }
public string Brand { get; set; }
public string Type { get; set; }
#region Button Work
private ICommand m_ButtonCommand;
public ICommand ButtonCommand
{
get
{
return m_ButtonCommand;
}
set
{
m_ButtonCommand = value;
}
}
public void DoAction(object obj)
{
if (obj.ToString() == "Hai")
{
if (selectedPerson != null)
{
this.Name = selectedPerson.Name;
this.Brand = selectedPerson.Brand;
this.Type = selectedPerson.Type;
RaisePropertyChanged(null);
}
}
if (obj.ToString() == "Exit")
{
MessageBox.Show("This program will now close");
Environment.Exit(0);
}
}
#endregion
This way, data is set and I can populate the fields when wanted.
Along with initializing it first
public RosterViewModel()
{
ButtonCommand = new RelayCommand(new Action<object>(DoAction));
So, it works now.
You have a couple of choices really:
(1) Have a VM property that is bound to the SelectedItem of your ListBox, and then you don't need to send any parameters with your button command ... you can just perform your ChooseCommand on the currently SelectedItem (as known by you VM).
or
(2) Pass in the SelectedItem as a parameter on your command, like this ...
<Button Grid.Row="4" Content="Choose" Height="23" HorizontalAlignment="Left"
VerticalAlignment="Bottom" Name="btnChoose" Width="90"
Command="{Binding ChooseCommand}"
CommandParameter="{Binding ElementName=MyListBox,Path=SelectedItem}" />
Note that you will need to give your ListBox a name (x:Name="MyListBox") and that your CommandParameter is referring to that ListBox, and its SelectedItem property.

Categories

Resources