I have a Class named Layer2Info
public class Layer2Info
{
public ObservableCollection<totalAvailablePorts> availableClientPorts = new ObservableCollection<totalAvailablePorts>();
}
The totalAvailablePorts Class is
public class totalAvailablePorts : INotifyPropertyChanged
{
public int _portID;
public Boolean _isSelected;
public int portID
{
get { return _portID; }
set { _portID = value; NotifyPropertyChanged("portID"); }
}
public Boolean isSelected
{
get { return _isSelected; }
set { _isSelected = value; NotifyPropertyChanged("isSelected"); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public override string ToString()
{
return string.Format("{0}", portID);
}
}
The creation of the data in availableClientPorts is:
for (int i = 1; i <= 3; i++)
{
totalAvailablePorts newPort = new totalAvailablePorts();
newPort.portID = i;
newPort.isSelected = false;
layer2InfoConfig.availableClientPorts.Add(newPort);
}
Now, in my MainWindow I'm binding the ListBox to the Layer2Info.availableClientPorts like this:
clientPortsList.ItemsSource = layer2InfoConfig.availableClientPorts;
and last is my xaml:
<ListBox x:Name="clientPortsList"
SelectionMode="Extended"
DisplayMemberPath="{Binding Path=portID}"
SelectedValuePath="{Binding Path=isSelected}"
Height="50"/>
Now, i'm able to see all the ports (1-3) in the ListBox, but what I want to do is that on every line that I select in the ListBox, I want the isSelected value in the availableClientPorts to change to true, and I have no idea where to start.
Any suggestions?
First, SelectedValuePath isn't what you think. MSDN says it "Gets or sets the path that is used to get the SelectedValue from the SelectedItem." So when the user selects an item, the clientPortsList will take that property of its own SelectedItem, and return the value of that property from clientPortsList.SelectedValue. With multi-select, that's not a real useful concept for you, and anyhow it's unrelated to the question you're asking here.
What you want to do is, for each totalAvailablePorts instance, bind the isSelected property of that instance to the IsSelected property of the ListBoxItem that owns it. You could do that with an item template, but a Style is much simpler (and better, if you aren't interested in recreating or altering the default ListBoxItem visual behavior). That answer is already on StackOverflow:
<ListBox ItemsSource="..."
x:Name="clientPortsList"
SelectionMode="Extended"
DisplayMemberPath="{Binding Path=portID}" >
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<!-- binding totalAvailablePorts.isSelected to ListBoxItem.IsSelected -->
<Setter Property="IsSelected" Value="{Binding isSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
For the ListBoxItem instances, their DataContext will be their respective totalAvailablePorts instances, so isSelected (lower-case I) will be "in scope".
Related
So I have a ListBox with CheckBox-es that have the IsChecked property bound to the Item's property called IsSelected. That produces a weird behavior where if I click on the item itself it checks the checkbox (good) and sets the property on the item (good), but doesn't actually select the item in the list box, ie. the highlighting isn't there. I am guessing that the ListBox IsSelected property needs to be set as well for that right?
Now, I am trying to get the multi-select behavior to work so I changed the SelectionMode to Extended. Now, I can select only Items, not the checkboxes. What happens is that if I use SHIFT + click by pointing at the area next to the item, not the item itself, then it select multiple items, but clicking on the items themselves doesn't do the trick of multi-selection not does it check the checkboxes. What is going on in here?
I would like to be able to select multiple items by holding shift etc, and have that trigger the property on the Elevation item so I know which ones are checked. Any help is appreciated.
Here's my XAML:
<ListBox x:Name="LevelsListBox"
ItemsSource="{Binding Elevations, UpdateSourceTrigger=PropertyChanged}"
SelectionMode="Extended"
BorderThickness="0">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected}" Content="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
My View Model:
public class AxoFromElevationViewModel : ViewModelBase
{
public AxoFromElevationModel Model { get; }
public RelayCommand CheckAll { get; }
public RelayCommand CheckNone { get; }
public AxoFromElevationViewModel(AxoFromElevationModel model)
{
Model = model;
Elevations = Model.CollectElevations();
CheckAll = new RelayCommand(OnCheckAll);
CheckNone = new RelayCommand(OnCheckNone);
}
private void OnCheckNone()
{
foreach (var e in Elevations)
{
e.IsSelected = false;
}
}
private void OnCheckAll()
{
foreach (var e in Elevations)
{
e.IsSelected = true;
}
}
/// <summary>
/// All Elevation Wrappers.
/// </summary>
private ObservableCollection<ElevationWrapper> _elevations = new ObservableCollection<ElevationWrapper>();
public ObservableCollection<ElevationWrapper> Elevations
{
get { return _elevations; }
set { _elevations = value; RaisePropertyChanged(() => Elevations); }
}
}
Finally my Elevation Class:
public sealed class ElevationWrapper : INotifyPropertyChanged
{
public string Name { get; set; }
public ElementId Id { get; set; }
public object Self { get; set; }
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set { _isSelected = value; RaisePropertyChanged("IsSelected"); }
}
public ElevationWrapper(View v)
{
Name = v.Name;
Id = v.Id;
Self = v;
IsSelected = false;
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
You should bind the IsSelected property of your ListBoxItems to the IsSelected property of your view model. This way CheckBoxes will trigger the selection and when you select an item, the related CheckBox will be checked.
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
It seems to me you want to sync 3 properties ListBoxItem.IsSelected, CheckBox.IsChecked and your models IsSelected.
My advice is that only one of the templates/styles should bind to the underlying model so I will add Yusuf answer as I will use the ListBoxItem style to bind to your model property.
After that you should bind the Checkbox.IsChecked to the ListBoxItem.IsSelected and your ListBox should look like this:
<ListBox x:Name="LevelsListBox"
ItemsSource="{Binding Elevations, UpdateSourceTrigger=PropertyChanged}"
SelectionMode="Extended"
BorderThickness="0">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Content="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Always try to bind XAML properties in a chain way, e.g. model.A binds to Model.B binds to Model.C, doing this should help you keep updates consistent and avoid wierd cases.
There is an issue with this code though, after you select multiple items and click one check box it will only unselect that item but if you click another item it will unselect all except that item.
I want to do data validation on combo-box selected value. it's default value is 0 so i want it should be selected with any value not 0.
I have binded it's selected value with int dependency property in view model.
i know we can do data validation by dependency property so any help much appreciated.
<ComboBox x:Name="cmbBox" Width="100" Margin="10,0,20,0" ItemsSource="{Binding Path=TotalPeople}"
SelectedItem="{Binding Mode=TwoWay, Path=SelectedPeopleNumber}" Height="20"/>
here is viewmodel dependency property code.
public ExpenseViewModel()
{
totalPeople = new List<int>();
populate();
}
private void populate()
{
totalPeople.Add(2);
totalPeople.Add(3);
totalPeople.Add(4);
totalPeople.Add(5);
}
private List<int> totalPeople;
public List<int> TotalPeople
{
get { return totalPeople; }
set
{
if (totalPeople != value)
{
totalPeople = value;
NotifyPropertyChanged("TotalPeople");
}
}
}
public int SelectedPeopleNumber
{
get { return (int)GetValue(SelectedPeopleNumberProperty); }
set { SetValue(SelectedPeopleNumberProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectedPeopleNumber. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedPeopleNumberProperty =
DependencyProperty.Register("SelectedPeopleNumber", typeof(int), typeof(ExpenseViewModel), new PropertyMetadata(0));
i want combobox should be red bordered when default value is 0.
You seem to have got confused between DependencyProperty and Databinding Property.
Databinding property are the ones which has source items used for displaying the data in the controls and also to store the selected values.
DependencyProperty(DP) are the ones to which you bind the above databinding properties.
In your Xaml Code SelectedItem is a DP and you have bound it to another DP(SelectedPeopleNumber) which I guess you did not intend to do.
Below is the fixed code
<ComboBox x:Name="cmbBox" Width="100" Margin="10,0,20,0" ItemsSource="{Binding Path=TotalPeople}"
SelectedItem="{Binding Path=SelectedIndex, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}" Height="20">
<ComboBox.Resources>
<Style TargetType="{x:Type ComboBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding (Validation.Errors)[0].ErrorContent,
RelativeSource={x:Static RelativeSource.Self}}"/>
</Trigger>
</Style.Triggers>
</Style>
</ComboBox.Resources>
</ComboBox>
The ViewModel
class ExpenseViewModel : INotifyPropertyChanged, IDataErrorInfo
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
var pc = PropertyChanged;
if (pc != null)
{
pc(this, new PropertyChangedEventArgs(propName));
}
}
private void populate()
{
_totalPeople.Add(2);
_totalPeople.Add(3);
_totalPeople.Add(4);
_totalPeople.Add(5);
NotifyPropertyChanged("TotalPeople");
}
private List<int> _totalPeople;
public IEnumerable<int> TotalPeople
{
get { return _totalPeople; }
}
public ExpenseViewModel()
{
_totalPeople = new List<int>();
populate();
}
public int SelectedIndex { get; set; }
private string _error;
public string Error
{
get { return _error; }
}
public string this[string columnName]
{
get
{
if (columnName.Equals("SelectedIndex"))
{
if(SelectedIndex==0)
{
_error = "Please Select One";
}
}
return Error;
}
}
}
I am making a WPF application that is navigable via custom "Next" and "Back" buttons and commands (i.e. not using a NavigationWindow). On one screen, I have a ListBox that has to support multiple selections (using the Extended mode). I have a view model for this screen and store the selected items as a property, since they need to be maintained.
However, I am aware that the SelectedItems property of a ListBox is read-only. I have been trying to work around the issue using this solution here, but I have not been able to adopt it into my implementation. I found that I can't differentiate between when one or more elements are deselected and when I navigate between screens (NotifyCollectionChangedAction.Remove is raised in both cases, since technically all the selected items are deselected when navigating away from the screen). My navigation commands are located in a separate view model which manages the view models for each screen, so I can't put any implementation related to the view model with the ListBox in there.
I have found several other less elegant solutions, but none of these seem to enforce a two-way binding between the view model and the view.
Any help would be greatly appreciated. I can provide some of my source code if it would help to understand my problem.
Try creating an IsSelected property on each of your data items and binding ListBoxItem.IsSelected to that property
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
Rachel's solutions works great! But there is one problem I've encountered - if you override the style of ListBoxItem, you loose the original styling applied to it (in my case responsible for highlighting the selected item etc.). You can avoid this by inheriting from the original style:
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
Note setting BasedOn (see this answer)
.
I couldn't get Rachel's solution to work how I wanted it, but I found Sandesh's answer of creating a custom dependency property to work perfectly for me. I just had to write similar code for a ListBox:
public class ListBoxCustom : ListBox
{
public ListBoxCustom()
{
SelectionChanged += ListBoxCustom_SelectionChanged;
}
void ListBoxCustom_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItemsList = SelectedItems;
}
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register(nameof(SelectedItemsList), typeof(IList), typeof(ListBoxCustom), new PropertyMetadata(null));
}
In my View Model I just referenced that property to get my selected list.
I kept looking into an easy solution for this but with no luck.
The solution Rachel has is good if you already have the Selected property on the object within your ItemsSource. If you do not, you have to create a Model for that business model.
I went a different route. A quick one, but not perfect.
On your ListBox create an event for SelectionChanged.
<ListBox ItemsSource="{Binding SomeItemsSource}"
SelectionMode="Multiple"
SelectionChanged="lstBox_OnSelectionChanged" />
Now implement the event on the code behind of your XAML page.
private void lstBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listSelectedItems = ((ListBox) sender).SelectedItems;
ViewModel.YourListThatNeedsBinding = listSelectedItems.Cast<ObjectType>().ToList();
}
Tada. Done.
This was done with the help of converting SelectedItemCollection to a List.
Here's yet another solution. It's similar to Ben's answer, but the binding works two ways. The trick is to update the ListBox's selected items when the bound data items change.
public class MultipleSelectionListBox : ListBox
{
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(IEnumerable<string>), typeof(MultipleSelectionListBox),
new FrameworkPropertyMetadata(default(IEnumerable<string>),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
public IEnumerable<string> BindableSelectedItems
{
get => (IEnumerable<string>)GetValue(BindableSelectedItemsProperty);
set => SetValue(BindableSelectedItemsProperty, value);
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
BindableSelectedItems = SelectedItems.Cast<string>();
}
private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MultipleSelectionListBox listBox)
listBox.SetSelectedItems(listBox.BindableSelectedItems);
}
}
Unfortunately, I wasn't able to use IList as the BindableSelectedItems type. Doing so sent null to my view model's property, whose type is IEnumerable<string>.
Here's the XAML:
<v:MultipleSelectionListBox
ItemsSource="{Binding AllMyItems}"
BindableSelectedItems="{Binding MySelectedItems}"
SelectionMode="Multiple"
/>
There's one thing to watch out for. In my case, a ListBox may be removed from the view. For some reason, this causes the SelectedItems property to change to an empty list. This, in turn, causes the view model's property to be changed to an empty list. Depending on your use case, this may not be desirable.
This was pretty easy to do with a Command and the Interactivities EventTrigger. ItemsCount is just a bound property to use on your XAML, should you want to display the updated count.
XAML:
<ListBox ItemsSource="{Binding SomeItemsSource}"
SelectionMode="Multiple">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
CommandParameter="{Binding ElementName=MyView, Path=SelectedItems.Count}" />
</i:EventTrigger>
</Interaction.Triggers>
</ListView>
<Label Content="{Binding ItemsCount}" />
ViewModel:
private int _itemsCount;
private RelayCommand<int> _selectionChangedCommand;
public ICommand SelectionChangedCommand
{
get {
return _selectionChangedCommand ?? (_selectionChangedCommand =
new RelayCommand<int>((itemsCount) => { ItemsCount = itemsCount; }));
}
}
public int ItemsCount
{
get { return _itemsCount; }
set {
_itemsCount = value;
OnPropertyChanged("ItemsCount");
}
}
Turns out binding a check box to the IsSelected property and putting the textblock and checkbox within a stack panel does the trick!
Not satisfied with the given answers I was trying to find one by myself...
Well it turns out to be more like a hack then a solution but for me that works fine. This Solution uses MultiBindings in a special way.
First it may look like a ton of Code but you can reuse it with very little effort.
First I implemented a 'IMultiValueConverter'
public class SelectedItemsMerger : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
SelectedItemsContainer sic = values[1] as SelectedItemsContainer;
if (sic != null)
sic.SelectedItems = values[0];
return values[0];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new[] { value };
}
}
And a SelectedItems Container/Wrapper:
public class SelectedItemsContainer
{
/// Nothing special here...
public object SelectedItems { get; set; }
}
Now we create the Binding for our ListBox.SelectedItem (Singular). Note: You have to create a static Resource for the 'Converter'. This may be done once per application and be reused for all ListBoxes that need the converter.
<ListBox.SelectedItem>
<MultiBinding Converter="{StaticResource SelectedItemsMerger}">
<Binding Mode="OneWay" RelativeSource="{RelativeSource Self}" Path="SelectedItems"/>
<Binding Path="SelectionContainer"/>
</MultiBinding>
</ListBox.SelectedItem>
In the ViewModel I created the Container where I can bind to. It is important to initialize it with new() in order to fill it with the values.
SelectedItemsContainer selectionContainer = new SelectedItemsContainer();
public SelectedItemsContainer SelectionContainer
{
get { return this.selectionContainer; }
set
{
if (this.selectionContainer != value)
{
this.selectionContainer = value;
this.OnPropertyChanged("SelectionContainer");
}
}
}
And that's it. Maybe someone sees some improvements?
What do You think about it?
This was a major issue for me, some of the answers I have seen were either too hackish, or required resetting the SelectedItems property value breaking any code attached to the properties OnCollectionChanged event. But I managed to get a workable solution by modifying the collection directly and as a bonus it even supports SelectedValuePath for object collections.
public class MultipleSelectionListBox : ListBox
{
internal bool processSelectionChanges = false;
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(object), typeof(MultipleSelectionListBox),
new FrameworkPropertyMetadata(default(ICollection<object>),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
public dynamic BindableSelectedItems
{
get => GetValue(BindableSelectedItemsProperty);
set => SetValue(BindableSelectedItemsProperty, value);
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
if (BindableSelectedItems == null || !this.IsInitialized) return; //Handle pre initilized calls
if (e.AddedItems.Count > 0)
if (!string.IsNullOrWhiteSpace(SelectedValuePath))
{
foreach (var item in e.AddedItems)
if (!BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
BindableSelectedItems.Add((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
}
else
{
foreach (var item in e.AddedItems)
if (!BindableSelectedItems.Contains((dynamic)item))
BindableSelectedItems.Add((dynamic)item);
}
if (e.RemovedItems.Count > 0)
if (!string.IsNullOrWhiteSpace(SelectedValuePath))
{
foreach (var item in e.RemovedItems)
if (BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
BindableSelectedItems.Remove((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
}
else
{
foreach (var item in e.RemovedItems)
if (BindableSelectedItems.Contains((dynamic)item))
BindableSelectedItems.Remove((dynamic)item);
}
}
private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MultipleSelectionListBox listBox)
{
List<dynamic> newSelection = new List<dynamic>();
if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
foreach (var item in listBox.BindableSelectedItems)
{
foreach (var lbItem in listBox.Items)
{
var lbItemValue = lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null);
if ((dynamic)lbItemValue == (dynamic)item)
newSelection.Add(lbItem);
}
}
else
newSelection = listBox.BindableSelectedItems as List<dynamic>;
listBox.SetSelectedItems(newSelection);
}
}
}
Binding works just as you would have expected MS to have done themselves:
<uc:MultipleSelectionListBox
ItemsSource="{Binding Items}"
SelectionMode="Extended"
SelectedValuePath="id"
BindableSelectedItems="{Binding mySelection}"
/>
It has not been thoroughly tested but has passed first glance inspections. I tried to keep it reuseable by employing dynamic types on the collections.
It took me a while to implement binding/using SelectedItems as I am not an expert at this so I wanted to share my solution if someone might find it useful. Do not forget to download Microsoft.Xaml.Behaviors.Wpf from Nuget for this solution.
I have benefited from Accessing WPF ListBox SelectedItems
View:
Window x:Class="WpfAppSelectedItems.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:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:WpfAppSelectedItems"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ListBox Height="250" Width="300"
ItemsSource="{Binding Items}" SelectionMode="Extended"
>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.InputBindings>
<KeyBinding Gesture="Ctrl+A" Command="{Binding SelectAllCommand}" />
</ListBox.InputBindings>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged" >
<i:CallMethodAction TargetObject="{Binding}" MethodName="ListBox_SelectionChanged"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
</Grid>
</Window>
`
Code behind:
namespace WpfAppSelectedItems
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel(); //connecting window to VM
}
}
}
ViewModel:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.Xaml.Behaviors;
using System.Windows;
namespace WpfAppSelectedItems
{
internal class ViewModel: Presenter
{
//Creating ItemPresenter class. IsSelected binded to Style in the view
public class ItemPresenter : Presenter
{
private readonly string _value;
public ItemPresenter(string value)
{
_value = value;
}
public override string ToString()
{
return _value;
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
OnPropertyChanged();
}
}
}
//Placing items to the Items which is binded to the ListBox
public ObservableCollection<ItemPresenter> Items { get; } = new ObservableCollection<ItemPresenter>
{
new ItemPresenter("A"),
new ItemPresenter("B"),
new ItemPresenter("C"),
new ItemPresenter("D")
};
//Do something when selection changed including detecting SelectedItems
public void ListBox_SelectionChanged()
{
foreach (var item in Items)
{
if (item.IsSelected)
MessageBox.Show(fufuitem.ToString());
}
}
};
//Notify View if a property changes
public abstract class Presenter : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
if you just want to get the Name of the selected Element you can do the following:
View:
<ListBox
x:Name="Folders"
Grid.Row="1"
Grid.Column="0"
ItemsSource="{Binding YourListWithStings}"
SelectionMode="Single"
SelectedItem="{Binding ToYourOutputVariable}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Viewmodel:
private string _ToYourOutputVariable
public string ToYourOutputVariable
{
get {return _ToYourOutputVariable; }
set
{
_ToYourOutputVariable = value;
NotifyOfPropertyChange();
MessageBox.Show(_ToYourOutputVariable);
}
}
The messageBox shows the name of the selected listitem. You could call a function where you open the MessageBox
So here is the XAML that I have:
<ItemsControl ItemsSource="{Binding Path=Groups}" ItemTemplateSelector="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=ListTemplateSelector}"/>
Here is my ListTemplateSelector class:
public class ListTemplateSelector : DataTemplateSelector {
public DataTemplate GroupTemplate { get; set; }
public DataTemplate ItemTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
GroupList<Person> list = item as GroupList<Person>;
if (list != null && !list.IsLeaf)
return GroupTemplate;
return ItemTemplate;
}
}
The GroupTemplate data template references the ListTemplateSelector inside itself, so this is why I have set up like I have it set up. It's the only recursive hack I could put together. But that's not the problem I'm having.
My problem is, I want to change from ItemTemplate to GroupTemplate when the IsLeaf property changes. This works beautifully the very first time since it reads the property the first time. But once this property changes, the template selector doesn't get reapplied. Now, I could use triggers to bind to the value and set the item template appropriately, but I need to be able to set a different template for each item, as they could be in a different state.
For instance, say I have a list of groups like this:
Group 1: IsLeaf = false, so template = GroupTemplate
Group 2: IsLeaf = true, so template = ItemTemplate
Group 3: IsLeaf = false, so template = GroupTemplate
And once group 1's IsLeaf property changes to true, the template needs to automatically change to ItemTemplate.
EDIT:
Here is my temporary solution. Any better way to do it?
<ItemsControl ItemsSource="{Binding Path=Groups}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="ContentTemplate" Value="{DynamicResource ItemTemplate}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsLeaf}" Value="False">
<Setter Property="ContentTemplate" Value="{DynamicResource GroupTemplate}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I found this workaround that seems easier to me. From within the TemplateSelector listen to the property that your care about and then reapply the template selector to force a refresh.
public class DataSourceTemplateSelector : DataTemplateSelector
{
public DataTemplate IA { get; set; }
public DataTemplate Dispatcher { get; set; }
public DataTemplate Sql { get; set; }
public override DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
var ds = item as DataLocationViewModel;
if (ds == null)
{
return base.SelectTemplate(item, container);
}
PropertyChangedEventHandler lambda = null;
lambda = (o, args) =>
{
if (args.PropertyName == "SelectedDataSourceType")
{
ds.PropertyChanged -= lambda;
var cp = (ContentPresenter)container;
cp.ContentTemplateSelector = null;
cp.ContentTemplateSelector = this;
}
};
ds.PropertyChanged += lambda;
switch (ds.SelectedDataSourceType.Value)
{
case DataSourceType.Dispatcher:
return Dispatcher;
case DataSourceType.IA:
return IA;
case DataSourceType.Sql:
return Sql;
default:
throw new NotImplementedException(ds.SelectedDataSourceType.Value.ToString());
}
}
}
Regarding your EDIT, wouldn't a DataTemplate Trigger be enough instead of using a Style? That is:
<ItemsControl ItemsSource="{Binding Path=Groups}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl x:Name="cc" Content="{Binding}" ContentTemplate="{DynamicResource ItemTemplate}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsLeaf}" Value="False">
<Setter TargetName="cc" Property="ContentTemplate" Value="{DynamicResource GroupTemplate}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Returning back to your original solution and the problem of "the template selector doesn't get reapplied": you can refresh your view like that
CollectionViewSource.GetDefaultView(YourItemsControl.ItemsSource).Refresh();
where for brevity sake your ItemsControl is referenced by its name ("YourItemsControl") added to your XAML:
<ItemsControl x:Name="YourItemsControl" ItemsSource="{Binding Path=Groups}"
ItemTemplateSelector="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=ListTemplateSelector}"/>
The only problem may be how to choose right place in your project for this refresh instruction. It could go into a view code-behind, or, if your IsLeaf is a DP, the right place would be a dependency-property-changed callback.
I do it with a binding proxy.
It works like a normal binding proxy (but with 2 Props - copies data from DataIn to DataOut), but sets the DataOut to NULL and back to the DataIn value whenever the Trigger value changes:
public class BindingProxyForTemplateSelector : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxyForTemplateSelector();
}
#endregion
public object DataIn
{
get { return (object)GetValue(DataInProperty); }
set { SetValue(DataInProperty, value); }
}
public object DataOut
{
get { return (object) GetValue(DataOutProperty); }
set { SetValue(DataOutProperty, value); }
}
public object Trigger
{
get { return (object) GetValue(TriggerProperty); }
set { SetValue(TriggerProperty, value); }
}
public static readonly DependencyProperty TriggerProperty = DependencyProperty.Register(nameof(Trigger), typeof(object), typeof(BindingProxyForTemplateSelector), new PropertyMetadata(default(object), OnTriggerValueChanged));
public static readonly DependencyProperty DataInProperty = DependencyProperty.Register(nameof(DataIn), typeof(object), typeof(BindingProxyForTemplateSelector), new UIPropertyMetadata(null, OnDataChanged));
public static readonly DependencyProperty DataOutProperty = DependencyProperty.Register(nameof(DataOut), typeof(object), typeof(BindingProxyForTemplateSelector), new PropertyMetadata(default(object)));
private static void OnTriggerValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// this does the whole trick
var sender = d as BindingProxyForTemplateSelector;
if (sender == null)
return;
sender.DataOut = null; // set to null and then back triggers the TemplateSelector to search for a new template
sender.DataOut = sender.DataIn;
}
private static void OnDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var sender = d as BindingProxyForTemplateSelector;
if (sender == null)
return;
sender.DataOut = e.NewValue;
}
}
Use it like this:
<Grid>
<Grid.Resources>
<local:BindingProxyForTemplateSelector DataIn="{Binding}" Trigger="{Binding Item.SomeBool}" x:Key="BindingProxy"/>
</Grid.Resources>
<ContentControl Content="{Binding Source={StaticResource BindingProxy}, Path=DataOut.Item}" ContentTemplateSelector="{StaticResource TemplateSelector}"/>
</Grid>
So you don't bind to your DataContext directly, but to the BindingProxy's DataOut, which mirrors the original DataContext, but with a small difference: When the trigger changes (in this example a bool value inside the 'Item'), the TemplateSelector gets retriggered.
You don't have to change your TemplateSelector for this.
It is also possible to add more Triggers, just add a Trigger2.
I wasn't really satisfied with the solutions I will post the way I've managed to get the selector check for changes:
public class DynamicSelectorContentControl : ContentControl
{
// Using a DependencyProperty as the backing store for ListenToProperties. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ListenToPropertiesProperty =
DependencyProperty.Register("ListenToProperties", typeof(string),
typeof(DynamicSelectorContentControl),
new FrameworkPropertyMetadata(string.Empty));
public DynamicSelectorContentControl()
{
this.DataContextChanged += DynamicSelectorContentControl_DataContextChanged;
}
public string ListenToProperties
{
get { return (string)GetValue(ListenToPropertiesProperty); }
set { SetValue(ListenToPropertiesProperty, value); }
}
private void CheckForProperty(object sender, PropertyChangedEventArgs e)
{
if (ListenToProperties.Contains(e.PropertyName))
{
ClearSelector();
}
}
private void ClearSelector()
{
var oldSelector = this.ContentTemplateSelector;
if (oldSelector != null)
{
this.ContentTemplateSelector = null;
this.ContentTemplateSelector = oldSelector;
}
}
private void DynamicSelectorContentControl_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
var listOfProperties = ListenToProperties.Split(',').Select(s => s.Trim());
var oldObservable = e.OldValue as INotifyPropertyChanged;
if (oldObservable != null && listOfProperties.Any())
{
PropertyChangedEventManager.RemoveHandler(oldObservable, CheckForProperty, string.Empty);
}
var newObservable = e.NewValue as INotifyPropertyChanged;
if (newObservable != null && listOfProperties.Any())
{
PropertyChangedEventManager.AddHandler(newObservable, CheckForProperty, string.Empty);
}
if (e.OldValue != null)
{
ClearSelector();
}
}
}
Usage in XAML:
<controls:DynamicSelectorContentControl DockPanel.Dock="Top"
ContentTemplateSelector="{StaticResource AgeGenderSelector}"
ListenToProperties="Gender, Age"
Content="{Binding .}"/>
This could be changed to have the dependency be a list, but a string was better for my case.
It works well and has no memory leak. Besides, you can have you DataTemplates in an extra file which does not garbage your main xaml.
Cheers,
Marco
I'm tracking ListView selection changes in an MVVM design by binding to IsSelected. I also need to track the current item by enabling IsSynchronizedWithCurrentItem.
I find that when I have two ListView binding to the same collection I get the InvalidOperationException: "Collection was modified; enumeration operation may not execute." It seems to be a synchonization error between the two ListViews; one is triggering a PropertyChanged event while the other is updating the Selector perhaps?
I can't figure out how to get around this other than forgoing use of IsSynchronizedWithCurrentItem and managing it myself. Any ideas?
Thanks.
The ViewModel and code behind:
public class Item : INotifyPropertyChanged
{
public string Name{ get; set; }
public bool IsSelected
{
get { return isSelected; }
set { isSelected = value; OnPropertyChanged("IsSelected"); }
}
private bool isSelected;
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ViewModel
{
public ViewModel()
{
Items = new ObservableCollection<Item>()
{
new Item(){Name = "Foo"},
new Item(){Name = "Bar"}
};
}
public ObservableCollection<Item> Items { get; private set; }
}
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
The XAML:
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="100" Width="100">
<StackPanel>
<ListView DataContext="{Binding Items}" ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="True" SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name, Mode=OneWay}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView DataContext="{Binding Items}" ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="True" SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name, Mode=OneWay}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Window>
I cannot offer a direct fix for your problem. However, I do have a solution that will work.
What you can do is introduce a second property on your View Model called 'SelectedItem' that will hold a reference to the Item that is selected in your ListView. In addition, in your View Model you listen for the PropertyChanged event. If the associated Property Name is IsSelected then you update the SelectedItem property to be the sender of that event (the Item that now has IsSelected = true). You can then bind the SelectedItem property of the ListView to the property of the same name of the ViewModel class.
My code for the revised ViewModel class is below.
public class ViewModel : INotifyPropertyChanged
{
private Item _selectedItem;
public ViewModel()
{
Items = new ObservableCollection<Item>()
{
new Item {Name = "Foo"},
new Item {Name = "Bar"}
};
foreach ( Item anItem in Items )
{
anItem.PropertyChanged += OnItemIsSelectedChanged;
}
}
public ObservableCollection<Item> Items { get; private set; }
public Item SelectedItem
{
get { return _selectedItem; }
set
{
// only update if the value is difference, don't
// want to send false positives
if ( _selectedItem == value )
{
return;
}
_selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnItemIsSelectedChanged(object sender, PropertyChangedEventArgs e)
{
if ( e.PropertyName != "IsSelected" )
{
return;
}
SelectedItem = sender as Item;
}
private void OnPropertyChanged(string propertyName)
{
if ( PropertyChanged != null )
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The issue seems to happen when you bind to a listbox's IsSelected and use SelectionMode='Single'
I found that changing the SelectionMode = 'Multiple' and then just added logic to the ViewModel to ensure that there was ever only one item with IsSelected set to true worked.