Filter WPF TreeView using MVVM - wpf

I currently have a TreeView which has the following structure:
<TreeView ItemsSource="{Binding RootViewModels}"
FontSize="12">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="local:TreeViewItemBehaviour.IsBroughtIntoViewWhenSelected"
Value="True" />
<Setter Property="IsExpanded"
Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected"
Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="Visibility"
Value="{Binding IsVisible, Mode=TwoWay, Converter={StaticResource boolVisibilityConverter}}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding SomeDisplayText}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
where RootViewModels and Children are of type ObservableCollection<SomeItemViewModel>
In the same View I have a ComboBox and I want the selected item in this ComboBox to serve as the criteria to filter the TreeView by. I have a ViewModelRepository which is also of type ObservableCollection<SomeItemViewModel> and is provided by my IoC container. I use this to create my other ObservableCollections that are in the XAML above.
At the moment, I'm trying to set the Visibility of each SomeItemViewModel in my ViewModelRepository when an item in the ComboBox is selected. This seems to work for anything below the first two levels in the tree, but not for the 2nd level itself (the first level being the root which is always visible).
However, it doesn't work consistently. When I apply the "filter" the correct nodes are set invisible, but if I then expand a node which contains "filtered" nodes then any subsequent "filters" fail.
I've read about binding ItemsControls to a CollectionViewSource in order to do filtering, but I can't find an example of it's usage with the TreeView. Does anyone have any pointers on how I can use it here?

you could use ICollectionView to filter your collection.
something like this:
ICollectionView view = CollectionViewSource.GetDefaultView(this.YourObservableCollection);
view.Filter = o => {
var itemtofilter = o as yourviewmodeltype;
//check your conditions here
...
return (bool)itemtofilercondition;
};
edit: you have to recreate the view if you call new YourObservableCollection();

Related

TreeView HierarchicalDataTemplate does not apply ItemContainerStyle

I try do display hierarchical data with a TreeView and I would like to set different DataTemplates for my different Children types.
But the thing is, that my style does not get applied.
Maybe its a very simple mistake but i really do not find it.
<TreeView ItemsSource="{Binding List}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Main}" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Property1}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Type2}">
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
<TextBlock Text="{Binding Property2}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Type3}">
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="False"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
Ok, I know what is going wrong. HierarchicalDataTemplate.ItemContainerStyle contains a style which is applied to the ItemsContainer where the children for the current node are stored. Try this as an experiment, change your style to:
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="True" />
<Setter Property="Foreground" Value="Navy" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
You will notice that the node that you put this style onto continues to have a black foreground, but all it's children will now have a foreground of Navy.
It is a little counter-intuitive, but when you think about it, I guess it makes sense. So, bearing this in mind, I think the best solution is to bind IsExpanded for all TreeViewItems to a variable in the VM and then pick different values based on types there.
I had a similar issue maybe. In case Main, Type2 and Type3 are interfaces the selection in XAML won't work, I had to use classes. If you want to use interfaces you could implement a template selector.

Conditionally changing the Foreground of a WPF ComboBox item depending upon a value in the databound ItemsSource item

I have a WPF ComboBox bound to a Collection List<Users>. I have applied a DataTemplate to show the FirstName using a TextBlock and this works as expected:
<ComboBox Margin="5" ItemsSource="{Binding Path=TheUsers}" Name="cboUsers">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Margin="10" Text="{Binding Path=FirstName}">
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>`
I have an item in my User class called IsActive which is a Boolean value. If true then I want to set the Foreground of the TextBlock to Navy.
I have spent so much time on what should be so easy and looked all over the web but most articles talk about changing the overall colour or binding to another element in the xaml.
I tried implementing a DataTrigger and after an hour removed the code because it was not working. It would not recognise my field name. Does anyone have a very simple guide to how to do this or what would be the best approach?
As you apparently are not dealing with fields after all, this style should do what you want:
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive}" Value="True">
<Setter Property="Foreground" Value="Navy"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
It would not recognise my field name.
You cannot bind to fields, end of story.

WPF Highlight Item

I have a ViewModel that provides a collection of Items. There is also a ActiveItem propery. The Items collection may or may not contain ActiveItem.
What I want to do (in XAML) is display the items as a list and highlight any of the items that are equal to Active Item.
I have tried the following with no success:
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border x:Name="outerBorder" Background="Green">
<TextBlock Text="{Binding ItemId}" />
</Border>
<DataTemplate.Triggers>
<DataTrigger
Binding="{Binding
Path=DataContext.Item.ItemId,
RelativeSource={RelativeSource TemplatedParent},
Mode=Default}"
Value="{Binding
Path=DataContext.ActiveItem.ItemId,
RelativeSource={RelativeSource AncestorType=Window},
Mode=Default}"
>
<Setter TargetName="outerBorder"
Property="Background" Value="Orange" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
As you can see I attempted to use a DataTrigger to match the current item with the ActiveItem but it doesn't work. I think this is something to do with my trying to use a Binding in DataTrigger.Value - something I haven't seen any other examples of.
Any ideas how I might make this work?
Thanks,
Daniel
Since you're using MVVM, why not just have the view model expose a property telling the view whether it's active or not? That'll get that logic out of you view and into your VM.

Is it possible to highlight a ComboBoxItem, but not set it as the selected item?

I have a ComboBox that I am populating with a list of parts for a Return Authorization receiving app. In our RA system, a customer can specify a return for a Kit, but only actually return part of the kit. So because of this, my ComboBox shows a list of parts that belong to the kit, and asks the receiver to choose which part was actually received.
I have found the defaulting the selected item in my received part list to the part specified in the return makes to lazy receivers, and incorrect part information being received. So I have left the ComboxBox unselected.
What I would like to do is to highlight the specified part in the ComboBox, without actually selecting it. This way the actual part can be quickly found, while still requiring the user to actually choose it.
Although, this doesn't work, I think it will illustrate what I would LIKE to do:
<ComboBox Grid.Column="1" ItemsSource="{Binding Path=Part.MasterPart.FamilyParts}"
SelectedItem="{Binding Path=ReceivedPart, ValidatesOnDataErrors=True}" >
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Part.MaxId}"
Value="{Binding Path=Part.MaxId}">
<Setter Property="Background" Value="LightSalmon" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
You've got the right idea. The only thing I see wrong with your code is the DataTrigger attributes.
If Value was, well, just a value, it would work:
<DataTrigger Binding="{Binding Path=Part.MaxId}" Value="999" >
I would wrap this logic up into a new property on the viewmodel for simplicity:
<DataTrigger Binding="{Binding Path=Part.ShouldHighlight}" Value="true">
There are ways to highlight other than setting the background color, and I'd recommend you explore them because it can be confusing to users to have different background colors for different reasons (selection versus highlighting). For example, you could put a little star next to relevant items or make them bold.
That said, You can just do this to set the background color of the ComboBoxItem:
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="Background" Value="{Binding Part.MaxId, Converter={StaticResource BackgroundConverter}}"/>
</Style>
</ComboBox.ItemContainerStyle>
Even better, use a view model and just bind directly to a BackgroundColor property:
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="Background" Value="{Binding BackgroundColor}"/>
</Style>
</ComboBox.ItemContainerStyle>
Instead of adding a property to the view model, you could use a Style Selector (http://msdn.microsoft.com/en-us/library/system.windows.controls.styleselector.aspx) to determine which style to use for an item.
You're on the right track, but what I would do is put a bool read-only property in your Part class that told the combobox whether it should be highlighted in this instance. You could try something like this:
<ComboBox Grid.Column="1" ItemsSource="{Binding Path=Part.MasterPart.FamilyParts}"
SelectedItem="{Binding Path=ReceivedPart, ValidatesOnDataErrors=True}" >
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Border Background="LightSalmon" Visibility="{Binding Part.Highlighted, Converter={StaticResource BoolToVizConverter}}"/>
<TextBlock Text="{Binding Part.Name}"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
This way, the background of the border wouldn't display at all if Highlighted were false.

How can I bind an ObservableCollection of ViewModels to a MenuItem?

When I bind Menu Items with an ObservableCollection, only the "inner" area of the MenuItem is clickable:
alt text http://tanguay.info/web/external/mvvmMenuItems.png
In my View I have this menu:
<Menu>
<MenuItem
Header="Options" ItemsSource="{Binding ManageMenuPageItemViewModels}"
ItemTemplate="{StaticResource MainMenuTemplate}"/>
</Menu>
Then I bind it with this DataTemplate:
<DataTemplate x:Key="MainMenuTemplate">
<MenuItem
Header="{Binding Title}"
Command="{Binding DataContext.SwitchPageCommand,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Menu}}}"
Background="Red"
CommandParameter="{Binding IdCode}"/>
</DataTemplate>
Since each ViewModel in the ObservableCollection ManageMenuPageItemViewModels has a property Title and IdCode, the above code works fine at first sight.
HOWEVER, the problem is that the MenuItem in the DataTemplate is actually inside another MenuItem (as if it is being bound twice) so that in the above DataTemplate with Background="Red" there is a Red box inside each menu item and only this area can be clicked, not the whole menu item area itself (e.g. if the user clicks on the area where the checkmark is or to the right or left of the inner clickable area, then nothing happens, which, if you don't have a separate color is very confusing.)
What is the correct way to bind MenuItems to an ObservableCollection of ViewModels so that the whole area inside each MenuItem is clickable?
UPDATE:
So I made the following changes based on advice below and now have this:
alt text http://tanguay.info/web/external/mvvmMenuItemsYellow.png
I have only a TextBlock inside my DataTemplate, but I still can't "color the whole MenuItem" but just the TextBlock:
<DataTemplate x:Key="MainMenuTemplate">
<TextBlock Text="{Binding Title}"/>
</DataTemplate>
And I put the Command binding into Menu.ItemContainerStyle but they don't fire now:
<Menu DockPanel.Dock="Top">
<Menu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Background" Value="Yellow"/>
<Setter Property="Command" Value="{Binding DataContext.SwitchPageCommand,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Menu}}}"/>
<Setter Property="CommandParameter" Value="{Binding IdCode}"/>
</Style>
</Menu.ItemContainerStyle>
<MenuItem
Header="MVVM" ItemsSource="{Binding MvvmMenuPageItemViewModels}"
ItemTemplate="{StaticResource MainMenuTemplate}"/>
<MenuItem
Header="Application" ItemsSource="{Binding ApplicationMenuPageItemViewModels}"
ItemTemplate="{StaticResource MainMenuTemplate}"/>
<MenuItem
Header="Manage" ItemsSource="{Binding ManageMenuPageItemViewModels}"
ItemTemplate="{StaticResource MainMenuTemplate}"/>
</Menu>
I found using MVVM with MenuItems to be very challenging. The rest of my application uses DataTemplates to pair the View with the ViewModel, but that just doesn't seem to work with Menus because of exactly the reasons you've described. Here's how I eventually solved it. My View looks like this:
<DockPanel>
<Menu DockPanel.Dock="Top" ItemsSource="{Binding Path=(local:MainViewModel.MainMenu)}">
<Menu.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding Path=(contracts:IMenuItem.Header)}"/>
<Setter Property="MenuItem.ItemsSource" Value="{Binding Path=(contracts:IMenuItem.Items)}"/>
<Setter Property="MenuItem.Icon" Value="{Binding Path=(contracts:IMenuItem.Icon)}"/>
<Setter Property="MenuItem.IsCheckable" Value="{Binding Path=(contracts:IMenuItem.IsCheckable)}"/>
<Setter Property="MenuItem.IsChecked" Value="{Binding Path=(contracts:IMenuItem.IsChecked)}"/>
<Setter Property="MenuItem.Command" Value="{Binding}"/>
<Setter Property="MenuItem.Visibility" Value="{Binding Path=(contracts:IMenuItem.Visible),
Converter={StaticResource BooleanToVisibilityConverter}}"/>
<Setter Property="MenuItem.ToolTip" Value="{Binding Path=(contracts:IMenuItem.ToolTip)}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=(contracts:IMenuItem.IsSeparator)}" Value="true">
<Setter Property="MenuItem.Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MenuItem}">
<Separator Style="{DynamicResource {x:Static MenuItem.SeparatorStyleKey}}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Menu.ItemContainerStyle>
</Menu>
</DockPanel>
If you notice, I defined an interface called IMenuItem, which is the ViewModel for a MenuItem. Here's the code for that:
public interface IMenuItem : ICommand
{
string Header { get; }
IEnumerable<IMenuItem> Items { get; }
object Icon { get; }
bool IsCheckable { get; }
bool IsChecked { get; set; }
bool Visible { get; }
bool IsSeparator { get; }
string ToolTip { get; }
}
Notice that the IMenuItem defines IEnumerable Items, which is how you get sub-menus. Also, the IsSeparator is a way to define separators in the menu (another tough little trick). You can see in the xaml how it uses a DataTrigger to change the style to the existing separator style if IsSeparator is true. Here's how MainViewModel defines the MainMenu property (that the view binds to):
public IEnumerable<IMenuItem> MainMenu { get; set; }
This seems to work well. I assume you could use an ObservableCollection for the MainMenu. I'm actually using MEF to compose the menu out of parts, but after that the items themselves are static (even though the properties of each menu item are not). I also use an AbstractMenuItem class that implements IMenuItem and is a helper class to instantiate menu items in the various parts.
UPDATE:
Regarding your color problem, does this thread help?
Don't put the MenuItem in the DataTemplate. The DataTemplate defines the content of the MenuItem. Instead, specify extraneous properties for the MenuItem via the ItemContainerStyle:
<Menu>
<Menu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Title}"/>
...
</Style>
</Menu.ItemContainerStyle>
<MenuItem
Header="Options" ItemsSource="{Binding ManageMenuPageItemViewModels}"
ItemTemplate="{StaticResource MainMenuTemplate}"/>
</Menu>
Also, take a look at HierarchicalDataTemplates.
Here is how I have done my menus. It may not be precisely what you need, but I think it is pretty close.
<Style x:Key="SubmenuItemStyle" TargetType="MenuItem">
<Setter Property="Header" Value="{Binding MenuName}"></Setter>
<Setter Property="Command" Value="{Binding Path=MenuCommand}"/>
<Setter Property="ItemsSource" Value="{Binding SubmenuItems}"></Setter>
</Style>
<DataTemplate DataType="{x:Type systemVM:TopMenuViewModel}" >
<Menu>
<MenuItem Header="{Binding MenuName}"
ItemsSource="{Binding SubmenuItems}"
ItemContainerStyle="{DynamicResource SubmenuItemStyle}" />
</Menu>
</DataTemplate>
<Menu DockPanel.Dock="Top" ItemsSource="{Binding Menus}" />
TopMenuViewModel is a collection of the menus that will appear on the menu bar. They each contain the MenuName that will be displayed and a collection called SubMenuItems that I set to be the ItemsSource.
I control the way the SubMenuItems are displayed by way of the style SumMenuItemStyle. Each SubMenuItem has its own MenuName property, Command property of type ICommand, and possibly another collection of SubMenuItems.
The result is that I am able to store all my menu information in a database and dynamically switch what menus are displayed at runtime. The entire menuitem area is clickable and displays correctly.
Hope this helps.
Just make your DataTemplate to be a TextBlock (or maybe a stack panel with an icon and a TextBlock).

Resources