In my View I have a Search Box for filtering and a tree that should be automatically expanded on specific conditions and when the search string is changed.
So the user should be able to see all nodes that are found.
I'm using a TreeListView which is a simple TreeView with columns, it also behaves like one.
Also I'm using a ControlTemplate.
Because the control template is ignoring any ItemsSource set in the original TreeView I'm using a hack:
ControlTemplate:
<ControlTemplate x:Key="TreeControlTemplate">
<Border>
<treeList:TreeListView ItemsSource="{Binding DataContext, RelativeSource={RelativeSource AncestorType={x:Type treeList:TreeListView}}}"
[...]
TreeView:
<treeList:TreeListView Template="{StaticResource TreeControlTemplate}"
DataContext="{Binding RootItem.FilteredChildren}" />
This works so far so good.
When the Filter is changing there is a NotifyOfPropertyChange(() => FilteredChildren);
To expand the tree I'm using this code in code-behind:
private ViewModel _viewModel;
public View()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
Tree.DataContextChanged += Tree_DataContextChanged;
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (DataContext is ViewModel viewModel)
{
DataContextChanged -= OnDataContextChanged;
_viewModel = viewModel;
}
}
private async void Tree_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
await Task.Delay(500);
if (_viewModel.ShouldExpandTreesAfterUpdate())
ExpandTree(Tree);
}
private void ExpandTree(DependencyObject parent)
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is TreeListViewItem treeListViewItem)
treeListViewItem.ExpandSubtree();
else
ExpandTree(child);
}
}
This works, but only when I use the Task.Delay
There are a few downsides I am not able to overcome tho:
The Task.Delay seems to be required otherwise the UI is probably not ready. How can I do without?
It would be nice to do this without accessing the ViewModel from View code-behind (optional)
Since I'm binding my ItemsSource the Items property seems to be empty and I'm basicially crawling the entire visual tree just to find the actual tree items. (optional)
How can I do without?
You need to wait until the tree has been refreshed based on the new DataContext one way or another. Using the Dispatcher to execute a delegate at a specified priority is one option, e.g.:
private async void Tree_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
_ = Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, () =>
{
if (_viewModel.ShouldExpandTreesAfterUpdate())
ExpandTree(Tree);
});
}
Related
I opened the question here but we cannot come to the solution for my problem. I decided to create new question as we came to some assumptions and the former question does not refer to the real problem(we thought it is the problem with binding but as you will read on it is not).
In few words I have a ListView with data from list called jointList.
The list is doing well and it has all the data necessary. (I checked it)
On each row of the ListView I put a ToggleSwitch(in xaml) and then I try to do something with each of the switches.
Each switch should correspond to the data from the same row.
I created Toggled event that should apply to all toggleSwitches like this:
private void ToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
foreach (var product in jointList)
{
if (product.IsOn == true)
{
ToggleTest.Text = product.ProductId.ToString(); // this is for testing only, later I would do something with the data retrieved
ToggleTest.Visibility = Visibility.Visible;
}
else
{
ToggleTest.Visibility = Visibility.Collapsed;
}
}
}
But this is making only one toggleSwitch work. It's the switch that corresponds to the last added product to the list ( I am guessing that it is refering to the last Id). The other switches return nothing as if the method was not iterating through the list correctly or as if there was only one switch hooked up.
So, is it possible to get all switches up and running by using just one Toggled event as I attempt to do?
Here's a sample which shows one way.
In this example we have the following Product view model:
public class Product : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (value == _name) return;
_name = value;
OnPropertyChanged();
}
}
So just a single Name-property.
Then we have MainPage where we create a collection of products:
private void FrameworkElement_OnLoaded(object sender, RoutedEventArgs e)
{
var items = new ObservableCollection<Product>();
for (int i = 0; i < 9; i++)
{
items.Add(new Product($"item {i}"));
}
this.Items.ItemsSource = items;
}
And the XAML which creates the view:
<ListView Loaded="FrameworkElement_OnLoaded" x:Name="Items">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="RowContent" Text="{Binding Name}"/>
<ToggleSwitch x:Name="Toggle" Grid.Column="1" Toggled="Toggle_OnToggled"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The result:
Now we want to change the text when user toggles the switch. This is done in Toggle_OnToggled-event handler:
private void Toggle_OnToggled(object sender, RoutedEventArgs e)
{
var toggle = (ToggleSwitch) sender;
var dataContext = ((Grid)toggle.Parent).DataContext;
var dataItem = (Product) dataContext;
dataItem.Name = $"Toggled {toggle.IsOn}";
}
So after a few toggles:
Mikael Koskinen has delivered the answer to my problem.
Most of my code was correct and identical to his solution, apart from the last bit that is OnToggled event handler.
Here is the working andd correct handler:
private void Toggle_OnToggled(object sender, RoutedEventArgs e)
{
var toggle = (ToggleSwitch)sender;
var dataContext = ((Grid)toggle.Parent).DataContext;
var dataItem = (ScheduleList)dataContext;
ToggleTest.Text = dataItem.ProductId;
}
My previous version of handler didn't include the important bit, that is dataContext and dataItem.
It works like a charm now.
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";
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.
In a past few months I've played a lot with the TreeView and now I get to the UI freeze problem. It comes when you have large amount of the items and the data part for those Items are created very quickly but creating TreeViewItems and visualizing those (it must be done on UI thread) takes a time.
Let's take Shell browser and C:\Windows\System32 directory as an example. (I reworked http://www.codeproject.com/Articles/24237/A-Multi-Threaded-WPF-TreeView-Explorer solution for that.) This directory has ~2500 files and folders.
The DataItem and Visual loading are implemented in different threads but as the file and directory info are read quickly it gives no benefit. Application freezes when it creates TreeViewItems and makes those visible.
I've tried:
Set a different DispatcherPriorities for the UI thread when to load items, for example the window was interactive (I was able to move it) with DispatcherPriority.ContextIdle, but then items were loaded really slow..
Create and visualize items in blocks, like 100 items per once, but hat no benefit, the UI thread still were freezing..
My goal is that the application would be interactive while loading those item's!
At the moment I have only one idea how to solve this, to implement my own control which tracks window size, scrollbar position and loads only the items which are visable, but it's not so easy to do that and I'm not sure that at the end performance would be better.. :)
Maybe somebody has idea how to make application interactive while loading bunch of visual items?!
Code:
Complete Solution could be found there: http://www.speedyshare.com/hksN6/ShellBrowser.zip
Program:
public partial class DemoWindow
{
public DemoWindow()
{
InitializeComponent();
this.Loaded += DemoWindow_Loaded;
}
private readonly object _dummyNode = null;
delegate void LoaderDelegate(TreeViewItem tviLoad, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem);
delegate void AddSubItemDelegate(TreeViewItem tviParent, IEnumerable<ItemToAdd> itemsToAdd);
// Gets an IEnumerable for the items to load, in this sample it's either "GetFolders" or "GetDrives"
// RUNS ON: Background Thread
delegate IEnumerable<ItemToAdd> DEL_GetItems(string strParent);
void DemoWindow_Loaded(object sender, RoutedEventArgs e)
{
var tviRoot = new TreeViewItem();
tviRoot.Header = "My Computer";
tviRoot.Items.Add(_dummyNode);
tviRoot.Expanded += OnRootExpanded;
tviRoot.Collapsed += OnItemCollapsed;
TreeViewItemProps.SetItemImageName(tviRoot, #"Images/Computer.png");
foldersTree.Items.Add(tviRoot);
}
void OnRootExpanded(object sender, RoutedEventArgs e)
{
var treeViewItem = e.OriginalSource as TreeViewItem;
StartItemLoading(treeViewItem, GetDrives, AddItem);
}
void OnItemCollapsed(object sender, RoutedEventArgs e)
{
var treeViewItem = e.OriginalSource as TreeViewItem;
if (treeViewItem != null)
{
treeViewItem.Items.Clear();
treeViewItem.Items.Add(_dummyNode);
}
}
void OnFolderExpanded(object sender, RoutedEventArgs e)
{
var tviSender = e.OriginalSource as TreeViewItem;
e.Handled = true;
StartItemLoading(tviSender, GetFilesAndFolders, AddItem);
}
void StartItemLoading(TreeViewItem tviSender, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
{
tviSender.Items.Clear();
LoaderDelegate actLoad = LoadSubItems;
actLoad.BeginInvoke(tviSender, tviSender.Tag as string, actGetItems, actAddSubItem, ProcessAsyncCallback, actLoad);
}
void LoadSubItems(TreeViewItem tviParent, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
{
var itemsList = actGetItems(strPath).ToList();
Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList);
}
// Runs on Background thread.
IEnumerable<ItemToAdd> GetFilesAndFolders(string strParent)
{
var list = Directory.GetDirectories(strParent).Select(itemName => new ItemToAdd() {Path = itemName, TypeOfTheItem = ItemType.Directory}).ToList();
list.AddRange(Directory.GetFiles(strParent).Select(itemName => new ItemToAdd() {Path = itemName, TypeOfTheItem = ItemType.File}));
return list;
}
// Runs on Background thread.
IEnumerable<ItemToAdd> GetDrives(string strParent)
{
return (Directory.GetLogicalDrives().Select(x => new ItemToAdd(){Path = x, TypeOfTheItem = ItemType.DiscDrive}));
}
void AddItem(TreeViewItem tviParent, IEnumerable<ItemToAdd> itemsToAdd)
{
string imgPath = "";
foreach (ItemToAdd itemToAdd in itemsToAdd)
{
switch (itemToAdd.TypeOfTheItem)
{
case ItemType.File:
imgPath = #"Images/File.png";
break;
case ItemType.Directory:
imgPath = #"Images/Folder.png";
break;
case ItemType.DiscDrive:
imgPath = #"Images/DiskDrive.png";
break;
}
if (itemToAdd.TypeOfTheItem == ItemType.Directory || itemToAdd.TypeOfTheItem == ItemType.File)
IntAddItem(tviParent, System.IO.Path.GetFileName(itemToAdd.Path), itemToAdd.Path, imgPath);
else
IntAddItem(tviParent, itemToAdd.Path, itemToAdd.Path, imgPath);
}
}
private void IntAddItem(TreeViewItem tviParent, string strName, string strTag, string strImageName)
{
var tviSubItem = new TreeViewItem();
tviSubItem.Header = strName;
tviSubItem.Tag = strTag;
tviSubItem.Items.Add(_dummyNode);
tviSubItem.Expanded += OnFolderExpanded;
tviSubItem.Collapsed += OnItemCollapsed;
TreeViewItemProps.SetItemImageName(tviSubItem, strImageName);
tviParent.Items.Add(tviSubItem);
}
private void ProcessAsyncCallback(IAsyncResult iAR)
{
// Call end invoke on UI thread to process any exceptions, etc.
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() => ProcessEndInvoke(iAR)));
}
private void ProcessEndInvoke(IAsyncResult iAR)
{
try
{
var actInvoked = (LoaderDelegate)iAR.AsyncState;
actInvoked.EndInvoke(iAR);
}
catch (Exception ex)
{
// Probably should check for useful inner exceptions
MessageBox.Show(string.Format("Error in ProcessEndInvoke\r\nException: {0}", ex.Message));
}
}
private struct ItemToAdd
{
public string Path;
public ItemType TypeOfTheItem;
}
private enum ItemType
{
File,
Directory,
DiscDrive
}
}
public static class TreeViewItemProps
{
public static string GetItemImageName(DependencyObject obj)
{
return (string)obj.GetValue(ItemImageNameProperty);
}
public static void SetItemImageName(DependencyObject obj, string value)
{
obj.SetValue(ItemImageNameProperty, value);
}
public static readonly DependencyProperty ItemImageNameProperty;
static TreeViewItemProps()
{
ItemImageNameProperty = DependencyProperty.RegisterAttached("ItemImageName", typeof(string), typeof(TreeViewItemProps), new UIPropertyMetadata(string.Empty));
}
}
Xaml:
<Window x:Class="ThreadedWpfExplorer.DemoWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ThreadedWpfExplorer"
Title="Threaded WPF Explorer" Height="840" Width="350" Icon="/ThreadedWpfExplorer;component/Images/Computer.png">
<Grid>
<TreeView x:Name="foldersTree">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate DataType="ContentPresenter">
<Grid>
<StackPanel Name="spImg" Orientation="Horizontal">
<Image Name="img"
Source="{Binding
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type TreeViewItem}},
Path=(local:TreeViewItemProps.ItemImageName)}"
Width="20" Height="20" Stretch="Fill" VerticalAlignment="Center" />
<TextBlock Text="{Binding}" Margin="5,0" VerticalAlignment="Center" />
</StackPanel>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
Alternative Loading items in blocks:
private const int rangeToAdd = 100;
void LoadSubItems(TreeViewItem tviParent, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
{
var itemsList = actGetItems(strPath).ToList();
int index;
for (index = 0; (index + rangeToAdd) <= itemsList.Count && rangeToAdd <= itemsList.Count; index = index + rangeToAdd)
{
Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList.GetRange(index, rangeToAdd));
}
if (itemsList.Count < (index + rangeToAdd) || rangeToAdd > itemsList.Count)
{
var itemsLeftToAdd = itemsList.Count % rangeToAdd;
Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList.GetRange((rangeToAdd > itemsList.Count) ? index : index - rangeToAdd, itemsLeftToAdd));
}
}
What you're looking for is known as UI Virtualization and is supported by a number of different WPF controls. Regarding the TreeView in particular, see this article for details on how to turn on virtualization.
One major caveat is that in order to benefit from this feature, you need to use the ItemsSource property and provide items from a collection rather than adding items directly from your code. This is a good idea to do anyway, but it may require some restructuring to get it functional with your existing code.
Why not just create your observable collection and bind to it from xaml?
Check out the MvvM design pattern and you just create a class, and point the xaml at it, in there, from the initialisation, create your list, and then tell the treeview to bind to that list, displaying properties of the each item in your list.
I know this is a little scant on info, but to do MvvM is really easy and just look through stackoverflow and you'll see examples.
You really don't need to call begininvoke on every item - and that's just not from an mvvm point of view - just bind to a list.
You can use indexed 'levels' to your objects too.
Another helpful technique is this regard, is Data Virtualization. There is a good article and sample project on CodeProject, that talks about Data Virtualization in WPF.
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.