WPF ListBoxItem Visibility and ScrollBar - wpf

I was hoping to collapse certain ListBoxItems based on a property of their data context.
I came up with the following (trimmed for brevity)
<ListBox ItemsSource="{Binding SourceColumns}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsDeleted}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock VerticalAlignment="Center" Margin="5,0" Text="{Binding ColumnName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This "works" in that it does collapse the listboxitems that are marked as "IsDeleted", however the vertical scrollbar does not adjust for the "missing" items. As I'm scrolling, all of a sudden the bar gets bigger and bigger (without moving) until I scroll past the point of the hidden items, and then finally starts to move.
I also tried explicitly setting the height and width to 0 as well in the data trigger, to no avail.
Does anyone know if there's a workaround for this?

Enter CollectinViewSource
One thing you can do is connect your ListBox to your items through a CollectionViewSource.
What you do is create the collectionViewSource in XAML:
<Window.Resources>
<CollectionViewSource x:Key="cvsItems"/>
</Window.Resources>
Connect to it in your CodeBehind or ViewModel
Dim cvsItems as CollectionViewSource
cvsItems = MyWindow.FindResource("cvsItems")
and set it's source property to your collection of items.
cvsItems.Source = MyItemCollection
Then you can do filtering on it. The collectionViewSource maintains all of the items in the collection, but alters the View of those items based on what you tell it.
Filtering
To filter, create a CollectionView using your CollectionViewSource:
Dim MyCollectionView as CollectionView = cvsItems.View
Next write a filtering function:
Private Function FilterDeleted(ByVal item As Object) As Boolean
Dim MyObj = CType(item, MyObjectType)
If MyObj.Deleted = True Then Return False Else Return True End If
End Function
Finally, write something that makes the magic happen:
MyCollectionView .Filter = New Predicate(Of Object)(AddressOf FilterDeleted)
I usually have checkboxes or Radiobuttons in a hideable expander that lets me change my filtering options back and forth. Those are bound to properties each of which runs the filter function which evaluates all the filters and then returns whether the item should appear or not.
Let me know if this works for you.
Edit:
I almost forgot:
<ListBox ItemsSource="{Binding Source={StaticResource cvsItems}}"/>

The answer is to set the VirtualizingStackPanel.IsVirtual="False" in your listbox.
Why don't my listboxitems collapse?

Related

WPF: how to recreate ItemContainer?

Following my previous question How to change ComboBox item visibility, since the problem is slightly changed i decided to open a new post to solve the it. For those who don't want to read all the comments on the previous post, here is the situation.
I have a DataGrid that is generated at run time. Each column of this datagrid have a combobox inside the header. All those comboboxes have the same Source, that is an observable collection of a class item. Every item show a property that i use in the ItemContainerStyle of the comboboxes, to decide whether each ComBoBoxItem should be Visible or not.
Now, as far as i know, WPF work this way : if a view contain controls like combobox or treeview, then their items (i.e. ComboBoxItem, TreeViewItem...), won't be generated until it's not necessary (for example when the dropdown of a combobox is opened). If i apply an ItemContainerStyle, this will tell to the target how its items should be created. The problem is, that at the moment that this items are generated, every change i need to apply to the style, won't be saved.
Here is my code:
<DataGrid HeadersVisibility="Column" Name="griglia" Grid.Row="2" ItemsSource="{Binding Path=Test}" AutoGenerateColumns="True" IsReadOnly="True" ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Visible" ScrollViewer.HorizontalScrollBarVisibility="Visible">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="ContentTemplate" >
<Setter.Value>
<DataTemplate DataType="DataGridColumnHeader" >
<ComboBox ItemContainerStyle="{StaticResource SingleSelectionComboBoxItem}" DisplayMemberPath="Oggetto" Width="100" Height="20" ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},Path=DataContext.Selezione, UpdateSourceTrigger=LostFocus}" SelectionChanged="SingleSelectionComboBox_SelectionChanged">
</ComboBox>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.ColumnHeaderStyle>
</DataGrid>
ItemContainerStyle:
<Style x:Key="SingleSelectionComboBoxItem" TargetType="ComboBoxItem" BasedOn="{StaticResource {x:Type ComboBoxItem}}">
<Style.Triggers>
<DataTrigger Binding="{Binding Selezionato}" Value="True">
<!-- Hide it -->
<Setter Property="Visibility" Value="Collapsed" />
<!-- Also prevent user from selecting it via arrows or mousewheel -->
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
SelectionChanged:
private void SingleSelectionComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var item in e.RemovedItems.OfType<PopolazioneCombo>())
{
item.Selezionato = false;
}
foreach (var item in e.AddedItems.OfType<PopolazioneCombo>())
{
item.Selezionato = true;
}
}
My requirement is that, if in any of the N combobox an item is selected, then, that item cannot be selected by anyone, until he lose the SelectedItem status. For instance let's assume i have 2 combobox and a collection of 4 Item (x,y,a,b) . If x is selected in ComboBox1, then x can't be selected in none of the 2 ComboBox until the SelectedItem of the ComboBox1 change (from x to y for instance). Now i can even accept the fact that the item in the dropdown is just disabled if it makes the things easier, i just need the fact that it cannot be selected again if he is already selected.
The problem is that this solution work for every ComboBox that has its ItemContainerGenerator.Status = NotStarted (this means that the ComboBoxItem are still not created). If i open the dropdown of a combobox, then its ComboBox items will retain their style no matter what i do (cause ItemContainerGenerator.Status = ContainersGenerated), while the combobox that i didn't opened keep track of the changes in the visibility of the items.
I'm Looking for a solution to recreate these items do that the new style with the changes in the visibility will be applied
after a lucky research on internet (i.e. i've found the correct combination of keyword), i found this wonderful method, so here the updated solution:
private void ComboBox_DropDownOpened(object sender, EventArgs e)
{
ComboBox c = sender as ComboBox;
c.Items.Refresh();
}
add this event in the xaml so that it became:
<ComboBox DropDownOpened="ComboBox_DropDownOpened" ItemContainerStyle="{StaticResource SingleSelectionComboBoxItem}" DisplayMemberPath="Oggetto" Width="100" Height="20" ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},Path=DataContext.Selezione, UpdateSourceTrigger=LostFocus}" SelectionChanged="SingleSelectionComboBox_SelectionChanged"/>
and magically the combobox show the correct changes. Now i'm not a big fan of code behind, so if there is a way to do this adding a property to my Collection, or via xaml it would be better

Listbox "IsSelected" binding only partially working

I have a ListBox that I populate dynamically via a binding (this is defined in a DataTemplate, which is why the binding is somewhat unusual):
<ListBox SelectionMode="Extended" ItemsSource="{Binding DataContext.ResultList, RelativeSource={RelativeSource AncestorType=Window}}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Object}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Each ListBoxItem's IsSelected property is bound to an IsSelected property on a custom object.
When I select individual ListBoxItems, the binding works properly - the custom object's IsSelected property is updated in my ViewModel. However, if I select all of the ListBoxItems with a Ctrl+A command, only the currently visible ListBoxItems (those that are currently in my scrolling viewport) update their ViewModel bindings. On the frontend, all the ListBoxItems appear to be selected, and the ListBox.SelectedItems.Count property on the container ListBox shows that all items are selected.
Furthermore, as I scroll through the ListBox after selecting all ListBoxItems with Ctrl+A, the bindings are successfully updated when each ListBoxItem is scrolled into view.
Why does this binding seem to be only partially working? Is there a better way to handle the binding of the IsSelected property when large numbers of ListBoxItems can be selected simultaneously?
Edit:
This behavior doesn't happen exclusively with the Ctrl+A command - I get the same results when selecting all the items using a shift+click.
I think the behavior you're seeing is to due to VirtualizingStackPanel.IsVirtualizing which is True by default when binding to ItemsSource of ListBox
if you for eg set your ListBox such as:
<ListBox VirtualizingStackPanel.IsVirtualizing="False" SelectionMode="Extended" ItemsSource="{Binding DataContext.ResultList, RelativeSource={RelativeSource AncestorType=Window}}">
or
<ListBox ...>
...
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
then you should see all your bound items have their IsSelected updated accordingly with Ctrl+A or Shift + ...
Properties such as Count of the collection even with virtualization would report the correct value to accommodate for things like computing the required ScrollBar.Height. Items which are outside the View-port do not get rendered hence no bindings are in effect on them until they actually get used.

How to bind to Tabcontrol.Items

I have a WPF application that I'm trying to dynamically add items to a tabcontrol. I have a list of menu items that should be databound to the tabcontrol's items. The only problem is that TabControl.Items does not notify others that items have been added. I've tested this by binding instead to TabControl.Items.Count and get calls to the converter (but the value passed in is the count and not something useful). Here's the relevent code that doesn't get databound properly because Items doesn't call out updates:
<MenuItem ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TabControl}}, Path=Items, Converter={StaticResource TabControlItemConverter}}">
This MenuItem XAML is inside a ControlTemplate for a TabControl. With static items, i.e., items that are already defined in a TabControl, this code works perfectly. But I have a TabControl that gets items added at runtime and can't seem to update this binding. Has anyone added some sort of attached property to a TabControl that can bind to the Items collection?
Edit for background info
The TabControl that has items added to it is a region (this is a Prism application). Here is the relevent XAML
<TabControl cal:RegionManager.RegionName="{x:Static local:LocalRegionNames.SelectedItemRegion}" >
<TabControl.Resources>
<Style TargetType="TabItem" BasedOn="{StaticResource TabItemStyle}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Style="{StaticResource tabItemImage}" Height="20" />
<TextBlock Text="{Binding Content.DataContext.TabHeader, RelativeSource={RelativeSource AncestorType=TabItem}}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.Resources>
</TabControl>
The relevent code for adding a view to the region is here:
ProjectDetailView view = new ProjectDetailView();
ProjectDetailViewModel viewModel = new ProjectDetailViewModel();
viewModel.CurrentProject = project;
view.DataContext = viewModel;
IRegionManager retManager = RegionManager.Regions[LocalRegionNames.SelectedItemRegion].Add(view, null, true);
RegionManager.Regions[LocalRegionNames.SelectedItemRegion].Activate(view);
All this works fine...views get added, the tab control adds items, and views appear. But the Items property on the tabcontrol never broadcasts the changes to its collection.
You do the same thing for TabControls, you bind the ItemsSource, the only thing you need to take into account is that the source collection should implement INotifyCollectionChanged if you want it updated if items are added. ObservableCollection<T> already implements the interface and is often used as source for such bindings.

Re-evaluating a MultiConverter when a parameter changes?

I have a listbox representing a SQL WHERE statement and the items inside can be grouped.
For example, a user can have the following items in their ListBox
I would like to show the AND/OR value only if the ListBox item is not the first item in the listbox or a group (like example). I am currently using a MultiConverter on the ListBox's ItemTemplate that accepts the ListBox's ItemSource and the Current Item as parameters, however existing items do not get their AND/OR visibility updated when the user adds a new item, or drags an existing item to a new spot in the listbox.
Is there a way to tell the MultiConverter to reevaluate when one of its parameters, the ListBox's ItemSource, changes? I am using MVVM and the ListBox is bound to an ObservableCollection of items.
Update
Code as requested by Adam...
<ListBox x:Name="WhereList" ItemsSource="{Binding Path=CurrentQuery.WhereList}">
<ListBox.Style>
<Style TargetType="{x:Type ListBox}">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch"
Margin="{Binding Path=Depth, Converter={StaticResource QueryBuilder_DepthToMarginConverter}}">
<Label Content="{Binding Path=BooleanComparison}" Padding="0">
<Label.Visibility>
<MultiBinding Converter="{StaticResource ShouldShowOperatorConverter}">
<Binding ElementName="WhereList" Path="ItemsSource"/>
<Binding />
</MultiBinding>
</Label.Visibility>
</Label>
<Label Content="{Binding ConditionText}" Padding="0" HorizontalAlignment="Stretch" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.Style>
</ListBox>
The MultiConverter accepts a list of items, and the current item. It checks to see if the item is the first item in the list, or if the previous item in the list is a GroupStart item. If either of these conditions are true, it returns Visibility.Collapsed, otherwise it returns Visibility.Visible.
It works fine for the first load, and changes made to a single item (dragging a new item into the listbox, or dragging an existing item to a new location in the listbox) will correctly update the new item's AND/OR visibility, however it does not change any other item then the one being added/moved. So if you drag a new item to the top of the list, it will correctly hide the AND/OR of the new item, however it will not update the 2nd item (former first item) to show the AND/OR. This really affects the readability of the items in the list and prevents the user from seeing if they are currently linking the item with an AND or an OR which makes a big difference in the results returned.
I'm fairly sure it has something to do with the fact I'm using a MultiConverter since my DepthToMarginConverter works fine (for example, grouping items correctly updates the margin of all items within the group).
You need to raise the PropertyChanged event on the ListBox's ItemsSource in the ViewModel. If your ViewModel base class has a RaisePropertyChanged method, or some other INOtifyPropertyChanged helper, raise that on your collection - this should cause the ListBox to refresh it's data, and run it through the converters again.
I can't figure out a way to get my MultiConverter to refresh when one of the parameters change (yes it implements INotifyPropertyChange) so I ended up just adding a property to my item of IsBooleanOperatorShown and using a regular BooleanToVisibility converter

Strange behaviour in the WPF DataTemplate and property.

I have a DataTemplate that displays buttons in a StackPanel. Whenever a user clicks a button, the button is supposed to glow. So I have written the necessary DataTrigger in the Template and a Boolean condition in the property that I'm binding too. Here are the details below:
<DataTemplate x:Key="ActionItemsTemplate" DataType="ActionItemViewModel">
<ItemsControl IsTabStop="False" ItemsSource="{Binding}" Margin="6,2">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button x:Name="ActionButton" Command="{Binding Path=Command }" Content="{Binding Path=DisplayName}" Style="{DynamicResource HeaderButton}"/>
<!-- Set special values for Selected Item -->
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="ActionButton" Property="Style" Value="{DynamicResource MainWindowSelectedButton}"/>
<!--Command="{Binding Path=Command}" Content="{Binding Path=DisplayName}"-->
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
I have implemented the INotifyPropertyChanged interface and the Property ActionItems returns an ObservableCollection.
The Problem: When I change the ObservableCollection and call the INotifyPropertyChanged event, it is not reflected in the DataTemplate directly, but only altering the property. However if I re-assgn the entire object with itself, it works perfectly.
e.g
void Test1()
{
_commands[0].IsSelected = !_commands[0].IsSelected;
_commands[0] = _commands[0]; // Does not work If this line is commented out
ActionItems = _commands;
}
What could the problem be?
Edit: I see that the problem could be with the DataBinding in this case.
I have arrived at a similar problem now where I have bound the IsExpanded property of the Expander control to a bool property inside a TabPanel. When I toggle the bool property, the value is changed behind, but not reflected in the display. However suppose I change tabs and come back, I see the change has taken place. Is this related to this problem here?
And again I wonder what the problem could be (narrowed it down a bit :))
Edit 2: Solution for second Problem: I found that the OnPropertyChangedEvent of the INotifyPropertyChanged Interface needs to be called whenever a programmatic update on the
IsExpanded property is updated. As for the Original Problem, this does not seem to be the case, and I'm still trying to figure out what is going wrong. :)
I'm led to believe that you cannot replace the collection, only alter its contents.
a Boolean condition in the property that I'm binding too
I am not completely sure what you mean here, but I take the guess that this is the IsSelected property on your "commands" and that you forgot to raise PropertyChanged for that property.

Resources