Reputation: 1192
In my application, I have a database-like structure, where the database object itself contains several ObservableCollection<KeyValuePair<Guid, T>>
collections. The Guid
s act similar to primary keys in a relational database, i.e., they provide 1:1 and 1:n mappings between objects of the different collections (“tables” in database-ese).
Now consider binding the ObservableCollection<KeyValuePair<Guid, T>>
that is at the root of the object hierarchy in an ItemsControl
. Inside the DataTemplate
, I want to bind a subset of another of the collections to a DependencyProperty
of a UserControl
, where the Guid
s match values that each of the objects in the first collection carry.
As a plethora of answers here on SO suggest, a CollectionViewSource
is what I need, i.e.,
<ItemsControl ItemsSource="{Binding RootObjectCollection}>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:CustomUserControl>
<local:CustomUserControl.SubsetCollection>
<Binding>
<Binding.Source>
<CollectionViewSource Source="{Binding DataContext.Database.SubsetCollection, RelativeSource={RelativeSource AncestorType=UserControl}}"
Filter="someFilter"
???? FilterParameters="{Binding SelectedKeys}" />
</Binding.Source>
</Binding>
</local:CustomUserControl.SubsetCollection>
</local:CustomUserControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
However, I need to dynamically pass a parameter of type ObservableCollection<Guid>
to the filter of the CollectionViewSource
.
I’m frankly lost, since the documentation doesn’t have anything on this. I can’t believe that I’m the first one in need of a parameterized dynamic filter that doesn’t bind to a text field… Any hint is greatly appreciated!
The above code should now be a little clearer. Aside from that, some more background information so as to clarify @erotavlas’s questions:
The code above resides in a view with its own view model as data context. The CustomUserControl
that is instantiated within the DataTemplate
has its own view model, too. What I try above is passing the filter result (which is a subset of the SubsetCollection
based on a primary key indicator contained in the ItemControl
’s current RootObjectCollection
element) to the CustomUserControl
’s corresponding field.
All of the ObservableCollection
s reside within the surrounding view’s view model in an object called Database
. This object contains several of those ObservableCollections
, among others, RootObjectCollection
and SubsetCollection
.
Upvotes: 0
Views: 466
Reputation: 1192
When I posted this question, I was in a hurry since only a few days were missing before a very important demo (financing round of the product). Since I couldn’t figure out how to approach the problem with CollectionViewSource
, I decided to try a solution using the old MultiValueConverter
approach, all while being fully aware that this would have me create a new ObservableCollection
of the subset collection’s values, which—according to the C# man page for ObservableCollection<T>(IEnumerable<T>
) would only work one-way since “the elements are copied onto the ObservableCollection<T>
”. I thought that it’d be better to show that the view is filled from the database without reflecting back changes into the database, than to show nothing at all.
Imagine my surprise when it turned out that the man page is not entirely correct here: only primitive values are copied, wile complex objects are passed by reference into the new ObservableCollection<T>
! This means that the following snippet is an entirely valid solution to my problem:
<ItemsControl ItemsSource="{Binding RootObjectCollection}>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:CustomUserControl>
<local:CustomUserControl.SubsetCollection>
<MultiBinding Converter="{StaticResource SubsetEntryFromRootObjectIdSelectionConverter}">
<Binding Path="Value.SubsetIds" />
<Binding Path="DataContext.Database.SubsetCollection" RelativeSource="{RelativeSource AncestorType=UserControl}" />
</MultiBinding>
</local:CustomUserControl.SubsetCollection>
</local:CustomUserControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The important part here is the MultiValueConverter
itself, which is defined as
public class SubsetEntryFromRootObjectIdSelectionConverter: IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
if (values[0] == null) // no reference ids contained
return new ObservableCollection<SubsetItem>();
if (!(values[0] is ObservableCollection<Guid>))
throw new InvalidOperationException("Value must be a collection of Guids.");
if (!(values[1] is ObservableCollection<KeyValuePair<Guid, SubsetItem>>))
throw new InvalidOperationException("Value must be a collection of SubsetItems.");
var selectedKeys = (ObservableCollection<Guid>)values[0];
var originalCollection = (ObservableCollection<KeyValuePair<Guid, SubsetItem>>)values[1];
var queryCollection = originalCollection.Where(kvp => selectedKeys.Contains(kvp.Key)).Select(kvp => kvp.Value);
// it seems that the man page is misleading and that this constructor indeed copies references, not objects
return new ObservableCollection<SubsetItem>(queryCollection);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
Since this solution works flawlessly, I didn’t implement the one suggested by @mm8 above. However, technically speaking, the suggested solution is a direct answer to my question, whereas mine is, being honest, rather a workaround. Therefore, I’ll accept @mm8’s answer instead of mine.
Upvotes: 1
Reputation: 169200
However, I need to dynamically pass a parameter of type
ObservableCollection<Guid>
to the filter of theCollectionViewSource
.
CollectionViewSource.Filter
is an event and you can't pass any custom parameters to it. You get a FilterEventArgs
that has a read-only reference to the item and an Accepted
property that you can set to indicate whether to include the item in the filtered set, but that's it.
You could perhaps consider to create a class that extends CollectionViewSource
and add your own custom dependency property it. This should enable you to bind to a source property like SelectedKeys
. You can then retrieve the value of the dependency property by casting the sender
argument in the Filter
event handler, e.g.:
private void Cvs_Filter(object sender, FilterEventArgs e)
{
YourCustomCollectionViewSource cvs = sender as YourCustomCollectionViewSource;
//..
}
Upvotes: 0