I'm using a RadGridView to display a bunch of items in a grid. For each item, I want to switch between two different templates based on the data being given. One is a dependency property which essentially pops a text block in, the other is another RadGridView to display a table.
When put in statically, they both work individually, but I want to dynamically select these two different templates. My selector does not get called, however, and thus no template is used.
Resources:
<Window.Resources>
<DataTemplate x:Key="theBasicView">
<controls:InfoDetailsControl InfoDetail="{Binding InfoDetails}" />
</DataTemplate>
<DataTemplate x:Key="theTableView">
<telerik:RadGridView ItemsSource="{Binding DetailsTable}" />
</DataTemplate>
<analysis:DetailsTemplateSelector
BasicView="{StaticResource theBasicView}"
TableView="{StaticResource theTableView}"
x:Key="detailsTemplateSelector"
/>
</Window.Resources>
And the template selector in question:
<telerik:RadGridView.RowDetailsTemplate>
<DataTemplate>
<ItemsControl
ItemTemplateSelector="{StaticResource detailsTemplateSelector}"
/>
</DataTemplate>
</telerik:RadGridView.RowDetailsTemplate>
If it is a BasicView, then the DetailsTable should be null. Otherwise, it should be a TableView. Here is my DetailsTemplateSelector:
public class DetailsTemplateSelector : DataTemplateSelector
{
public DataTemplate BasicView { get; set; }
public DataTemplate TableView { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
FrameworkElement element = container as FrameworkElement;
if (element != null && item != null && item is ViewableRuleResult)
{
ViewableRuleResult ruleResult = item as ViewableRuleResult;
Window window = Application.Current.MainWindow;
if (ruleResult.DetailsTable == null)
{
return BasicView;
}
else
{
return TableView;
}
}
return null;
}
}
Putting a breakpoint in the SelectTemplate function never gets hit. Why is my DetailsTemplateSelector never getting called? I have a feeling that the template selector in my RowDetailsTemplate isn't right. Let me know if you need more detail or something is unclear.
Thanks!
Fixed it. Turns out RadGridView has a property RowDetailsTemplateSelector. Using the following XAML:
<telerik:RadGridView x:Name="resultsgrid"
RowDetailsTemplateSelector="{StaticResource detailsTemplateSelector}"
ItemsSource="{Binding ViewableItems}"
AutoGenerateColumns="False"
Margin="0,0,0,30"
IsReadOnly="True"
>
And completely deleting the RowDetailsTemplate previously defined, it now functions properly.
Related
I have the following ListBox with the ContentControl as DataTemplate:
<ListBox x:Name="lstActionConfigs" ItemsSource="{Binding Path=AllActionConfigList}" SelectedItem="{Binding Path=ListSelectedItem, Mode=TwoWay}" HorizontalContentAlignment="Stretch" Grid.Row="3" Margin="0,0,0,5">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type helper:ItemDetails}">
<ContentControl Template="{StaticResource ResourceKey=actionDetailsListItemTemplate}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<i:Interaction.Behaviors>
<behaviours:BringIntoViewBehaviour CustomIsSelected="{Binding Path=IsSelected, Mode=TwoWay}"/>
</i:Interaction.Behaviors>
</ContentControl>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Each bounded instance has 'IsSelected' property which notify the UI on changes via INotifyPropertyChanged:
public bool IsSelected
{
get { return isSelected; }
set
{
isSelected = value;
notify("IsSelected");
}
}
I built a custom behavior that brings into view the elements that changed it's IsSelectedProperty to true, as the follows:
public class BringIntoViewBehaviour : Behavior<FrameworkElement>
{
public bool CustomIsSelected
{
get { return (bool)GetValue(CustomIsSelectedProperty); }
set { SetValue(CustomIsSelectedProperty, value); }
}
public static readonly DependencyProperty CustomIsSelectedProperty =
DependencyProperty.Register("CustomIsSelected", typeof(bool), typeof(BringIntoViewBehaviour), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(customIsSelectedPropertyChanged_Callback)));
private static void customIsSelectedPropertyChanged_Callback(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
BringIntoViewBehaviour thisControl = o as BringIntoViewBehaviour;
if (thisControl == null)
return;
bringIntoView(thisControl);
}
}
This item is not presented this moment on the UI as it located at the bottom of the list (there is a scroll bar).
I updated the IsSelected property with true value.
However, the customIsSelectedPropertyChanged_Callback method should be executed as we updated it's bounded property.
But, in practice, this method is invoked only when this item is presented on UI when moving the scroll bar down to it.
The reason most likely is UI virtualization. ListBox items host is by default VirtualizingStackPanel. It will not generate items which are out of view now, so when you set IsSelected on your model, your DataTemplate together with your behaviour are not created yet. Only when you scroll down, control is created together with behaviour from data template, and after it is bound CustomIsSelectedProperty is set to true, so your callback is called.
To verify this assumption you can disable UI virtualization for your ListBox and see if that resolves the problem.
I am trying to use a DataTemplateSelector to load different sets of controls based on a combobox selection but it seems to never get called. There is nothing fancy inside my DataTemplates except labels and more comboboxes inside a grid.
Here is my TemplateSelector
public class PWRPTemplateSelector : DataTemplateSelector
{
public DataTemplate Product { get; set; }
public DataTemplate Project { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if(item == null) { return base.SelectTemplate(item, container); }
return (int)item == 0 ? Product : Project;
}
}
And my xaml
<DataTemplate x:Key="Project">
.....
</DataTemplate>
<DataTemplate x:Key="Project">
.....
</DataTemplate>
<c:PWRPTemplateSelector x:Key="PWRPTemplateSelector" Product="{StaticResource Product}" Project="{StaticResource Project}"/>
<ItemsControl ItemsSource="{Binding TestSelc, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemTemplateSelector="{StaticResource PWRPTemplateSelector}" Grid.Row="1" Grid.ColumnSpan="2" />
My ItemControl is bound to the selected property of a Combobox which is an int. Everything gets executed properly, except the DataTemplate never gets called. All of the property's get change and implement INotifyPropertyChanged.
UPDATE:
I can get my DataTemplateSelector if I change my selected item to a string value. However, the value gets passed as a char and not the entire string. The method gets called for every char in the string then.
I'm working on setting up a ListView whose Source property is set to an ivar of a class of mine, called Cat.
Each Cat has an ObservableCollection of Trait objects:
private ObservableCollection<Trait> _traits = new ObservableCollection<Trait>();
public ObservableCollection<Trait> Traits
{
get
{
return _traits;
}
}
public void AddTrait(Trait t)
{
_traits.Add(t);
// Is this redundant? Is one better than the other?
this.OnPropertyChanged("_traits");
this.OnPropertyChanged("Traits");
}
public IEnumerator<Object> GetEnumerator()
{
return _traits.GetEnumerator();
}
And then I'm assigning the Source property to this Traits collection:
this.CollectionViewSource.Source = CurrentCat.Traits;
This works properly, and the Trait objects are properly displayed in my ListView.
The issue is that changes to this underlying _traits collection do not cause the UI to update properly. For example, this:
void AddTraitButton_Click(object sender, RoutedEventArgs e)
{
if (this.CurrentCat != null)
{
this.CurrentCat.AddTrait(new Trait());
}
}
Doesn't seem to have any effect immediately in the UI, but if I reset the Source property like so:
var oldSource = this.CollectionViewSource.Source;
this.CollectionViewSource.Source = null;
this.CollectionViewSource.Source = oldSource;
Then the ListView updates properly. But, I'm sure there must be something that I'm missing, as I'd like for the UI to update upon the addition/removal of an item.
Edit: The CollectionViewSource is being applied to the ListView in my XAML file:
<CollectionViewSource x:Name="CollectionViewSource" x:Key="CollectionViewSource" />
...
<ListView x:Name="ItemListView" ItemsSource="{Binding Source={StaticResource CollectionViewSource}}" ...
I can't seem to find it now, but I seem to remember some problem with binding to CollectionViewSource. Have you tried binding directly to CurrentCat.Traits and setting this.DataContext = this in the code-behind (I am assuming you aren't using MVVM here)?
<ListView x:Name="ItemListView" ItemsSource="{Binding CurrentCat.Traits}" />
Rather than binding to the CollectionViewSource directly and replacing its Source to force a refresh, I believe you want to bind to the CVS's View property...
<ListView x:Name="ItemListView"
ItemsSource="{Binding Source={StaticResource CollectionViewSource}, Path=View}" ...
...and call CollectionViewSource.Refresh() after updating the source collection.
void AddTraitButton_Click(object sender, RoutedEventArgs e)
{
if (this.CurrentCat != null)
{
this.CurrentCat.AddTrait(new Trait());
this.CollectionViewSource.Refresh();
}
}
Also, a couple notes, since you seem relatively new to .NET/WPF conventions:
The private members of .NET classes are typically referred to as "fields" rather than "ivars" (Objective-C background? :))
Prefixing class members with the this keyword is usually redundant, unless there is another identifier in scope with the same name
It's worth exploring the MVVM and related patterns if you'll be doing anything non-trivial in WPF; they help you keep your views (XAML objects) as light and easy-to-change as possible.
In your case, for example, I assume the code you've shown is from the code-behind of whatever Window or UserControl contains your ListView. Following the MVVM pattern would involve creating a separate "ViewModel" class that would contain the Traits collection and expose it via a CollectionViewSource (using the View property, as I've mentioned). Your UserControl would then have an instance of the ViewModel assigned as its DataContext, and the ListView could be bound to the exposed CollectionView.
You may work exclusively with the ObservableCollection still. Although there is one problem - it would not show the data in IsInDesignMode. Maybe in the future it will improve.
public class MainViewModel : ViewModelBase
{
...
private ObservableCollection<PartViewModel> _parts;
public ObservableCollection<PartViewModel> Parts
{
get
{
if (_parts == null)
{
_parts = new ObservableCollection<PartViewModel>();
_parts.CollectionChanged += _parts_CollectionChanged;
}
return _parts;
}
}
object m_ReorderItem;
int m_ReorderIndexFrom;
void _parts_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
m_ReorderItem = e.OldItems[0];
m_ReorderIndexFrom = e.OldStartingIndex;
break;
case NotifyCollectionChangedAction.Add:
if (m_ReorderItem == null)
return;
var _ReorderIndexTo = e.NewStartingIndex;
m_ReorderItem = null;
break;
}
}
private PartViewModel _selectedItem;
public PartViewModel SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (_selectedItem != value)
{
_selectedItem = value;
RaisePropertyChanged("SelectedItem");
}
}
}
...
#region ViewModelBase
public override void Cleanup()
{
if (_parts != null)
{
_parts.CollectionChanged -= _parts_CollectionChanged;
}
base.Cleanup();
}
#endregion
}
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<Grid.Resources>
<CollectionViewSource x:Name="PartsCollection" Source="{Binding Parts}"/>
</Grid.Resources>
<ListView Margin="20" CanReorderItems="True" CanDragItems="True" AllowDrop="True"
ItemsSource="{Binding Source={StaticResource PartsCollection}}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
...
</ListView.ItemTemplate>
</ListView>
</Grid>
I have made a tree View in wpf Using MVVM .
it is working fine but here is one problem that leaf node contains some checkboxes and user have only two options either to select one or none .
So here how i can restricted user to select maximum only one cold drink.
I did one trick but it didn't work that when i have already selected a drink and then i select another one than i set the last selected value in the observable collection to false but it doesn't affect on view and selected check boxes remains selected although in collection only one option's value is true.
I cant use radio button instedof checkbox becasue user can select none of the options and i cant give an additional option for none of the above.
If any one have any solution so please let me know I'll be very thankful.
updated question:
i think i didn't define my problem in a proper way so i am giving my code snipperts here hope by this i'll get the solution o f my problem...
My View Model Class
namespace TestViewModels
{
public class ViewModel :ViewModelBase
{
private ObservableCollection<AvailableProducts> _MyTreeViewProperty
public ObservableCollection<AvailableProducts> MyTreeViewProperty
{
get { return _MyTreeViewProperty
set { _MyTreeViewProperty value;
RaisePropertyChanged("MyTreeViewProperty");}
}
}
public class AvailableProducts
{
private string _BrandName;
public string BrandName
{
get { return _BrandName
set { _BrandName = value; }
}
private bool _IsExpanded;
public bool IsExpanded
{
get
{
return _IsExpanded;
}
set
{
_IsExpanded = value;
}
}
private ObservableCollection<ProductTypes> _MyProductTypes
public ObservableCollection<ProductTypes> MyProductTypes
{
get { return _MyProductTypes}
set { _MyProductTypes= value; }
}
}
public class ProductTypes
{
private string _ProductTypeName;
public string ProductTypeName
{
get { return _ProductTypeName;
set { _ProductTypeNamevalue; }
}
private ObservableCollection<ProductSubTypes> _ProdSubTypes;
public ObservableCollection<ProductSubTypes> ProdSubTypes
{
get { return _ProdSubTypes;}
set { _ProdSubTypes;= value; }
}
}
public class ProductSubTypes
{
private string _ProductSubTypeName;
public string ProductSubTypeName
{
get { return _ProductSubTypeName;
set { _ProductSubTypeName;}
}
private int _ParentID;
public int ParentID
{
get { return _ParentID;}
set { _ParentID;= value; }
}
private bool _IsAssigned;
public bool IsAssigned
{
get { return _IsAssigned; }
set
{
_IsAssigned = value;
if _ParentID;!= 0)
{
//updating data in database
//Calling and setting new collection value in property
//issue : updated collection sets in setter of MyTreeViewProperty but before calling getter
// it comes to IsAssigned getter so view doesnt get updated collection of MyTreeViewProperty
}
RaisePropertyChanged("IsAssigned");
}
}
}
}
View
<Page x:Class="ShiftManagerViews.Pages.ProductTreeSelection
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DataContext="{Binding ProductsTree, Source={StaticResource Locator}}"
mc:Ignorable="d" Width="870" Height="665"
>
<TreeView Margin="10,10,0,13" ItemsSource="{Binding MyTreeViewProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="800" Height="Auto" MinHeight="400" MaxHeight="800">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:AvailableProducts}"
ItemsSource="{Binding MyProductTypes}">
<WrapPanel>
<Image Width="20" Height="20" Source="/ShiftManagerViews;component/Images/12.bmp"/>
<Label Content="{Binding BrandName}" FontSize="14"/>
</WrapPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:ProductTypes}"
ItemsSource="{Binding ProdSubTypes}">
<WrapPanel>
<Image Width="18" Height="15" Source="/ShiftManagerViews;component/Images/12.bmp"/>
<Label Content="{Binding ProductTypeName}" FontSize="13"/>
</WrapPanel>
</HierarchicalDataTemplate>
<!-- the template for showing the Leaf node's properties-->
<DataTemplate DataType="{x:Type local:ProductSubTypes}">
<StackPanel>
<CheckBox IsChecked="{Binding IsAssigned, Mode=TwoWay}" Content="{Binding ProductSubTypeName}" Height="25">
</CheckBox>
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
What about using a ListBox to display sub-items instead of a TreeView? You can style that so the items contain a CheckBox to show IsSelected instead of highlighting the item.
I'd suggest your user interface is wrong. If the user can only pick one then it would be better to swap these for radio buttons and add a "None of the above" option. That'll then give you the behaviour you want for free and your UI will be more intuitive.
EDIT: Since you say you can't add a "None" option and want to use a checkbox (even though I strongly disagree on checkboxes where a radio button is more appropriate - a common UI error)...
The technical problem you are probably facing is that an ObservableCollection only raises notification events if the collection itself changes. i.e. Only if items are added or removed. It does not raised events when items within the collection change, therefore the changing the status of the checkbox in the code will not raise the event for the UI binding to act on.
One solution to this to write a custom class that extends ObservableCollection that does provide this behaviour
From MSDN:
If you need to know if someone has changed a property of one of the
items within the collection, you'll need to ensure that the items in
the collection implement the INotifyPropertyChanged interface, and
you'll need to manually attach property changed event handlers for
those objects. No matter how you change properties of objects within
the collection, the collection's PropertyChanged event will not fire.
As a matter of fact, the ObservableCollection's PropertyChanged event
handler is protected—you can't even react to it unless you inherit
from the class and expose it yourself. You could, of course, handle
the PropertyChanged event for each item within the collection from
your inherited collection
I upvoted Rachel's answer, it is a common way in WPF to databind sets of radio buttons or check boxes. If you still want to go the tree view way, below code works. All view related code is in the view, so below code follows MVVM principles. If you are a MVVM purist you can put the code behind and a TreeView control in a user control if you do not want any code behind.
XAML:
<TreeView ItemsSource="{Binding Path=Drinks}">
<TreeView.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding .}" Checked="OnCheckBoxChecked" Unchecked="OnCheckBoxUnchecked" Loaded="OnCheckBoxLoaded" />
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Code behind + VM:
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new VM();
}
private void OnCheckBoxChecked(object sender, System.Windows.RoutedEventArgs e)
{
foreach (CheckBox checkBox in _checkBoxes.Where(cb => cb != sender))
{
checkBox.IsChecked = false;
}
(DataContext as VM).CurrentDrink = (sender as CheckBox).Content.ToString();
}
private void OnCheckBoxUnchecked(object sender, System.Windows.RoutedEventArgs e)
{
(DataContext as VM).CurrentDrink = null;
}
private void OnCheckBoxLoaded(object sender, System.Windows.RoutedEventArgs e)
{
_checkBoxes.Add(sender as CheckBox);
}
private List<CheckBox> _checkBoxes = new List<CheckBox>();
}
public class VM
{
public List<string> Drinks
{
get
{
return new List<string>() { "Coffee", "Tea", "Juice" };
}
}
public string CurrentDrink { get; set; }
}
I did one trick but it didn't work that when i have already selected a
drink and then i select another one than i set the last selected value
in the observable collection to false but it doesn't affect on view
and selected check boxes remains selected although in collection only
one option's value is true.
Make sure that your child objects (AvailableProducts
and SubProductTypes) also implement INotifyPropertyChanged, this will make sure that the UI receives changes when modify the object.
Once all of you objects update the UI properly you will be able to layer in, and test, whatever custom business logic you need.
So if you have a product type that can only have one sub chosen, you could add a property on ProductType called OnlyAllowOneChild. Whenever, a child object raises a IsAssigned changed event, the parent can set false all other children. This of course requires you to have the parent either register for the children's PropertyChangedEvent, or got grab an EventAggregator (MVVMLight Messenger, or PRISM EvenAggregator) and create a messaging system.
Finally i am succeeded to solve my problem.
on Is Assigned property i am updating my database values and calling a method in view using MVVM Light messaging and passing currently selected leaf's parent id in it as a parameter...
Added a property in class Product Types to expand the parent node of the last selected leaf..
In view's method i am refreshing data context's source and passing currently selected leaf's parent id tO the VM to set its Is Expanded property value to true...
By this my view is working perfectly as same as i want...
If any body have solution better than this than I'll be happy to know.
I'm having trouble filtering hierarchical data that's being displayed in nested xaml templates.
I've got a ObservableCollection<Foo> Foos, that I'm displaying in XAML.
Lets say Foo looks like:
class Foo
{
public ObservableCollection<Bar> Bars;
}
class Bar
{
public ObservableCollection<Qux> Quxes;
}
I'm displaying Foos with the following xaml:
<Grid>
<Grid.Resources>
<CollectionViewSource x:Key="MyCVS" Source="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}}, Path=DataContext.UnifiedSymbols}" Filter="MyCVS_Filter" />
<DataTemplate x:Key="NestedTabHeaderTemplate">
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
<DataTemplate x:Key="NestedTabContentTemplate">
<ListBox ItemsSource="{Binding Path=Quxes}" DisplayMemberPath="Name"/>
</DataTemplate>
<DataTemplate x:Key="TopLevelTabHeaderTemplate">
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
<DataTemplate x:Key="TopLevelTabContentTemplate">
<TabControl ItemsSource="{Binding Path=Bars}"
ItemTemplate="{StaticResource NestedTabHeaderTemplate}"
ContentTemplate="{StaticResource NestedTabContentTemplate}"
/>
</DataTemplate>
</Grid.Resources>
<TabControl ItemSource="{Binding correct binding for my control's collection of Foos}"
ItemTemplate="{StaticResource TopLevelTabHeaderTemplate}"
ContentTemplate="{StaticResource TopLevelTabContentTemplate}"
x:Name="tabControl"
/>
</Grid>
To put it into words, there's a tab control, with a tab for each Foo. Each Foo is a tab control, with each Bar it contains in it's own tab. Each Bar contain's a listbox of its Quxes.
or:
______ ______ ______
| Foo1 | Foo2 | Foo3 |
|______ ______ |
| Bar1 | Bar2 |______|
| | qux1 ||
| | qux2 ||
| | qux3 ||
----------------------
I also have a TextBox that I'd like to use to filter this breakdown. When I type in the text box, I'd like to filter the quxes so those not containing the text wouldn't be visible. Ideally Bar tabs would also be hidden if they have no visible quxes, and Foo tabs hidden when they have no visible Bars
I have considered two approaches:
Approach 1, reset the Filter property on the appropriate CollectionViewSources
On my text box's TextChanged event, I loop through my Foo's asking for the corresponding (static) TabControl's CollectionViewSource:
foreach(Foo foo in tabControl.Items)
{
var tabItem = tabControl.ItemContainerGenerator.ContainerFromItem(foo); // This is always of type TabItem
// How do I get the TabControl that will belong to each of Foo's Bar's?
}
Approach 2, declare the ListView's ItemSource to a CollectionViewSource
I tried setting the Filter via xaml, by changing this line:
<ListBox ItemsSource="{Binding Path=Quxes}" DisplayMemberPath="Name">
to this,
<CollectionViewSource x:Key="MyCVS" Source="?????" Filter="MyCVS_Filter" />
...
<ListBox ItemsSource="{Binding Source={StaticResource MyCVS}}" DisplayMemberPath="Name">
I've tried a number of things where I have "?????" but I cannot correctly bind to the ListBox's datacontext and appropriate Quxes member. Nothing I try results in the quxes being displayed, and I get no errors on the console. Even If I could get this approach to work, I'm not sure how I would re-trigger this filter when the text in the search box changed.
Any advice or direction would be appreciated.
Edit
Finally I've got it working with your requirements.
Here is the link to the updated project.
(edit by luke)
This is the (excellent) solution I ended up going with, so I'm going to extract the important parts, and actually make them part of the post here:
The key xaml portion ends up looking like this:
<CollectionViewSource x:Key="FooCVS" x:Name="_fooCVS" Source="{Binding Foos, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type WpfApplication1:MainWindow}}}" Filter="_fooCVS_Filter"/>
<CollectionViewSource x:Key="BarCVS" x:Name="_barCVS" Source="{Binding Bars, Source={StaticResource FooCVS}}" Filter="_barCVS_Filter"/>
<CollectionViewSource x:Key="QuxCVS" x:Name="_quxCVS" Source="{Binding Quxs, Source={StaticResource BarCVS}}" Filter="_quxCVS_Filter"/>
I set the respective control to each one of these views as the control's ItemSource. The magic is in the binding of each CVS. Each CVS gets the data context for the control/templated control in which appears, so you can use the the real name of the bound object's collection. I'm not sure I understand why binding the source of that source binding to itself (the CVS) works, but it does so beautifully.
The code for the filter TextBox then becomes something like:
private void filterTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
var cvs = TryFindResource("FooCVS") as CollectionViewSource;
if (cvs != null)
{
if (cvs.View != null)
cvs.View.Refresh();
}
cvs = TryFindResource("QuxCVS") as CollectionViewSource;
if (cvs != null)
{
if (cvs.View != null)
cvs.View.Refresh();
}
cvs = TryFindResource("BarCVS") as CollectionViewSource;
if (cvs != null)
{
if (cvs.View != null)
cvs.View.Refresh();
}
}
Excellent solution as it requires no changing of the underlying objects or hierarchy.
I think you should expose an ICollectionView from your View-Model instead of (or in addition to) an ObservableCollection. This would bring all the business logic involved with filtering/sorting into the VM, which is the right place for it.
You can get the ICollectionView of a collection by creating a CollectionViewSource, setting its Source property to the collection and retrieving the View property.
(Update) Here's some sample code:
class Foo
{
public Foo()
{
_bars = new ObservableCollection<Bar>();
Bars = new CollectionViewSource { Source = _bars }.View;
}
private ObservableCollection<Bar> _bars;
public ICollectionView Bars { get; private set; }
public void Filter(string quxName)
{
Bars.Filter = o => ((Bar)o).Quxes.Any(q => q.Name == quxName);
foreach (Bar bar in Bars)
{
bar.Filter(quxName);
}
}
}
class Bar
{
private ObservableCollection<Qux> _quxes;
public ICollectionView Quxes { get; private set; }
public void Filter(string quxName)
{
Quexs.Filter = o => ((Qux)o).Name == quxName;
}
}
class Qux
{
public string Name { get; set; }
}
I've had the similar problem at the work today and came out with the following solution:
Add Visibility property to all your elements directly or through adapter pattern.
Visibility Visibility
{
get { return visibility; }
set { visibility = value; PropertyChanged("Visibility"); }
}
Bind Visibility property of controls to corresponding Visibility properties from step1.
Implement simple filtering to your data through extension methods or inside them.
void Filter(Func<Foo, bool> filterFunc)
{
foreach (var item in foos)
{
if (!filterFunc(item))
item.Visibility = Visibility.Collapsed;
else
item.Visibility = Visibility.Visible;
}
}
Add simple filter calls on TextChanged event of your TextBox.
Filter(n => n.Name.ToLower().Contains(textBox.Text));
or a bit more advanced for you container controls:
Filter(c => c.Items.Any(i => i.Visibility == Visibility.Visible));