Set ListBox SelectedItems based on another collection - c#

I have a ListBox that is bound to MyCollection of type IEnumerable<string>.
<ListBox x:Name="ListBox" ItemsSource="{Binding MyCollection, Mode=OneWay}" SelectionMode="Multiple"/>
I have another List<string> SubCollection that contains a subset of MyCollection
Whenever SubCollection changes, I'd like the SelectedItems to be highlighted as per the SubCollection
Is there any Binding, Behaviour or any other way to accomplish this?
Edit:
Let's say I have a ListBox bound to MyCollection { "Orange", "Mango", "Stawberry", "Pineapple" }
Let's assume that I press a button to load data from a database and the result is "Orange", "Mango" which is then placed into to SubCollection. ListBox should now have "Orange", "Mango" as its SelectedItems.

I have a suggestion for you.
You can bind to the "IsSelected" property of the ListBoxItem.
To do that you have to use a collection of objects(let's say MyListBoxItem) instead of collection of strings.
public class MyListBoxItem
{
public string Description { get; set; }
public bool IsSelected { get; set; }
}
Use this "IsSelected" property of the MyListBoxItem class to bind the "IsSelected" property of the ListBoxItem.
Ex: In you view model,
this.MyCollection = new ObservableCollection<MyListBoxItem>();
MyListBoxItem item1 = new MyListBoxItem()
item1.Description = "Mango";
item1.IsSelected = true;
MyCollection .add(item1);
MyListBoxItem item2 = new MyListBoxItem()
item2 .Description = "Orange";
item2 .IsSelected = false;
MyCollection .add(item2 );
XAML (Inside the ListBox)
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>

You could create a List AttachedProperty for the ListBox that adds items to the ListBox.SelectedItems when the list is changed.
The AttachedProperty solution keeps WPF clean and your MVVM pattern, Also it makes this feature reusable thoughout all your projects :)
Here is an example:
AttachedProperty:
public static class ListBoxExtensions
{
// Using a DependencyProperty as the backing store for SearchValue. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedItemListProperty =
DependencyProperty.RegisterAttached("SelectedItemList", typeof(IList), typeof(ListBoxExtensions),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnSelectedItemListChanged)));
public static IList GetSelectedItemList(DependencyObject obj)
{
return (IList)obj.GetValue(SelectedItemListProperty);
}
public static void SetSelectedItemList(DependencyObject obj, IList value)
{
obj.SetValue(SelectedItemListProperty, value);
}
private static void OnSelectedItemListChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var listbox = d as ListBox;
if (listbox != null)
{
listbox.SelectedItems.Clear();
var selectedItems = e.NewValue as IList;
if (selectedItems != null)
{
foreach (var item in selectedItems)
{
listbox.SelectedItems.Add(item);
}
}
}
}
}
Xaml Usage:
<ListBox ItemsSource="{Binding Items}" SelectionMode="Multiple"
local:ListBoxExtensions.SelectedItemList="{Binding SelectedItems}" />
Demo:
Working example if you want to test:
Xaml:
<Window x:Class="WpfApplication17.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication17"
Title="MainWindow" Height="227" Width="170" Name="UI">
<Grid DataContext="{Binding ElementName=UI}">
<ListBox ItemsSource="{Binding Items}" SelectionMode="Multiple"
local:ListBoxExtensions.SelectedItemList="{Binding SelectedItems}" Margin="0,0,0,37" >
<ListBox.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="{x:Static SystemColors.HighlightColor}" />
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.Resources>
</ListBox>
<Button Content="Populate SelectedItemList" Click="Button_Click" Height="32" Margin="2,0,1,2" VerticalAlignment="Bottom"/>
</Grid>
</Window>
Code:
namespace WpfApplication17
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private List<string> _selectedItems = new List<string>();
private ObservableCollection<string> _items = new ObservableCollection<string>
{ "Orange", "Mango", "Stawberry", "Pineapple", "Apple", "Grape", "Banana" };
public MainWindow()
{
InitializeComponent();
}
public ObservableCollection<string> Items
{
get { return _items; }
set { _items = value; }
}
public List<string> SelectedItems
{
get { return _selectedItems; }
set { _selectedItems = value; OnPropertyChanged("SelectedItems"); }
}
private void Button_Click(object sender, RoutedEventArgs e)
{
SelectedItems = new List<string> { "Orange", "Pineapple", "Apple" };
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string e)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(e));
}
}
public static class ListBoxExtensions
{
// Using a DependencyProperty as the backing store for SearchValue. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedItemListProperty =
DependencyProperty.RegisterAttached("SelectedItemList", typeof(IList), typeof(ListBoxExtensions),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnSelectedItemListChanged)));
public static IList GetSelectedItemList(DependencyObject obj)
{
return (IList)obj.GetValue(SelectedItemListProperty);
}
public static void SetSelectedItemList(DependencyObject obj, IList value)
{
obj.SetValue(SelectedItemListProperty, value);
}
private static void OnSelectedItemListChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var listbox = d as ListBox;
if (listbox != null)
{
listbox.SelectedItems.Clear();
var selectedItems = e.NewValue as IList;
if (selectedItems != null)
{
foreach (var item in selectedItems)
{
listbox.SelectedItems.Add(item);
}
}
}
}
}
}
Result:

The standard data binding doesn’t work, because the SelectedItems property is read-only.
One simple approach, is to manually iterate the MyCollection and Set the IsSelected Property of each item, based on the items in SubCollection.
For this, the items in MyCollection list should contain objects inherited from ListBoxItem which would expose the IsSelected Property.
Here is a sample WPF app to demostrate it :
The button click would update the selected items based on items in SubCollection list. For now I have hardcode the values of SubCollection list.
Based on you implementation you could update your SubCollection, and hook the code inside button click event to any other events appropriately.(like make SubCollection list as ObservableCollection and hook to ObservableCollection.CollectionChange)
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"
Title="MainWindow" Height="250" Width="255">
<Grid>
<StackPanel Orientation="Vertical">
<ListBox x:Name="ListBox" ItemsSource="{Binding MyCollection}" SelectionMode="Extended" Margin="10,10"/>
<Button Content="UpdateSelection" Click="Button_Click" Margin="10,10"/>
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
_myCollection = new ObservableCollection<MyListBoxItem>() { new MyListBoxItem("Orange"), new MyListBoxItem("Mango"), new MyListBoxItem("Stawberry"), new MyListBoxItem("Pineapple") };
//Items to be selected on this.MyCollection ListBox
this.SubCollection = new List<string>() { "Pineapple", "Mango" };
this.DataContext = this;
}
private ObservableCollection<MyListBoxItem> _myCollection;
public ObservableCollection<MyListBoxItem> MyCollection
{
get { return _myCollection; }
set
{
this._myCollection = value;
}
}
public IList<string> SubCollection { get; set; }
private void Button_Click(object sender, RoutedEventArgs e)
{
// Clear all the selected items in the ListBox
this.MyCollection.ToList().ForEach(item => item.IsSelected = false);
// SELECT only the items in MySubCollection into this.MyCollection ListBox
this.MyCollection.Where(item => this.SubCollection.Contains(item.Content.ToString())).ToList().ForEach(item => item.IsSelected= true);
}
}
public class MyListBoxItem : ListBoxItem
{
public MyListBoxItem(string displayName)
{
this.Content = displayName;
}
}
}

Check this blogpost: how-to-databind-to-selecteditems. There is a demo project with code attached.

Use "SelectedValue" property of the ListBox.
(Because you are using collection of string to bind the List Box.)
Define a string type property in your view model (let's say MySelectedValue).
Then Bind it to the "SelectedValue" property of the both List Boxes. (Remeber to set the Mode = TwoWay in your Main ListBox)
Let's say a method will execute when you select an item in Sub collection List Box. (You have to trigger the SelectionChanged event of the Sub Collection ListBox)
That's it.
So. You have to do is:
Use same property to bind the "SelectedValue" property of the BOTH
List Boxes.
Let's say if you wanna bind a collection of objects to the List Box.
Then use SelectedItem property to achieve this task.

in case you know the index number you want to select, you can use
int[] index = {1, 2, 5};
for( int i in index)
{
listBox.SetSelected( i, true );
}

Related

How to invoke an event by clicking on a ListBoxItem I added with code

XAML:
<ListBox x:Name="otherMedia" />
C#:
private void functionName(String path){
String[] files = System.IO.Directory.GetFiles(path);
List<ListBoxItem> listBoxItems = new List<ListBoxItem>();
foreach (var item in files)
{
string fileName = System.IO.Path.GetFileNameWithoutExtension(item);
ListBoxItem listBoxItem = new ListBoxItem()
{
AllowDrop =false,
MinWidth = 10,
};
listBoxItem.Content = fileName;
listBoxItems.Add(listBoxItem);
}
otherMedia.ItemsSource = listBoxItems;
}
How can I invoke an event on one of these ListBoxItem elements by clicking on them?
Don't do things that way. Use dependency properties, data binding and styles instead:
<Window x:Class="CSharpTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<ListBox ItemsSource="{Binding FileNames}" SelectedItem="{Binding SelectedFileName}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="AllowDrop" Value="False"/>
<Setter Property="MinWidth" Value="10"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Window>
public partial class MainWindow : Window
{
private void functionName(string path)
{
List<string> fileNames = new List<string>();
foreach (string file in System.IO.Directory.GetFiles(path))
{
fileNames.Add(System.IO.Path.GetFileNameWithoutExtension(file));
}
FileNames = fileNames;
}
//Holds the list of file names which you want to display in your ListBox
public IList<string> FileNames
{
get { return (IList<string>)GetValue(FileNamesProperty); }
set { SetValue(FileNamesProperty, value); }
}
public static readonly DependencyProperty FileNamesProperty =
DependencyProperty.Register("FileNames", typeof(IList<string>), typeof(MainWindow), new PropertyMetadata(null));
//Holds the currently selected item from the ListBox
public string SelectedFileName
{
get { return (string)GetValue(SelectedFileNameProperty); }
set { SetValue(SelectedFileNameProperty, value); }
}
public static readonly DependencyProperty SelectedFileNameProperty =
DependencyProperty.Register("SelectedFileName", typeof(string), typeof(MainWindow), new PropertyMetadata(null, SelectedFileNameChanged));
//This static method runs whenever the selected item changes (because SelectedItem is bound to SelectedFileName)
private static void SelectedFileNameChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var window = (MainWindow)o;
//Do something here to handle the change of selection
}
}

Get all selected items across multiple nested ListBoxes with SelectionMode Multiple/Extended

I have a List of Lists and display it with nested ListBoxes:
MainWindow.xaml.cs
using System.Collections.Generic;
namespace WPF_Sandbox
{
public partial class MainWindow
{
public IEnumerable<IEnumerable<string>> ListOfStringLists { get; set; } = new[] { new[] { "a", "b" }, new[] { "c", "d" } };
public MainWindow()
{
InitializeComponent();
DoSomethingButton.Click += (sender, e) =>
{
// do something with all selected items
};
}
}
}
MainWindow.xaml
<Window x:Class="WPF_Sandbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
x:Name="ThisControl">
<StackPanel>
<ListBox ItemsSource="{Binding ListOfStringLists, ElementName=ThisControl}">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<TextBlock Text="{Binding}" />
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Name="DoSomethingButton" Content="DoSomething" />
</StackPanel>
</Window>
How can I get all selected items across all ListBoxes?
I found a few solutions getting one selected item but could not figure out how to do applie those in my scenario.
I have an idea on how to do this by wrapping the string arrays but I would prefer not doing this.
I would just add an event handler to the inner ListBox like so if not doing things the MVVM way:
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple" SelectionChanged="ListBox_SelectionChanged">
Then in your code behind implement the ListBox_SelectionChanged like so:
public List<string> FlatStringList = new List<string>();
private void ListBox_SelectionChanged(object sender,System.Windows.Controls.SelectionChangedEventArgs e)
{
FlatStringList.AddRange(e.AddedItems.Cast<string>());
foreach(string s in e.RemovedItems)
{
FlatStringList.Remove(s);
}
}
This is assuming you don't mind storing the selected strings in a flat list. Then you could implement your DoSomething button click event handler to do something with the FlatStringList.
Hope that helps.
The easiest way would be to iterate through the items in the ListBoxes:
private void DoSomethingButton_Click(object sender, RoutedEventArgs e)
{
List<string> selectedStrings = new List<string>();
foreach (IEnumerable<string> array in outerListBox.Items.OfType<IEnumerable<string>>())
{
ListBoxItem lbi = outerListBox.ItemContainerGenerator.ContainerFromItem(array) as ListBoxItem;
if (lbi != null)
{
ListBox innerListBox = GetChildOfType<ListBox>(lbi);
if (innerListBox != null)
{
foreach (string selectedString in innerListBox.SelectedItems.OfType<string>())
selectedStrings.Add(selectedString);
}
}
}
}
private static T GetChildOfType<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj == null)
return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null)
return result;
}
return null;
}
Note that the ListBoxItem may be virtualized away if you have a lot of inner IEnumerable<string>. You will then have to force the generation of the containers or disable UI virtualization:
WPF ListView virtualization. How to disable ListView virtualization?
This may affect the performance negatively so if this is an issue you should probably consider binding to an IEnumerable<YourType> and bind the SelectedItems property of the inner ListBox to a property of a YourType using a behaviour.
Since the SelectedItems property of a ListBox is read-only you can't bind to it directly: https://blog.magnusmontin.net/2014/01/30/wpf-using-behaviours-to-bind-to-readonly-properties-in-mvvm/.
Why don't you create a wrapper (as you said):
public class MyString : INotifyPropertyChanged
{
public MyString(string value) { Value = value; }
string _value;
public string Value { get { return _value; } set { _value = value; RaisePropertyChanged("Value"); } }
bool _isSelected;
public bool IsSelected { get { return _isSelected; } set { _isSelected = value; RaisePropertyChanged("IsSelected"); } }
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
Bind the IsSelected property of the ListBoxItems:
<StackPanel>
<ListBox ItemsSource="{Binding ListOfStringLists, ElementName=ThisControl}">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<TextBlock Text="{Binding Value}" />
</ItemContainerTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Name="DoSomethingButton" Content="DoSomething" />
</StackPanel>
and you are already done:
public IEnumerable<IEnumerable<MyString>> ListOfStringLists { get; set; } = new[] { new[] { new MyString("a"), new MyString("b") { IsSelected = true } }, new[] { new MyString("c"), new MyString("d") } };
public MainWindow()
{
this.InitializeComponent();
DoSomethingButton.Click += (sender, e) =>
{
foreach (var i in ListOfStringLists)
foreach (var j in i)
{
if (j.IsSelected)
{
// ....
}
}
};
}

How to set SeletedItems = null in a Listbox if i select a other Listbox

basically i'm trying to do THIS
but you can see it is not MVVM so i'm looking for a way to set SeletedItems = null or clear() depending on what's doable
because in my View i will got N ListBoxes and if he pressed a Button after selecting some Items i will change some properties of the SeletedItems but only for the last active Listbox
so i decided to use on SelectedItems Property for all the Listboxes but it doesn't work based on 2 problems i can't bind to to SelectedItems and based on this i can't test how to remove the selection from the other Listboxes
EDIT:
to give you an simple example:
XAML
<Window x:Class="Test.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">
<StackPanel>
<ListBox Width="432" Height="67"
HorizontalAlignment="Left" VerticalAlignment="Top"
SelectionMode="Extended"
<!-- SeletedItems="{Binding SelectedListItems}" ??? -->
ItemsSource="{Binding Collection1}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding MyText}"
Background="{Binding MyBackground}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ListBox Width="432" Height="67"
HorizontalAlignment="Left" VerticalAlignment="Top"
SelectionMode="Extended"
<!-- SeletedItems="{Binding SelectedListItems}" ??? -->
ItemsSource="{Binding Collection2}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding MyText}"
Background="{Binding MyBackground}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="unselect" Width="80" Height="150"
HorizontalAlignment="Right" VerticalAlignment="Top"
Command="{Binding MyCommand}"/>
</StackPanel>
</Window>
Code
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
namespace Test
{
/// <summary>
/// Interaktionslogik für MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new VM();
}
}
public class VM : INotifyPropertyChanged
{
private ObservableCollection<DetailVM> _SelectedListItems = new ObservableCollection<DetailVM>();
public ObservableCollection<DetailVM> SelectedListItems
{
get { return _SelectedListItems; }
set
{
_SelectedListItems = value;
OnPropertyChanged("SelectedListItems");
}
}
public List<DetailVM> Collection1 { get; set; }
public List<DetailVM> Collection2 { get; set; }
private RelayCommand _myCommand;
public ICommand MyCommand
{
get { return _myCommand?? (_myCommand= new RelayCommand(param => OnMyCommand())); }
}
public void OnMyCommand()
{
foreach DetailVM item in SelectedListItems
{
item.MyBackground ="Red";
}
}
public VM()
{
Collection1 = new List<DetailVM>();
Collection2 = new List<DetailVM>();
for (int i = 0; i < 10; i++)
{
Collection1.Add(new DetailVM { MyText = "C1ITEM " + i });
Collection2.Add(new DetailVM { MyText = "C2ITEM " + i });
}
}
#region INotifyPropertyChanged Member
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
public class DetailVM
{
public string MyText { get; set; }
public string MyBackground { get; set; }
}
}
The code above should change the color of the Textbox background to Red
if the user selected some Items in a Listbox and he should only be able to seleted Items in one Listbox at the same time
so how to do this? (bear in mind this is a simple example but i need this for N Listboxes which will be generated over a template)
First of all, I would recommend you to extend ListView so that it includes a bindable SelectedValues property (you cannot use the name SelectedItems since it's already a non-bindable property of ListView). Here's an example of how this can be achieved.
public class MultiSelectListView : ListView
{
// Using a DependencyProperty as backing store
public static readonly DependencyProperty SelectedValuesProperty =
DependencyProperty.Register("SelectedValues", typeof(IList), typeof(MultiSelectListView), new PropertyMetadata(default(IList), OnSelectedItemsChanged));
public IList SelectedValues
{
get { return (IList)GetValue(SelectedValuesProperty); }
set { SetValue(SelectedValuesProperty, value); }
}
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// if selected items list implements INotifyCollectionChanged, we subscribe to its CollectionChanged event
var element = (MultiSelectListView)d;
if (e.OldValue != null && e.OldValue is INotifyCollectionChanged)
{
var list = e.OldValue as INotifyCollectionChanged;
list.CollectionChanged -= element.OnCollectionChanged;
}
if (e.NewValue is INotifyCollectionChanged)
{
var list = e.NewValue as INotifyCollectionChanged;
list.CollectionChanged += element.OnCollectionChanged;
}
}
// when selection changes in the view, elements are added or removed from the underlying list
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
if (SelectedValues != null)
{
foreach (var item in e.AddedItems)
{
if (!SelectedValues.Contains(item))
SelectedValues.Add(item);
}
foreach (var item in e.RemovedItems)
{
if (SelectedValues.Contains(item))
SelectedValues.Remove(item);
}
}
base.OnSelectionChanged(e);
}
// when underlying list changes, we set the control's selected items to the contents of the list
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (SelectedValues != null)
{
SetSelectedItems(SelectedValues);
}
}
}
Once you've done this you can control the behavior of a list's selected items through the viewmodel. Clearing the viewmodel list clears the selected items in the control.
Next you can subscribe to the collection changed event of your selected items lists (in the view model) and in the handler check whether you need to clear any of your lists.

Binding a collection to a custom control property

I am having no luck trying to bind a collection of data to my custom control its property. I already have implemented the mechanism for a string property of this control (with a small help from here) and expected the collection type to be as easy. However I cannot make it work again.
Here is my custom control view
<UserControl x:Class="BadaniaOperacyjne.Controls.Matrix"
mc:Ignorable="d" Name="CustomMatrix"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<!-- ... -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<!-- ... -->
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ElementName=CustomMatrix, Path=Title}"/>
<Grid Grid.Row="2" Grid.Column="1" Name="contentGrid">
<ListBox ItemsSource="{Binding ElementName=CustomMatrix, Path=ItemsList}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</UserControl>
and its code-behind
#region ItemsList Property
public static readonly DependencyProperty ItemsListProperty =
DependencyProperty.Register("ItemsList", typeof(ObservableCollection<object>), typeof(Matrix), new PropertyMetadata(new ObservableCollection<object>(), new PropertyChangedCallback(ItemsListChanged)));
public ObservableCollection<object> ItemsList
{
get { return GetValue(ItemsListProperty) as ObservableCollection<object>; }
set { SetValue(ItemsListProperty, value); }
}
private void ItemsListChanged(object value)
{
System.Diagnostics.Debug.WriteLine("matrix: items list changed " + value);
if (ItemsList != null)
{
ItemsList.CollectionChanged += ItemsList_CollectionChanged;
System.Diagnostics.Debug.WriteLine("got " + string.Join(",", ItemsList.ToList()));
}
else
{
System.Diagnostics.Debug.WriteLine("got null");
}
}
void ItemsList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine("matrix: current items list collection changed");
}
private static void ItemsListChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// a breakpoint
((Matrix)d).ItemsListChanged(e.NewValue);
}
#endregion
// showing the Title property implementation just to state that
// it is done the same way as for ItemsList
#region Title Property
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(Matrix), new PropertyMetadata("", new PropertyChangedCallback(TitleChanged)));
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
private void TitleChanged(string title)
{
System.Diagnostics.Debug.WriteLine("matrix: title changed to: " + title);
}
private static void TitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((Matrix)d).TitleChanged((string)e.NewValue);
}
#endregion
And here's how I am trying to bind to that control
<custom:Matrix x:Name="customMatrix" DockPanel.Dock="Top" Title="{Binding Title}" ItemsList="{Binding Items}"/>
and the code-behind for the main page is
//internal ObservableCollection<List<int>> ItemsList { get; set; }
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ViewModel()
{
Items = new ObservableCollection<int> { 1, 2, 3, 4, 5, 6};
}
void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine("problem manager: items list changed " + e.NewItems.Count);
}
public ObservableCollection<int> Items { get; set; }
protected string title;
public string Title
{
get { return title; }
set
{
if (title != value)
{
title = value;
NotifyPropertyChanged("Title");
}
}
}
protected void NotifyPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
public ViewModel VM { get; private set; }
// this is the window constructor
private ProblemManager()
{
VM = new ViewModel();
DataContext = VM;
InitializeComponent();
VM.Title = "title";
}
private int i = 0;
private void btnAddRow_Click(object sender, RoutedEventArgs e)
{
// when doing either of these two lines below,
// the control breakpoint is never hit
VM.Items.Add(++i);
VM.Items = new ObservableCollection<int> { 2, 3 };
// however, when directly assigning to the control's property,
// the event is raised and the breakpoint is hit and the UI is updated
customMatrix.ItemsList = new ObservableCollection<object> { 1, 2, 3 };
customMatrix.ItemsList.Add(66);
// and this of course makes the control's title update
VM.Title = (++i).ToString();
}
Both DependencyPropertys for the control's Title and ItemsList are, I believe, created the same way. Nonetheless, the binding is probably not working as the ItemsListChanged event is not raised by that binding.
So, the problem is that I cannot bind my window's ViewModel.Items collection via XAML to the control's ItemsList collection. Is creating a DependencyProperty for a collection within a control any different from DependencyProperty for a simple string property?
Problem is in your DependencyProperty registration. Co-variance is not applicable for generic lists i.e. you cannot do this -
ObservableCollection<object> objects = new ObservableCollection<int>();
You have declared type of DP as ObservableCollection<object> but binding it with list of type ObservableCollection<int>.
You should change either type of DP to ObservableCollection<int> OR change binding collection type to ObservableCollection<object>.
public ViewModel()
{
Items = new ObservableCollection<object> { 1, 2, 3, 4, 5, 6};
}
public ObservableCollection<object> Items { get; set; }
After such a long time, I kind of needed this to work in both ways:
Be able to define a collection of any kind of items
Be informed when an item was added/removed and so on.
I am working on a bread crumb like control to be more precise.
Indeed, the ItemsSource DP is defined with an IList type, to allow the "genericity" aforementioned:
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IList), typeof(BreadCrumbUserControl),
new PropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
And here is how I use my BreadCrumb:
<views:BreadCrumbUserControl ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}"/>
where Items is an ObservableCollection of a custom type:
public ObservableCollection<BaseItem> Items { get; set; }
Indeed it works fine, my ItemsSource local property points to the right collection but I need to be informed in my UserControl when an item is added to the ObservableCollection.
So, I've made use of the PropertyChangedCallback delegate on my ItemsSource DP.
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
BreadCrumbUserControl thisUserControl = (BreadCrumbUserControl)d;
(e.NewValue as INotifyCollectionChanged).CollectionChanged += thisUserControl.BreadCrumbUserControl_CollectionChanged;
}
This is the trick that worked for me, because my input reference is in fact the ObservableCollection defined in the upper layers.
Both goals are now achieved: work with collection of any custom types and in the same type, stay informed.
If a protocol is set, reflection can be used to change values on our custom type.
Here is just an example of iterating on my collection which has some unknown type in my UserControl's world:
foreach (var baseItem in ItemsSource)
{
baseItem.GetType().GetProperty("IsActive").SetValue(baseItem, false);
}

How can i know if a ListBoxItem is the last item inside a Wpf's ListBox?

How can i know if a ListBoxItem is the last item of the collection (in the ItemContainerStyle or in the ItemContainer's template) inside a Wpf's ListBox?
That question is because I need to know if an item is the last item to show it in other way. For example: suppose i want to show items separated by semi-colons but the last one: a;b;c
This is easy to do in html and ccs, using ccs selector. But, how can i do this in Wpf?
As it seems to be rather difficult to implement an "Index" attached property to ListBoxItem to do the job right, I believe the easier way to accomplish that would be in MVVM.
You can add the logic necessary (a "IsLast" property, etc) to the entity type of the list and let the ViewModel deal with this, updating it when the collection is modified or replaced.
EDIT
After some attempts, I managed to implement indexing of ListBoxItems (and consequently checking for last) using a mix of attached properties and inheriting ListBox. Check it out:
public class IndexedListBox : System.Windows.Controls.ListBox
{
public static int GetIndex(DependencyObject obj)
{
return (int)obj.GetValue(IndexProperty);
}
public static void SetIndex(DependencyObject obj, int value)
{
obj.SetValue(IndexProperty, value);
}
/// <summary>
/// Keeps track of the index of a ListBoxItem
/// </summary>
public static readonly DependencyProperty IndexProperty =
DependencyProperty.RegisterAttached("Index", typeof(int), typeof(IndexedListBox), new UIPropertyMetadata(0));
public static bool GetIsLast(DependencyObject obj)
{
return (bool)obj.GetValue(IsLastProperty);
}
public static void SetIsLast(DependencyObject obj, bool value)
{
obj.SetValue(IsLastProperty, value);
}
/// <summary>
/// Informs if a ListBoxItem is the last in the collection.
/// </summary>
public static readonly DependencyProperty IsLastProperty =
DependencyProperty.RegisterAttached("IsLast", typeof(bool), typeof(IndexedListBox), new UIPropertyMetadata(false));
protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
{
// We capture the ItemsSourceChanged to check if the new one is modifiable, so we can react to its changes.
var oldSource = oldValue as INotifyCollectionChanged;
if(oldSource != null)
oldSource.CollectionChanged -= ItemsSource_CollectionChanged;
var newSource = newValue as INotifyCollectionChanged;
if (newSource != null)
newSource.CollectionChanged += ItemsSource_CollectionChanged;
base.OnItemsSourceChanged(oldValue, newValue);
}
void ItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.ReindexItems();
}
protected override void PrepareContainerForItemOverride(System.Windows.DependencyObject element, object item)
{
// We set the index and other related properties when generating a ItemContainer
var index = this.Items.IndexOf(item);
SetIsLast(element, index == this.Items.Count - 1);
SetIndex(element, index);
base.PrepareContainerForItemOverride(element, item);
}
private void ReindexItems()
{
// If the collection is modified, it may be necessary to reindex all ListBoxItems.
foreach (var item in this.Items)
{
var itemContainer = this.ItemContainerGenerator.ContainerFromItem(item);
if (itemContainer == null) continue;
int index = this.Items.IndexOf(item);
SetIsLast(itemContainer, index == this.Items.Count - 1);
SetIndex(itemContainer, index);
}
}
}
To test it, we setup a simple ViewModel and an Item class:
public class ViewModel : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
private ObservableCollection<Item> items;
public ObservableCollection<Item> Items
{
get { return this.items; }
set
{
if (this.items != value)
{
this.items = value;
this.OnPropertyChanged("Items");
}
}
}
public ViewModel()
{
this.InitItems(20);
}
public void InitItems(int count)
{
this.Items = new ObservableCollection<Item>();
for (int i = 0; i < count; i++)
this.Items.Add(new Item() { MyProperty = "Element" + i });
}
}
public class Item
{
public string MyProperty { get; set; }
public override string ToString()
{
return this.MyProperty;
}
}
The view:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication3"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="WpfApplication3.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="DataTemplate">
<Border x:Name="border">
<StackPanel Orientation="Horizontal">
<TextBlock TextWrapping="Wrap" Text="{Binding (local:IndexedListBox.Index), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Margin="0,0,8,0"/>
<TextBlock TextWrapping="Wrap" Text="{Binding (local:IndexedListBox.IsLast), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Margin="0,0,8,0"/>
<ContentPresenter Content="{Binding}"/>
</StackPanel>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding (local:IndexedListBox.IsLast), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="True">
<Setter Property="Background" TargetName="border" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="0.949*"/>
</Grid.RowDefinitions>
<local:IndexedListBox ItemsSource="{Binding Items}" Grid.Row="1" ItemTemplate="{DynamicResource DataTemplate}"/>
<Button Content="Button" HorizontalAlignment="Left" Width="75" d:LayoutOverrides="Height" Margin="8" Click="Button_Click"/>
<Button Content="Button" HorizontalAlignment="Left" Width="75" Margin="110,8,0,8" Click="Button_Click_1" d:LayoutOverrides="Height"/>
<Button Content="Button" Margin="242,8,192,8" Click="Button_Click_2" d:LayoutOverrides="Height"/>
</Grid>
</Window>
In the view's code behind I put some logic to test the behavior of the solution when updating the collection:
public partial class MainWindow : Window
{
public ViewModel ViewModel { get { return this.DataContext as ViewModel; } }
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.ViewModel.Items.Insert( 5, new Item() { MyProperty= "NewElement" });
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
this.ViewModel.Items.RemoveAt(5);
}
private void Button_Click_2(object sender, RoutedEventArgs e)
{
this.ViewModel.InitItems(new Random().Next(10,30));
}
}
This solution can handle static lists and also ObservableCollections and adding, removing, inserting items to it. Hope you find it useful.
EDIT
Tested it with CollectionViews and it works just fine.
In the first test, I changed Sort/GroupDescriptions in the ListBox.Items. When one of them was changed, the ListBox recreates the containeirs, and then PrepareContainerForItemOverride hits. As it looks for the right index in the ListBox.Items itself, the order is updated correctly.
In the second I made the Items property in the ViewModel a ListCollectionView. In this case, when the descriptions were changed, the CollectionChanged was raised and the ListBox reacted as expected.

Categories