Items.Count = 0 in SelectionChanged when there are items - c#

I want to display some custom content (using datatemplate) on button click:
<ContentControl x:Name="content" />
<Button Content="Test" Click="button_Click" />
Button shows/hides content like this
VM _vm = new VM();
void button_Click(object sender, RoutedEventArgs e) =>
content.Content = content.Content == null ? _vm : null;
Here is datatemplate:
<DataTemplate DataType="{x:Type local:VM}">
<ListBox ItemsSource="{Binding Items}" SelectionChanged="listBox_SelectionChanged">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</DataTemplate>
Event handler:
void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e) =>
Title = "Items: " + ((ListBox)sender).Items.Count;
Viewmodel:
public class VM
{
public List<Item> Items { get; } = new List<Item> { new Item(), new Item(), new Item() };
}
public class Item
{
public bool IsSelected { get; set; }
}
The problem: when datatemplate is unloaded then SelectionChanged for ListBox event is rised with no items.
I do not want this event. I don't want to see "Items: 0" after selecting something and unloading datatemplate.
Question: what is happening and how can I prevent this from happening?
Note: this is very short and simplified MCVE, i.e. not everything is pretty, though there are key points: datatemplate with ListBox inside, which uses IsSelected binding and I need to get rid from that SelectionChanged event at unloading.
Call stack:

This is working exactly as designed. You made a selection by clicking an item in the list box. When the template is unloaded, the ItemsSource binding is disconnected, and the items source becomes empty. At that point, the current selection is no longer valid (the item doesn't exist in the items source), so the selection is cleared. That's a selection change: the selection went from something to nothing. The event is expected to be raised under these circumstances.
It's rarely necessary to subscribe to SelectionChanged. It's usually better to bind the SelectedItem to a property on your view model. Whenever the selection changes, that property will be updated. Instead of responding to the SelectionChanged event, you can respond to that property changing.
This approach nicely avoids the issue you're seeing. Once the template is unloaded, the SelectedItem binding will be disconnected, so your view model won't be updated anymore. Consequently, you won't see that final change when the selection is cleared.
Alternate solution for multiple selections
If your ListBox supports multiple selections, you can continue subscribing to SelectionChanged. However, don't query listBoxItems; instead, scan through _vm.Items and see which items have IsSelected set to true. That should tell you the actual selection, and the results should not be affected by the template being unloaded.
You can also determine that the template was unloaded by checking whether (sender as ListBox)?.ItemsSource is null in your handler. However, this should not be necessary.

I think this is occurring because you are always overwriting the content:
VM _vm = new VM();
void button_Click(object sender, RoutedEventArgs e) =>
content.Content = content.Content == null ? _vm : null;
Change it to this so the list only gets assigned once, so it only gets assigned once.
void button_Click(object sender, RoutedEventArgs e)
{
if (content.Content == null )
{
content.Content = _vm;
// I also recommend you add the event handler for the ListBox here so it's not fired until you have content.
}
}

I think listBox_SelectionChanged when you unload the list this event is fired, because section actually changed, Check item count there, and if it is 0 set the title to default.
void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e) =>
Title = (((ListBox)sender).Items.Count > 0)? "Items: " + ((ListBox)sender).Items.Count: "Your title";

Related

How to bind the selecteditem property of Windows.UI.Xaml.Controls.TreeView when Selection mode is Multiple

I am using the Treeview control of Windows.UI.Xaml.Controls with 'multiple' selection mode. Here I need to bind some property as SelectedItem in twoway binding to fetch the selected nodes into an enumerable collection.
But Windows.UI.Xaml.Controls.TreeView does not have selectedItem property or any selectionChanged event. How do I fetch the selected nodes?
<muxc:TreeView SelectionMode="Multiple" Width="300" x:Name="CategoriesTree"
ItemsSource="{x:Bind _vm.Categories, Mode=OneWay}">
<muxc:TreeView.ItemTemplate>
<DataTemplate x:DataType="local1:CategoriesInfo">
<TreeViewItem ItemsSource="{x:Bind SubCategories}" Content="{x:Bind Name}" />
</DataTemplate>
</muxc:TreeView.ItemTemplate>
</muxc:TreeView>
I tried to use TreeView of Microsoft.UI.Xaml.Controls in WinUi2 but it throws a runtime error
No such Interface supported
by Windows.Ui.Xaml. How do I do this?
As you can see in the documentation, there is no node selectionChanged event for now.
Even with TreeView.ItemInvoked Event
This event is not fired when the item's multiple-selection checkbox is
checked or unchecked.
Here is a workaround, you can handle the mouse click event in the TreeView, detect the change of the TreeView SelectedNodes.
int oldCount = 0;
public MainPage()
{
this.InitializeComponent();
DessertTree.AddHandler(PointerReleasedEvent, new PointerEventHandler(TreeView_OnPointerReleased), true);
}
private void TreeView_OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
if (oldCount != DessertTree.SelectedNodes.Count)
{
foreach (TreeViewNode node in DessertTree.SelectedNodes)
{
do somthing...
}
oldCount = DessertTree.SelectedNodes.Count;
}
}

Datagrid sort, what happened to row?

I am coding a UserControl that can be use as a ScrollBar with color mark on it (like mostly all IDE).
My UserControl look
<UserControl x:Class="MarkedScrollBar" x:Name="Instance" ...>
<Grid Name="grd" >
<COLUMN/ROW DEF>...</>
<ScrollViewer Name="scrView" Grid.RowSpan="3" Grid.ColumnSpan="2" >
<my:IcuContentPresenter x:Name="presenter" Content="{Binding AdditionnalContent, ElementName=Instance}" ContentUpdated="presenter_ContentUpdated" />
</ScrollViewer>
<Canvas ...(Canvas place on the scrollbar) />
</Grid>
</UserControl>
On my UserControl, everything work fine. I can add content in my IcuContentPresenter (Extension of ContentPresenter to get 1 more event).
In my main window i use my MarkedScroolBar like this :
<my:IcuMarkedScrollBar Grid.Row="0" x:Name="bbb" Grid.Column="0" >
<my:IcuMarkedScrollBar.AdditionnalContent>
<my:IcuDataGrid ItemsSource="{Binding AAA, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" Loaded="DataGrid_Loaded" Width="300" LoadingRow="DataGrid_LoadingRow" />
</my:IcuMarkedScrollBar.AdditionnalContent>
</my:IcuMarkedScrollBar>
Again, i have a DataGrid extension to get 1 event (Sorted event, code from here).
Now my problem is that when i sort my datagrid Inside my MarkedScrollBar, i want the mark to follow the content, that the mark is binding to.
First attempt, I try to link the mark with the control having a Dictionary Key=Mark, Value=Control. I was getting the DatagridRow that I wanted to have a mark for my datacontext.
First time the datagrid appear, my mark are all well place, but if I sort, my mark don't follow. I went into debugging and saw that there is a DataGridRow that contains my datacontext item, but it's not the one i save in my Dictionary.
End First attempt
Second attempt, with the DataGridRow changing when I sort, I thought that if I was linking my DataContext item to my mark it would fix the bug.
So now, when I sort my datagrid, I try to find the datagridrow that contains my datacontext item. But it does find anything.
But, when i reexecute the same code manually with a button in the window, it's work. All my mark appear at the good position.
End second attempt
Brief explanation of my extension :
DataGrid : I use it to get the Sorted event, because in Sorting event the row where not already move.
ContentPresenter : I add event on it to get modification to the content, but not to the Property (like datagrid sorting). For that i needed to check the type of the content, so i don't support everything for now.
My code for the ContentPresenter is that :
public class IcuContentPresenter : ContentPresenter
{
public event EventHandler<ControlEventArgs> ContentUpdated;
public IcuContentPresenter()
{
this.Loaded += OnLoaded;
}
protected void OnLoaded(object p_oSender, RoutedEventArgs p_oEvent)
{
Type t = this.Content.GetType();
if (t == typeof(IcuDataGrid))
{
IcuDataGrid oControl = this.Content as IcuDataGrid;
oControl.Sorted += CallEvent;
}
}
void CallEvent(object sender, EventArgs e)
{
if (ContentUpdated != null)
{
ContentUpdated(this, new ControlEventArgs(Content.GetType()));
}
}
public UIElement FindVisualElementForObject(object p_oObject)
{
UIElement oTheOne = null;
if (Content.GetType() == typeof(IcuDataGrid))
{
oTheOne = FindInDatagrid(p_oObject);
}
return oTheOne;
}
private UIElement FindInDatagrid(Object p_oObj)
{
DataGrid oGrid = Content as DataGrid;
var a = (DataGridRow)oGrid.ItemContainerGenerator.ContainerFromItem(p_oObj);
return a;
}
//----------------------------------------------------
// Inner class
//----------------------------------------------------
public class ControlEventArgs : EventArgs
{
public ControlEventArgs(Type value)
{
ControlType = value;
}
public Type ControlType { get; set; }
}
}
To support a control i will have to add them here. On the load of this control, I check the content, if the content is my Datagrid on the event Sorted i call the my Event ContentUpdated to be able to update my mark. But at this moment, it's like my datagrid contains no row with my datacontext item.
When virtualization is enabled, row elements will only be generated for data items which are actually in view. You will need to associate the marks with the underlying data items (row objects) rather than the DataGridRow elements. The Items property on the DataGrid should have the rows in their correctly sorted positions.

ListPicker SelectionChanged Event Called Multiple Times During Navigation

I have a SelectionChanged event in a ListPicker within one of my application Pages that fires multiple times before the page is loaded. This is really inconvenient for me as when an item is selected, a MessageBox is displayed (and other actions will be performed). The MessageBox is displayed twice every time the page is NavigatedTo. How can I fix this?
XAML
<toolkit:ListPicker x:Name="ThemeListPicker" Header="Theme"
ItemTemplate="{StaticResource PickerItemTemplate}"
SelectionChanged="ThemeListPicker_SelectionChanged"/>
XAML.CS
private void ThemeListPicker_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
if(ThemeListPicker.SelectedIndex != -1)
{
var theme = (sender as ListPicker).SelectedItem;
if (index == 0)
{
Settings.LightTheme.Value = true;
MessageBox.Show("light");
}
else
{
Settings.LightTheme.Value = false;
MessageBox.Show("dark");
}
}
}
well, that's how a listpicker behaves, what best you can do is instead of making ThemeListPicker_SelectionChanged make a parent stackpanel inside the datatemplate somewhat like this
<Listpicker.ItemTemplate>
<DataTemplate x:Name="PickerItemTemplate">
<StackPanel tap="stk_Tap">
<TextBlock/>
</StackPanel>
</DataTemplate>
</Listpicker.ItemTemplate>
<Listpicker.FullModeItemTemplate>
<DataTemplate x:Name="PickerFullModeItemTemplate">
<StackPanel tap="stk_Tap">
<TextBlock/>
</StackPanel>
</DataTemplate>
<Listpicker.FullModeItemTemplate>
now use this tap stk_Tap to do your action as, this event would also get called every time the selection changed gets called but, it wont exhibit the buggy behavior like that of selection changed event.
hope this helps.
Attach the SelectionChanged event after the ListPicker is Loaded.
...
InitializeComponent();
YourListPicker.Loaded += YourListPicker_Loaded;
...
private void YourListPicker_Loaded(object sender, RoutedEventArgs e)
{
YourListPicker.SelectionChanged += YourListPicker_SelectionChanged;
}

WPF datagrid combobox column: how to manage event of selection changed?

I have a datagrid, with a combobox column
<DataGridComboBoxColumn x:Name="DataGridComboBoxColumnBracketType" Width="70" Header="Tipo di staffa" SelectedValueBinding="{Binding type, UpdateSourceTrigger=PropertyChanged}">
</DataGridComboBoxColumn>
I want an event that is fired only when the user changes the value into the combobox.
How can I resolve this?
I found a solution to this on CodePlex. Here it is, with some modifications:
<DataGridComboBoxColumn x:Name="Whatever">
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="{x:Type ComboBox}">
<EventSetter Event="SelectionChanged" Handler="SomeSelectionChanged" />
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
and in the code-behind:
private void SomeSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var comboBox = sender as ComboBox;
var selectedItem = this.GridName.CurrentItem;
}
And the xaml code provided by #kevinpo from CodePlex and help from David Mohundro's blog, programatically:
var style = new Style(typeof(ComboBox));
style.Setters.Add(new EventSetter(ComboBox.SelectionChangedEvent, new SelectionChangedEventHandler(SomeSelectionChanged)));
dataGridComboBoxColumn.EditingElementStyle = style;
To Complete Kevinpo answer, for the code behind you should add some protection because the selectionChanged event is triggered 2 time with a datagridcolumncombobox:
1) first trigger : when you selected a new item
2) Second trigger : when you click on an other datagridcolumn after you selected a new item
The problem is that on the second trigger the ComboBox
value is null because you don't have changed the selected item.
private void SomeSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var comboBox = sender as ComboBox;
if (comboBox.SelectedItem != null)
{
YOUR CODE HERE
}
}
That was my problem, I wish it will help someone else !
The problem of getting SelectionChanged events to fire on DataGridComboBoxColumn cells was one that plagued me recently. I used the following solution:
private void DataGridView_PreparingCellForEdit(object sender, DataGridPreparingCellForEditEventArgs e)
{
if (e.Column.DisplayIndex == 3) //Use this IF statement to specifiy which combobox columns you want to attach the event to.
{
ComboBox? cb = e.EditingElement as ComboBox;
if (cb != null)
{
// As this event fires everytime the user starts editing the cell you need to dettach any previously attached instances of "My_SelectionChanged", otherwise you'll have it firing multiple times.
cb.SelectionChanged -= My_SelectionChanged;
// now re-attach the event handler.
cb.SelectionChanged += My_SelectionChanged;
}
}
}
Then set up your custom SelectionChanged event handler to whatever you need:
private void My_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Your event code here...
}

ListBox ScrollIntoView when using CollectionViewSource with GroupDescriptions (i.e. IsGrouping == True)

Short version
I would like to scroll the ListBox item into view when the selection is changed.
Long version
I have a ListBox with the ItemsSource bound to a CollectionViewSource with a GroupDescription, as per the example below.
<Window.Resources>
<CollectionViewSource x:Key="AnimalsView" Source="{Binding Source={StaticResource Animals}, Path=AnimalList}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Category"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
<ListBox x:Name="AnimalsListBox"ItemsSource="{Binding Source={StaticResource AnimalsView}}" ItemTemplate="{StaticResource AnimalTemplate}" SelectionChanged="ListBox_SelectionChanged">
<ListBox.GroupStyle>
<GroupStyle HeaderTemplate="{StaticResource CategoryTemplate}" />
</ListBox.GroupStyle>
</ListBox>
There is a SelectionChanged event in the a code-behind file.
public List<Animal> Animals { get; set; }
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox control = (ListBox)sender;
control.ScrollIntoView(control.SelectedItem);
}
Now. If I set the AnimalsListBox.SelectedItem to an item that is currently not visible I would like to have it scroll in view. This is where it gets tricky, as the ListBox is being groups (the IsGrouped property is true) the call to ScrollIntoView fails.
System.Windows.Controls.ListBox via Reflector. Note the base.IsGrouping in the OnBringItemIntoView.
public void ScrollIntoView(object item)
{
if (base.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
this.OnBringItemIntoView(item);
}
else
{
base.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new DispatcherOperationCallback(this.OnBringItemIntoView), item);
}
}
private object OnBringItemIntoView(object arg)
{
FrameworkElement element = base.ItemContainerGenerator.ContainerFromItem(arg) as FrameworkElement;
if (element != null)
{
element.BringIntoView();
}
else if (!base.IsGrouping && base.Items.Contains(arg))
{
VirtualizingPanel itemsHost = base.ItemsHost as VirtualizingPanel;
if (itemsHost != null)
{
itemsHost.BringIndexIntoView(base.Items.IndexOf(arg));
}
}
return null;
}
Questions
Can anyone explain why it does not work when using grouping?
The ItemContainerGenerator.ContainerFromItem always returns null, even though it's status states that all the containers have been generated.
How I can achieve the scrolling into view when using grouping?
I have found a solution to my problem. I was certain that I wasn't the first person to hit this issue so I continued to search StackOverflow for solutions and I stumbled upon this answer by David about how ItemContainerGenerator works with a grouped list.
David's solution was to delay accessing the ItemContainerGenerator until after the rendering process.
I have implemented this solution, with a few changes that I will detail after.
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox control = (ListBox)sender;
if (control.IsGrouping)
{
if (control.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(DelayedBringIntoView));
else
control.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
else
control.ScrollIntoView(control.SelectedItem);
}
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
return;
ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(DelayedBringIntoView));
}
private void DelayedBringIntoView()
{
var item = ItemContainerGenerator.ContainerFromItem(SelectedItem) as ListBoxItem;
if (item != null)
item.BringIntoView();
}
Changes:
Only uses the ItemContainerGenerator approach when it IsGrouping is true, otherwise continue to use the default ScrollIntoView.
Check if the ItemContainerGenerator is ready, if so dispatch the action, otherwise listen for the ItemContainerGenerator status to change.. This is important as if it is ready then the StatusChanged event will never fire.
The out of the box VirtualizingStackPanel does not support virtualizing grouped collection views. When a grouped collection is rendered in an ItemsControl, each group as a whole is an item as opposed to each item in the collection which results in "jerky" scrolling to each group header and not each item.
You'll probably need to roll your own VirtualizingStackPanel or ItemContainerGenerator in order to keep track of the containers displayed in a group. It sounds ridiculous, but the default virtualization with grouping in WPF is lacking to say the least.

Categories