How to add different ListBox items WPF - c#

I am implementing a chat application like this image:
I started by creating a ListBox and setting a ListBox.ItemTemplate, but i can't figure out how can i control the ListBox to add an item with the layout as a message received or as send (just like Whatsapp).
Here is my code:
<ListBox Name="ChatListBox" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Disabled" Background="#00FFFFFF" BorderBrush="{x:Null}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Background="#00FFFFFF"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Focusable" Value="False"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<!--If the user sends a msg-->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="9*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Margin="0" BorderThickness="1" BorderBrush="#9f9f9f" Background="#c4df9b" CornerRadius="10">
<TextBlock Name="MsgText" Background="#c4df9b" Foreground="Black" TextAlignment="Center" TextWrapping="Wrap" Margin="5" Text="{Binding text}" FontSize="14"/>
</Border>
<Image Grid.Column="1" Source="Images/user.png" Margin="5" Height="{Binding ElementName=MsgText, Path=ActualHeight}"/>
<!--
If the user receives a msg
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="9*"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Source="Images/user.png" Margin="5" Height="{Binding ElementName=MsgText, Path=ActualHeight}"/>
<Border Grid.Column="1" Margin="0" BorderThickness="1" BorderBrush="#9f9f9f" Background="#c4df9b" CornerRadius="10">
<TextBlock Name="MsgText" Background="#c4df9b" Foreground="Black" TextAlignment="Center" TextWrapping="Wrap" Margin="5" Text="{Binding text}" FontSize="14"/>
</Border>-->
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Here is my c# code:
List<ChatItem> chatItem = new List<ChatItem>();
chatItem.Add(new ChatItem() { text = "Hello...", isFromUser = false });
chatItem.Add(new ChatItem() { text = "hi!", isFromUser = true });
chatItem.Add(new ChatItem() { text = "this is a test, this is a test, this is a test, this is a test, this is a test, this is a test, this is a test, this is a test, this is a test, this is a test", isFromUser = false });
ChatListBox.ItemsSource = chatItem;
This is how the ListBox is redered:
Is there any way of adding an IF statement at the ListBox with WPF? or how can i control which ListBox.ItemTemplate to add.

If your messages are of the same class you could use itemsTemplateSelector as in http://codingbandit.com/blog/?p=8
if there are different classes you should just use the datatemplate datatype property as in Conditional List itemtemplate or datatemplate in WPF

Another option is to simply use a data trigger:
<DataTemplate x:Key="ToTemplate">
... etc ...
</DataTemplate>
<DataTemplate x:Key="FromTemplate">
... etc ...
</DataTemplate>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<DataTrigger Binding="{Binding isFromUser}" Value="false">
<Setter Property="Template" Value="{StaticResource ToTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding isFromUser}" Value="true">
<Setter Property="Template" Value="{StaticResource FromTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>

Maybe this will give you an idea
Xaml
<Window x:Class="Q1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Q1"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="intDataTemplate">
<TextBox Text="{Binding Path=.}" Width="80"/>
</DataTemplate>
<DataTemplate x:Key="stringDataTemplate">
<TextBlock Text="{Binding Path=.}"/>
</DataTemplate>
<local:MyDataTemplateSelector IntDataTemplate="{StaticResource intDataTemplate}"
StringDataTemplate="{StaticResource stringDataTemplate}"
x:Key="myDataTemplateSelector"/>
</Window.Resources>
<Grid>
<ListBox x:Name="myListBox" ItemsSource="{Binding}"
ItemTemplateSelector="{StaticResource myDataTemplateSelector}">
</ListBox>
</Grid>
Code-Behind
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 Q1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<System.Object> myList = new List<object>();
myList.Add(1);
myList.Add("Alpha");
myList.Add(2);
myList.Add("Beta");
myList.Add(3);
myList.Add("Gamma");
myListBox.DataContext = myList;
}
}
}
DataTemplateSelector
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Q1
{
public class MyDataTemplateSelector : System.Windows.Controls.DataTemplateSelector
{
public System.Windows.DataTemplate IntDataTemplate { get; set; }
public System.Windows.DataTemplate StringDataTemplate { get; set; }
public MyDataTemplateSelector()
{
IntDataTemplate = new System.Windows.DataTemplate();
StringDataTemplate = new System.Windows.DataTemplate();
}
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
if (item is Int32)
{
return IntDataTemplate;
}
else
{
return StringDataTemplate;
}
}
}
}

You could use DataTemplateSelector to select the Template according with your data, here's an example.
For this example, I'll use messages, a client message and a server message:
public abstract class Message
{
public string Content { get; set; }
public override string ToString()
{
return Content;
}
}
public class ServerMessage : Message
{
}
public class ClientMessage : Message
{
}
In this way, I can check the type of the object and apply certain template.
Let's define our templates and selector:
XAML:
<DataTemplate x:Key="clientTemplate" >
<TextBlock Text="{Binding Content}"
Foreground="Red"/>
</DataTemplate>
<DataTemplate x:Key="serverClient">
<TextBlock Text="{Binding Content}"
Foreground="Green"/>
</DataTemplate>
<local:MessageTemplateSelector x:Key="messageSelector"
ServerTemplate="{StaticResource serverClient}"
ClientTemplate="{StaticResource clientTemplate}"/>
DataTemplateSelector
public class MessageTemplateSelector : DataTemplateSelector
{
public DataTemplate ClientTemplate { get; set; }
public DataTemplate ServerTemplate { get; set; }
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
if (item.GetType() == typeof(ClientMessage))
return ClientTemplate;
return ServerTemplate;
}
}
First of all, as you can see I create two DataTemplate properties on my Selector, which I set it by XAML, doing this is easy to retrieve the DataTemplate, and finally I just compare the item received by parameter, which is the binded object(Message), and check its type and return the template.
And that is it, it works like expected.
UPDATE: Let's supposed that you want Template your items based on their types, just like my example, you could this the same thing above without using TemplateSelector, you could define the DataType of your Template:
<Window.Resources>
<DataTemplate DataType="{x:Type local:ClientMessage}">
<TextBlock Text="{Binding Content}"
Foreground="Red"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ServerMessage}">
<TextBlock Text="{Binding Content}"
Foreground="Green"/>
</DataTemplate>
</Window.Resources>
Doing so, the Template will be selected automatically according the type of the object.

Related

How to show an ObservableCollection<Class> property

My code has a RightListBox and a CheckBox (under another ListBox), like this:
I'd like to show the RightListBox items in the CheckBox (i.e., T1, T2, T3), but it actually shows the ViewModel name. I tried in many ways (please see my XAML), but none of them shows the RightListBox items.
Here is the code:
MainWindow.xaml.cs
using ListBoxMoveAll.Model;
using ListBoxMoveAll.ViewModel;
using System.Collections.ObjectModel;
using System.Windows;
namespace ListBoxMoveAll
{
public partial class MainWindow : Window
{
public ObservableCollection<GraphViewModel> RightListBoxItems { get; }
= new ObservableCollection<GraphViewModel>();
public MainWindow()
{
InitializeComponent();
DataContext = this;
RightListBoxItems.Add(new GraphViewModel("T1", new Graph(10)));
RightListBoxItems.Add(new GraphViewModel("T2", new Graph(20)));
RightListBoxItems.Add(new GraphViewModel("T3", new Graph(30)));
}
}
}
MainWindow.xaml
<Window x:Class="ListBoxMoveAll.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:ListBoxMoveAll"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="600">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ListBox x:Name="RightListBox" Grid.Row="0" Grid.RowSpan="3" Grid.Column="2"
ItemsSource="{Binding RightListBoxItems}" DisplayMemberPath="Text"
SelectionMode="Extended" Margin="0,10"/>
<StackPanel Grid.Column="4" Grid.Row="0" Grid.RowSpan="3" Margin="0,10">
<!--<ListBox ItemsSource="{Binding Items, ElementName=RightListBox}">-->
<!--<ListBox ItemsSource="{Binding Items, ElementName=RightListBox}" DisplayMemberPath="Text">-->
<!--<ListBox ItemsSource="{Binding SelectedItems, ElementName=RightListBox}">-->
<ListBox ItemsSource="{Binding SelectedItems, ElementName=RightListBox}" DisplayMemberPath="Text">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<CheckBox Content="{TemplateBinding Content}" x:Name="checkBox"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</StackPanel>
</Grid>
</Window>
Graph.cs
namespace ListBoxMoveAll.Model
{
public class Graph
{
public Graph(int step) { Step = step; }
public int Step { get; set; }
}
}
GraphViewModel.cs
using ListBoxMoveAll.Model;
namespace ListBoxMoveAll.ViewModel
{
public class GraphViewModel
{
public string Text { get; }
public Graph Graph { get; }
public GraphViewModel(string text, Graph graph) => (Text, Graph) = (text, graph);
}
}
This looks so simple, but I still can't find a solution. So, please help me. Thank you.
Instead of ListBox.ItemContainerStyle you can use ItemTemplate. And then use binding to your Text property (not the Content) in GraphViewModel. Something like that
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Text}" x:Name="checkBox"/>
</DataTemplate>
</ListBox.ItemTemplate>
Another option is to use the same approach with your different list box
<ListBox ItemsSource="{Binding SelectedItems, ElementName=RightListBox}" DisplayMemberPath="Text"/>
You also should bind IsChecked property of your checkbox to a ViewModel property, there is no such property right now
For Content you need to specify which property you want to display for the checkbox.
<CheckBox Content="{Binding Path=Content.Text,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}" x:Name="checkBox"/>

(WPF) How to change ListBox items/headers font size at runtime?

I want to allow user to change the font size a listbox during runtime. What's the simplest way to achieve that? (also, is there a way to simplify the code in general?)
Here's a xaml for the list I have: https://pastebin.com/Y8q5W50S
<ListBox Grid.Row="1" Grid.Column="1" Name="CardsListBox" ItemsSource="{Binding Path=placement}"
Background="Transparent" BorderBrush="Transparent">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander IsExpanded="True" Style="{StaticResource ExpanderStyle}">
<Expander.Header>
<TextBlock Text="{Binding Name}" Style="{StaticResource PlacementHeaderStyle}" />
</Expander.Header>
<ItemsPresenter IsEnabled="False" />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Path=power}" Style="{StaticResource CardPowerStyle}" />
<TextBlock Grid.Column="2" Text="{Binding Path=name}" Style="{StaticResource CardNameStyle}" />
</Grid>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
So here is an example: Size of the font inside the item template of the ListBox is binded from ViewModel. The same value is also binded to slider so you can check how it works and change it on runtime.
First, you will have to create the view model where you will have to keep the value of font size. The view model will have to implement INotify property changed.
public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel()
{
Items = new ObservableCollection<string> {"item1", "item2"};
_fontSize = 12d;
}
public ObservableCollection<string> Items { get; set; }
private double _fontSize;
public double FontSize
{
get => _fontSize;
set
{
_fontSize = value;
OnPropertyChanged(nameof(FontSize));
}
}
#region INotifyPropertyChangedImplementation
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
Next step is to set view model as view data context for example like this ( Please, checkout for example, Caliburn Micro or Prism to make this automatically).
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
DataContext = new MainWindowViewModel();
InitializeComponent();
}
}
After that bind the font size to the list using a relative source binding from ListBox item template to window DataContext ( MainWindowViewModel).
Ex:
<Window x:Class="ListBindingWpfApp.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:ListBindingWpfApp"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel>
<TextBlock Text="Font size slider:"></TextBlock>
<Slider Maximum="30" Minimum="5" Value="{Binding FontSize}"/>
</StackPanel>
<ListBox Grid.Row="1" Name="CardsListBox" ItemsSource="{Binding Path=Items}"
Background="Transparent" BorderBrush="Transparent">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontSize="{Binding DataContext.FontSize, RelativeSource={RelativeSource AncestorType=Window}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
I have attached working project.
Sorry for the not detailed description I will update it later.
Sample Project

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>

Best way to stretch WPF ListBox.ItemTemplate's contents to the width of ListBoxItem

I know this may be duplicate, but I haven't found a best solution. I simply come across a issue of using ListBox.ItemTemplate, I want the content Grid to HorizontalAlignment="Stretch"(not work). So I tried to bind the Grid' Width to the ListBoxItem, but the last item behaves strangely. If bind to the Width of ListBox, there will be a scrollbar, although a converter may solve it, I think there must be some more simple and elegant solution.
Codebehind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new List<Data>()
{
new Data("a1","a2"),
new Data("b1","b2"),
new Data("c1","c2")
};
}
public class Data
{
public Data(string s1, string s2)
{
this.S1 = s1;
this.S2 = s2;
}
public string S1 { get; set; }
public string S2 { get; set; }
}
}
Xaml:
<Grid>
<ListBox ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="Blue"
Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}},
Path=ActualWidth, Mode=OneWay}"
>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding S1, Mode=OneWay}"
FontSize="20" Grid.Column="0"/>
<TextBlock Text="{Binding S2, Mode=OneWay}"
FontSize="20" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
Try to set ListBoxItem's HorizontalContentAlignment to Strecth, for example :
<ListBox ItemsSource="{Binding}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>

Updating a ListBox with different Content On Button Clicks in WPF

So I have a listbox and a tool bar in my WPF app. The tool bar just has regular controls, and the listbox has vertical expanders.
I need the listbox to have a different set of expanders depending on what button is clicked. Right now it looks like such:
<ListBox>
<local:Select_Analysis_Panel/>
</ListBox>
Where local:Select_Analysis_Panel is seperate user control file containing the expanders. What is the best way to go about dynamically updating the ListBox control's content upon a button click?
For the last couple hours I've been trying to use set DataTemplates for each expander set and bind the to the items control property with little avail with the code below. I'm just trying to get basic framework laid out before setting up a MVVM interface. Later on I was going to replace the ItemsSource="Network_anal" with you know ItemsSource="{Binding WhatExpanderViewModelProperty}" or something like that.
<ListBox Width="250" Margin="5,0,0,0">
<ListBox.Resources>
<DataTemplate DataType="Select_Analysis_Panel">
<local:Select_Analysis_Panel/>
</DataTemplate>
<DataTemplate x:Key="Network_anal" DataType="NetworkAnalysis">
<local:NetworkAnalysis/>
</DataTemplate>.Resources>
<ListBox.Template>
<ControlTemplate>
<Border Background="Red"/>
</ControlTemplate>
</ListBox.Template>
<ItemsControl ItemsSource="Network_anal"/>
</ListBox>
Am I taking the right approach to this at all?
Here's what I'm trying to do. Below when the "File" button is clicked the side bar displays these 2 expanders:
And when "Network Design" button these expanders are dipslayed:
Option 1:
Subclassing the sections:
each of these sections could be subclassed from a base section class and a specific DataTemplate could be used for each:
<Window x:Class="MiscSamples.MultiToolbar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MiscSamples"
Title="MultiToolbar" Height="300" Width="300">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</Window.Resources>
<DockPanel>
<ListBox ItemsSource="{Binding Sections}"
SelectedItem="{Binding SelectedSection}"
DisplayMemberPath="Name"
DockPanel.Dock="Top">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="Visibility" Value="{Binding IsVisible, Converter={StaticResource BoolToVisibilityConverter}}"/>
<Setter Property="MinWidth" Value="80"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border BorderBrush="Black" BorderThickness="1">
<ToggleButton IsChecked="{Binding IsSelected, Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}">
<ContentPresenter ContentSource="Content"/>
</ToggleButton>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
<ScrollViewer Width="300" DockPanel.Dock="Left">
<ContentPresenter Content="{Binding SelectedSection}">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type local:FileSection}">
<TextBlock Text="User Control For File Section"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:NetworkDesignSection}">
<TextBlock Text="User Control For Network Design"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:SelectAnalysisSection}">
<TextBlock Text="User Control For Select Analysis"/>
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
</ScrollViewer>
<Grid Background="Gray">
<TextBlock Text="Design Surface" TextAlignment="Center" VerticalAlignment="Center" FontWeight="Bold"/>
</Grid>
</DockPanel>
</Window>
Code Behind:
public partial class MultiToolbar : Window
{
public MultiToolbar()
{
InitializeComponent();
var vm = new MainViewModel();
vm.Sections.Add(new FileSection() {Name = "File"});
vm.Sections.Add(new NetworkDesignSection() { Name = "Network Design" });
vm.Sections.Add(new SelectAnalysisSection() { Name = "Select Analysis" });
DataContext = vm;
}
}
Main ViewModel:
public class MainViewModel: PropertyChangedBase
{
private ObservableCollection<Section> _sections;
public ObservableCollection<Section> Sections
{
get { return _sections ?? (_sections = new ObservableCollection<Section>()); }
}
private Section _selectedSection;
public Section SelectedSection
{
get { return _selectedSection; }
set
{
_selectedSection = value;
OnPropertyChanged("SelectedSection");
}
}
}
Sections:
public abstract class Section:PropertyChangedBase
{
public string Name { get; set; }
private bool _isEnabled = true;
public bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
OnPropertyChanged("IsEnabled");
}
}
private bool _isVisible = true;
public bool IsVisible
{
get { return _isVisible; }
set
{
_isVisible = value;
OnPropertyChanged("IsVisible");
}
}
//Optionally
//public string ImageSource {get;set;}
//ImageSource = "/Resources/MySection.png";
}
public class FileSection: Section
{
///... Custom logic specific to this Section
}
public class NetworkDesignSection:Section
{
///... Custom logic specific to this Section
}
public class SelectAnalysisSection: Section
{
///... Custom logic specific to File Section
}
//...etc etc etc
Result:
Notice that I'm using ToggleButtons bound to the ListBoxItem.IsSelected property to simulate a TabControl-like behavior.
You can set the DataContext of the whole form and bind the ItemsSource of the listbox, or set ItemsSource of the listbox to some collection directly.

Categories

Resources