I am trying to make a WPF listbox replicate the behaviour of an old Winforms CheckedListBox, or the checked list box used in e.g. AnkhSVN. I have seen examples that show how to use a DataTemplate to create a check box for every time (e.g. Wpf CheckedListbox - how to get selected item), but this feels very clunky compared to the winforms control:
The logic of "If the user changes a check state, ensure that check state changes for all selected items" is not present by default.
The hit area to change an item from checked to unchecked is the box /and/ the title, rather than just the box as in Winforms
I can handle the first issue by adding a listener to the PropertyChanged event on each item in the bound collection, and if IsChecked changes, then set IsChecked to the same value for all currently selected items.
However, I cannot find a good solution to the second issue. By splitting the DataTemplate into a Checkbox with no title, and a TextBlock with the title, I can reduce the hit area to change the check state to only the desired square. However, all mouse interaction which hits the TextBlock does nothing - I would like it to behave the same as in a normal listbox, or in the dead space outside of the Textblock: If the user is holding shift, then select everything up to and including this item, if not, then clear the selection and select only this item. I could try to implement something where I handled Mouse* events on the TextBlock, but that seems brittle and inelegant - I'd be trying to recreate the exact behaviour of the ListBox, rather than passing events to the listbox.
Here's what I've got currently:
XAML:
<ListBox x:Name="_lstReceivers" SelectionMode="Extended" Margin="10,41,6,15"
ItemsSource="{Binding Receivers}">
<ListBox.ItemTemplate>
<DataTemplate>
<ListBoxItem>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsChecked}" IsHitTestVisible="True"/>
<TextBlock Text="{Binding Item}" Background="{x:Null}" IsHitTestVisible="False"/><!--Attempt to make it pass mouse events through. Doesn't work. Yuk.-->
</StackPanel>
</ListBoxItem>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Code behind to get the "Change all checks at the same time" logic (removed some error handling for clarity):
private void ListBoxItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var item = sender as CheckableItem<Receiver>;
if (item == null)
return;
if (e.PropertyName == nameof(CheckableItem<Receiver>.IsChecked))
{
bool newVal = item.IsChecked;
foreach (CheckableItem<Receiver> changeItem in _lstReceivers.SelectedItems)
{
changeItem.IsChecked = newVal;
}
}
}
By trying various combinations of Background = "{x:Null}" and IsHitTestVisible="False", I did manage to get the entire item to not respond to mouse click events - but I could not make it have only the Checkbox respond to mouse events, while everything else is passed to the ListBox for proper selection processing.
Any help would be greatly appreciated.
Answering my own question again.
Well, I couldn't find a clean way to do it, so I ended up setting the ListBoxItem to have IsHitTestVisible="False", and manually tracing mouse events using PreviewMouseDown.
Final code:
XAML:
<ListBox x:Name="_lstReceivers" SelectionMode="Extended" Margin="10,41,6,15"
ItemsSource="{Binding Receivers}" PreviewMouseDown="_lstReceivers_MouseDown">
<ListBox.ItemTemplate>
<DataTemplate>
<ListBoxItem IsSelected="{Binding IsSelected}" IsHitTestVisible="False">
<StackPanel Orientation="Horizontal" Background="{x:Null}">
<CheckBox IsChecked="{Binding IsChecked}" IsHitTestVisible="True" Checked="CheckBox_Checked" Unchecked="CheckBox_Checked"/>
<TextBlock Text="{Binding Item}" Background="{x:Null}" IsHitTestVisible="False"/>
</StackPanel>
</ListBoxItem>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Code behind:
//Logic to handle allowing the user to click the checkbox, but have everywhere else respond to normal listbox logic.
private void _lstReceivers_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
Visual curControl = _lstReceivers as Visual;
ListBoxItem testItem = null;
//Allow normal selection logic to take place if the user is holding shift or ctrl
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl) || Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
return;
//Find the control which the user clicked on. We require the relevant ListBoxItem too, so we can't use VisualTreeHelper.HitTest (Or it wouldn't be much use)
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(curControl); i++)
{
var testControl = (Visual)VisualTreeHelper.GetChild(curControl, i);
var rect = VisualTreeHelper.GetDescendantBounds(testControl);
var pos = e.GetPosition((IInputElement)curControl) - VisualTreeHelper.GetOffset(testControl);
if (!rect.Contains(pos))
continue;
else
{
//There are multiple ListBoxItems in the tree we walk. Only take the first - and use it to remember the IsSelected property.
if (testItem == null && testControl is ListBoxItem)
testItem = testControl as ListBoxItem;
//If we hit a checkbox, handle it here
if (testControl is CheckBox)
{
//If the user has hit the checkbox of an unselected item, then only change the item they have hit.
if (!testItem.IsSelected)
dontChangeChecks++;
((CheckBox)testControl).IsChecked = !((CheckBox)testControl).IsChecked;
//If the user has hit the checkbox of a selected item, ensure that the entire selection is maintained (prevent normal selection logic).
if (testItem.IsSelected)
e.Handled = true;
else
dontChangeChecks--;
return;
}
//Like recursion, but cheaper:
curControl = testControl;
i = -1;
}
}
}
//Guard variable
int dontChangeChecks = 0;
//Logic to have all selected listbox items change at the same time
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
if (dontChangeChecks > 0)
return;
var newVal = ((CheckBox)sender).IsChecked;
dontChangeChecks++;
try
{
//This could be improved by making it more generic.
foreach (CheckableItem<Receiver> item in _lstReceivers.SelectedItems)
{
item.IsChecked = newVal.Value;
}
}
finally
{
dontChangeChecks--;
}
}
This solution works, but I don't like the coupling it introduces between my code and the exact behaviour of the ListBox implementation:
Checking the Keyboard state
It won't handle dragging if the user starts dragging inside a checkbox
It should happen on mouseup, not mousedown. But it's close enough for my needs.
PS: The bound class, even though it's irrelevant and obvious what it would have:
public class CheckableItem<T> : INotifyPropertyChanged
{
public T Item { get; set; }
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value)
return;
_isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
}
}
private bool _checked;
public bool IsChecked
{
get => _checked;
set
{
if (_checked == value)
return;
_checked = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsChecked)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Related
I have a DataGrid with CheckBox-type column which should be editable with single click. This is easily achieved by using template column with CheckBox inside:
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Path=IsSelected, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<CheckBox BackGround="Red" IsChecked="{Binding Path=IsSelected, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
the problem, however, is that single clicking will change the value without ever entering Edit mode. How can I ensure the edit mode is entered before changing the CheckBox value (all will single click)?
My best attempt on the problem was setting PreviewMouseLeftButtonDown on DataGridCell through style and forcing BeginEdit(). While this does begin edit, it is back to needing double click to interact.
private void DataGridCell_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is DataGridCell cell && !cell.IsEditing && e.OriginalSource is DependencyObject source)
{
var checkBoxParent = VisualExtensions.GetVisualParent<CheckBox>(source);
if (checkBoxParent != null) // ensure CheckBox was clicked
{
cell.Focus();
ItemListDG.BeginEdit();
}
}
}
I have also tried handling Selected or GotFocus without any luck (breaks other types of interaction), CheckBox.Checked events cannot be used neither because they trigger on re/load.
In case of Selected event, the problem is that it enables single click edit on all columns even though it is handled on just one column (again, set through style):
private void DataGridCell_Selected(object sender, RoutedEventArgs e)
{
// second part of condition is always true, no matter what cell is clicked
if (sender is DataGridCell cell && cell.Column == ItemListDG.Columns[0])
{
// try to detect if the CheckBox column was clicked, if not return
if (sender != e.OriginalSource) // always false
return;
ItemListDG.BeginEdit(e); // always executes no matter the source
}
}
The event which should be handled is PreviewMouseLeftButtonUp, same like the case with Down event, after BeginEdit() is called the original CheckBox does not exist anymore and cannot handle the un/checking. The solution to doubleclick problem is to find the new representation of the CheckBox and either re-raise the event threre or toggle it manually:
private void DataGridCell_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (sender is DataGridCell cell && !cell.IsEditing && e.OriginalSource is UIElement source)
{
var actualSource = source is CheckBox ?
(CheckBox)source : VisualExtensions.GetVisualParent<CheckBox>(source);
if (actualSource != null)
{
ItemListDG.BeginEdit();
var newSource = cell.GetVisualChild<CheckBox>();
if (newSource != null)
{
newSource.IsChecked = !newSource.IsChecked;
}
}
}
}
I am having issues with a ComboBoxselecting the first entered character, which then causes the a problem where the second entered character overwrites the first one.
EDIT: A small explanation of what I an trying to do.
I have set up the ComboBox to act as an autocomplete control. When I enter a character, I am using CollectionView class to filter any names that match each entered character. Upon entered text the ComboBox drop down menu needs to open up, which is why I am binding to IsDropDownOpen. This is how it is supposed to look here.
This is beyond me, I can't research what I need to do to stop this behavior.
Here is a screen shot of what I mean.
This is the ComboBox XAML:
<ComboBox Style="{StaticResource ComboBoxToggleHidden}"
DisplayMemberPath="FullName" SelectedValuePath="Key"
IsTextSearchEnabled="False"
IsEditable="True"
StaysOpenOnEdit="True"
Text="{Binding Path=EnteredText, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding Path=Employees}"
SelectedItem="{Binding UpdateSourceTrigger=PropertyChanged, Path=SelectedEmployee}"
IsDropDownOpen="{Binding IsDropDown}">
</ComboBox>
EDIT: I have narrowed it down to this, IsDropDown = true;, commenting this out fixes the issue. But I need the drop down when editing the ComboBox
In the EnteredText property
private string _enteredText;
public string EnteredText
{
get { return _enteredText; }
set
{
_enteredText = value;
Filter(value);
IsDropDown = true;
OnPropertyChanged();
}
}
public bool IsDropDown { get; set; }
OK, I solved this doing a hack, but it will have to do until I can figure out why this behavior is happening.
I created an KeyUpEvent Event in the constructor,
EventManager.RegisterClassHandler(typeof(TextBox), TextBox.KeyUpEvent,
new RoutedEventHandler(DeselectText));
Then in the Handler I just deselected the text.
private void DeselectText(object sender, RoutedEventArgs e)
{
var textBox = e.OriginalSource as TextBox;
if (textBox == null) return;
if (textBox.Text.Length >= 2) return;
textBox.SelectionLength = 0;
textBox.SelectionStart = 1;
}
I know this is a hack, but I have no choice until the correct solution is posted.
This is how it looks with the hack.
Consider this as another solution. It mights resove the side effect of above mentioned problem.
I am expecting the TemplateChild to have the Name (PART_EditableTextBox). If you are changing the name in the Template then please do the necessary changes here as well.
private TextBox EditableTextBox => (TextBox)GetTemplateChild("PART_EditableTextBox");
protected override void OnDropDownOpened(EventArgs e)
{
EditableTextBox.Select(Text.Length, 0);
base.OnDropDownOpened(e);
}
I have a Combobox in WPF, I have set Is Editable="true" which allows me enter any text in the combobox. I would like to restrict users from entering text outside datasource.
Xaml:
<ComboBox Name="service" Margin="0,0,0,4"
IsEditable="True"
Grid.Column="1"
Grid.ColumnSpan="2" Grid.Row="4"
SelectedValuePath="Id"
DisplayMemberPath="Service"
SelectedValue="{Binding Controller.Service1}"
ItemsSource="{Binding}" />
C#:
System.Data.DataView vw = tableAdapterServices.GetData().DefaultView;
service.ItemsSource = vw;
service.SelectedIndex = 0;
I do not want to allow users to enter text which is not present in the datasource, or handle it if the user enters any other text.
Update:
Thanks for the solution #Vishal, LostFocus event is handling the issue, but it gave rise to another issue. I have a button which is used to submit the combobox value along with other textbox values to the server. I am setting default value in the combobox in lostfocus event. But I need to prevent the button click event if some value other that datasource value is added in combobox.
You can check for selectedIndex in Lostfocus event :
private void ComboBox_LostFocus(object sender, EventArgs e)
{
if(((ComboBox)sender).SelectedIndex == -1)
{
//Text entered by user is not a part your ItemsSource's Item
SaveButton.IsEnabled = false;
}
else
{
//Text entered by user is a part your ItemsSource's Item
SaveButton.IsEnabled = true;
}
}
You can try handling the ComboBox's TextInput or PreviewTextInput events, doing the text search yourself, selecting the most appropriate item, and setting "e.Handled = true."
This works for a single character (i.e. if you enter the letter "j", it will select the first item that contains a "j" or "J"), but I'm sure there's a way to do this with your control. Just include a little more logic to achieve this.
private void MyComboBox_PreviewTextInput(object sender, TextCompositionEventArgs e) {
foreach (ComboBoxItem i in MyComboBox.Items) {
if (i.Content.ToString().ToUpper().Contains(e.Text.ToUpper())) {
MyComboBox.SelectedItem = i;
break;
}
}
e.Handled = true;
}
Ok, from what I understand the behaviour of the combobox should disregard an inserted character if there is no item in the datasource that contains the resulted string.
I believe you are doing it in a MVVM style since you are using binding. What you can do is to bind the ComboBox text to a property and the PreviewTextInput event to a command and do the filtering there.
XAML:
<ComboBox Name="service"
Margin="0,0,0,4"
IsEditable="True"
Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="4"
SelectedValuePath="Id"
DisplayMemberPath="Service"
SelectedValue="{Binding Controller.Service1}"
ItemsSource="{Binding}"
Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
>
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewTextInput">
<cmd:EventToCommand Command="{Binding TextInputCommand}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Combobox>
C# ViewModel:
public RelayCommand<object> TextInputCommand { get; set; }
public bool CanExecuteTextInputCommand(object param)
{
return true;
}
public void ExecuteTextInputCommand(object param)
{
TextCompositionEventArgs e = param as TextCompositionEventArgs;
string currentText = this.Text;
string entireText = string.Format("{0}{1}", currentText, e.Text);
var item = this.Items.Where(d => d.StartsWith(entireText)).FirstOrDefault();
if (item == null)
{
e.Handled = true;
this.Text = currentText;
}
}
Where Items is the ObservableCollection containing the items (in this case it's a list of strings) and Text is the property binded to the Combobox text.
EDIT: Ok so what you need to do to make it work is to go to your project, right click on References, choose Manage NuGet Packages, search and install MVVM Light. Two dlls that start with GalaSoft will be added to your references. After this, in your xaml code add these namespaces:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
What this allows you to do is to bind an event to a ICommand object.
I have a text box that will retain the last 10 entries entered, similar to the search box in Internet Explorer. The user can click on the dropdown menu to see last 10 entries. The drop down menu is a combo box. I created an Observable collection of strings that is bound to the combo box Itemssource.Below is the code.
Xaml
<Grid x:Name="TextBox_grid" Margin="0,0,40,0" Width="360" Height="23">
<ComboBox Name="cb" Margin="0,0,-29,0" Style="{DynamicResource Onyx_Combo}" ItemsSource="{Binding TextEntries, ElementName=TheMainWindow, Mode=OneWay}" IsEditable="False" Visibility="Visible" />
<Rectangle Fill="#FF131210" Stroke="Black" RadiusX="2" RadiusY="2"/>
<TextBox Name=UniversalTextBox Margin="0" Background="{x:Null}" BorderBrush="{x:Null}" FontSize="16" Foreground="#FFA0A0A0" TextWrapping="Wrap" PreviewKeyDown="TextBox_PreviewKeyDown"/>
</Grid>
Code
public partial class Window1 : Window
{
private ObservableCollection<string> m_TextEntries = new ObservableCollection<string>();
public Window1()
{
InitializeComponent();
}
public ObservableCollection<string> TextEntries
{
get { return m_TextEntries; }
}
private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
TextBox textBox = sender as TextBox;
if (textBox == null)
return;
if (e.Key == Key.Enter)
{
PopulateHistoryList(textBox.Text);
e.Handled = true;
}
if (e.Key == Key.Escape)
{
e.Handled = true;
}
}
private void PopulateHistoryList(string text)
{
m_TextEntries.Add(text);
}
private event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
The above code will populate the TextEntries collection when the Enter Key is pressed on the textbox. I need two things
How do I set the Selected Item of the combo box and how can I bind that to my text box.
The combobox(dropmenu) should only show the last 10 entries from the drop down menu.
Thanks in advance,
Using Expesssion Blend, binding the value of a property of one control to the property value of another control is easy, and it is known as ElementProperty Binding, here is a screenshot of where you access the ability to create this within Blend, note that the Textbox is the selected element in the Objects and Timeline panel, and it's the 'little box' to the right of the Text property in the properties panel which has been clicked to produce the context menu pictured...
Once you have selected 'Element Property Binding' for the text property of your textbox, your cursor will become a little bullseye icon, which you now will use to indicate what you want to bind to, by clicking it in either the design canvas or the Objects and Timeline panel, while the cursor appears that way...
Here we see the 'SelectedValue' property of the combo box being selected as the source of what is displayed in the textbox. Once done, the textbox will automagically be immediately set to display whatever is selected in the combo. Be sure to take a look at what Blend is doing in your XAML when you do this, as it will help you better understand what is actually going on, and might even teach you a thing or two about the binding syntax of XAML.
As for the list only ever having the last ten entries...there are several ways to do this, each one more or less appropriate, depending on the surrounding context, but here is one way; simply run a procedure similar to this one, whenever the box has entries added to it:
// assuming 'listItems' is your ObservableCollection
string[] items = listItems.ToArray();
// prepare a new array for the current ten
string[] tenItems = new string[10];
// copy a subset of length ten, to the temp array, the set your ObservableCollection to this array.
Array.Copy(items, (items.Length - 10), tenItems, 0, 10);
Note: The Array .Copy assumes the only way items are getting added to the observable collection is by some form of .Add, which always adds them to the end of the list...
Part of the answer
<TextBlock Text="{Binding ElementName=cb, Path=SelectedValue}" />
<ComboBox x:Name="cb" ItemsSource="{Binding Path=Fields}" SelectedValue="{Binding Path=SelectedValue}" />
And if you set the datacontext of the window
DataContext="{Binding RelativeSource={RelativeSource self}}">
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.