Re-evaluating a MultiConverter when a parameter changes? - wpf

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

Related

WPF TabControl: Children unloaded when other tab is selected

Is it possible to prevent WPF TabControl from unloading the children of a TabItem when selecting an other tab?
The problem I'm facing is similar to the one described here:
WPF TabControl - Preventing Unload on Tab Change?
The solution provided there seems to work only if the TabControl is data bound.
It doesn't work if you add TabItems:
<local:TabControlEx>
<TabItem Header="First Tab">
<TreeView ItemsSource="{Binding TreeNodes}" Unloaded="treeView_Unloaded">
<TreeView.Resources>
<DataTemplate DataType="{x:Type local:NodeViewModel}">
<TextBlock Text="{Binding NodeName}" />
</DataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</TabItem>
<TabItem Header="Second Tab">
<TextBlock Name="txText2">Second Text 2</TextBlock>
</TabItem>
</local:TabControlEx>
When you select "Second Tab", treeView_Unloaded is triggered.
Background:
In my real application, one of the TabItems contains a UserControl containing a data bound TreeView: TreeViewItem.IsSelected is bound to a property. Setting the Property bound to IsSelected selects the corresponding TreeViewItem. If the user switches to another tab, the TreeView is unloaded (removed from VisualTree). In that situation, setting IsSelected to true on any node not having a corresponding TreeViewItem causes the TreeView to misbehave - probably because the TreeView does not create a TreeViewItem for the node which should be selected, because the TreeView is not currently part of the visual tree. So what I want to achieve is that I can select any other node by setting IsSelected to true - even if the TreeView is currently on a non-visible tab. Moving the TreeView to some place outside the TabControl seems to resolve the problem - even if it is inside a panel which has visibility collapsed. So, visibility doesn't seem to be the problem, but the fact that the TreeView is not currently part of the visual tree.
The solution presented in the original question works if you override the control template based on the default control template of TabControl:
- Remove ContentPresenter
- Add a Grid named PART_ItemsHolder.
No more Unloaded events - no more trouble with the TreeView when it is on a non-selected TabItem.

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.

XAML Control array for static number of combo boxes

I would like an array of 10 combo boxes on a wpf form.
The ItemsSource of the combo boxes are identical - an ObservableCollection of selectable Items.
Each Selected Item will bound to an item in a different ObservableCollection, imaginatively called 'SelectedItems'..
What is the best way to do the array? I could of course have 10 separate combo boxes but this would be not very elegant..
I don't think an ItemsControl template is what I'm after as the number of combo boxes is static.
Thanks
Joe
If I understand you right, you have 10 ComboBoxes with the same item list, but different data sources
In that case, I could create a common style for the ComboBox which sets the common properties such as ItemsSource (and SelectedItem if the binding is the same for all items), and then actually create the individual ComboBoxes on the form as needed.
<StackPanel>
<StackPanel.Resources>
<Style TargetType="{x:Type ComboBox}">
<!-- Set the binding to wherever your ItemsSource resides. In this
case,I'm binding to a static class called Lists and a static
property called ComboBoxItems -->
<Setter Property="ItemsSource"
Value="{Binding Source={x:Static local:Lists.ComboBoxItems}}" />
<!-- Only use this setter if your binding is the same everywhere -->
<Setter Property="SelectedItem" Value="{Binding SelectedItem}" />
</Style>
</StackPanel.Resources>
<ComboBox DataContext="{Binding Item1}" />
<ComboBox DataContext="{Binding Item2}" />
<ComboBox DataContext="{Binding Item3}" />
<ComboBox DataContext="{Binding Item4}" />
<ComboBox DataContext="{Binding Item5}" />
<ComboBox DataContext="{Binding Item6}" />
<ComboBox DataContext="{Binding Item7}" />
<ComboBox DataContext="{Binding Item8}" />
<ComboBox DataContext="{Binding Item9}" />
<ComboBox DataContext="{Binding Item10}" />
</StackPanel>
Of course, if the DataSource for your ComboBoxes CAN be put in a collection, it is preferred that they are and that you use an ItemsControl to display the ComboBoxes
<ItemsControl ItemsSource="{Binding SelectedItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox SelectedItem="{Binding }"
ItemsSource="{Binding Source={x:Static local:Lists.ComboBoxItems}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Each Selected Item will bound to an item in a different ObservableCollection, imaginatively called 'SelectedItems'..
Given that you're effectively binding to a collection, I would do an ItemsControl template, and just treat it that way. Unless you want to customize the layout (ie: these won't be arranged together in the View), that will simplify the design, even if the number of items is always "static".
If you want to have the items arranged separately on the View, then just having 10 combo boxes may be more appropriate.
Personally I think an ItemsControl which has an ItemTemplate which constructs each ComboBox is the way to go! Are you always going to have exactly 10 of these?
From an MVVM perspective, I can imagine a parent View Model which has a collection of selection view models. Each selection view model will have the list of items that can be selected, and the currently selected item. This view model will easily bind to your view.
Not knowing why you need this... which would probably be important in deciding how to do this... here is my stab at it.
Why not design the UI, give each ComboBox a name, create a List and Add each into that List at runtime?

WPF ListBoxItem Visibility and ScrollBar

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?

Resources