Binding child nodes to CollectionViewSource - wpf

So here is my data structure:
MyViewModel
Docs (ObservableCollection<Doc>)
Specs (ObservableCollection<Spec>)
i.e. the ViewModel has an ObservableCollection named Docs that is a collection of Doc objects, and in turn each Doc object has a collection of Spec objects. A property named position (available in both Doc and Spec classes) stores the logical position of each doc/spec.
I now need to bind this structure to a TreeView. I need to keep both both Docs and Specs sorted at all times (TreeView supports drag-n-drop rearrangement of nodes), so a direct binding cannot work here.
Therefore I use a CollectionViewSource to perform sorting at runtime.
<CollectionViewSource x:Key="DocumentsCVS" Source="{Binding Docs}">
<CollectionViewSource.SortDescriptions>
<componentmodel:SortDescription PropertyName="position" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
and use it in my TreeView:
<TreeView ItemsSource="{Binding Source={StaticResource DocumentsCVS}}">
So far so good. The TreeView shows my Docs sorted.
But from here onward things become confusing. How/where do I create CollectionViewSource for my specs? I tried doing this in my HierarchicalDataTemplate:
<HierarchicalDataTemplate>
<HierarchicalDataTemplate.ItemsSource>
<Binding>
<Binding.Source>
<CollectionViewSource Source="{Binding Specs}">
<CollectionViewSource.SortDescriptions>
<componentmodel:SortDescription PropertyName="position" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Binding.Source>
</Binding>
</HierarchicalDataTemplate.ItemsSource>
</HierarchicalDataTemplate>
But this doesn't work. Only Docs are listed in the TreeView, with no children inside. My gut feeling is that CollectionViewSource probably doesn't live in the same DataContext as the parent TreeViewItem.
Or is this something else?
Edit
Here is the full XAML of my TreeView:
<TreeView ItemsSource="{Binding Source={StaticResource DocumentsCVS}}"
PresentationTraceSources.TraceLevel="High">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type vm:DocumentVM}">
<HierarchicalDataTemplate.ItemsSource>
<Binding>
<Binding.Source>
<CollectionViewSource Source="{Binding Specs}">
<CollectionViewSource.SortDescriptions>
<componentmodel:SortDescription PropertyName="position" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Binding.Source>
</Binding>
</HierarchicalDataTemplate.ItemsSource>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate DataType="{x:Type vm:SpecVM}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Grid.Column="0" Icon="PuzzlePiece" Width="16" Margin="3,3,6,3" Foreground="Orange" />
<Label Grid.Column="1" Content="{Binding name}" />
</Grid>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Icon="FileWordOutline" Height="16" Margin="3,3,6,3" Foreground="Crimson" />
<Label Grid.Column="1" Content="{Binding name}" />
</Grid>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>

<CollectionViewSource Source="{Binding Specs}">
The Binding on Source there doesn't know where to go look for a DataContext (no "framework mentor"). I've kicked this around a bit. I can't find a place to define the CollectionViewSource where it inherits the DataContext from the template.
I did find a solution.
C#
public class SortConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var view = CollectionViewSource.GetDefaultView(value);
view.SortDescriptions.Add(new SortDescription((string)parameter, ListSortDirection.Ascending));
return view;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
XAML
<HierarchicalDataTemplate
DataType="{x:Type vm:DocumentVM}"
ItemsSource="{Binding Specs, Converter={StaticResource SortConverter}, ConverterParameter=position}"
>
This could be made more useful by giving the converter multiple PropertyName/SortDirection properties, or a collection of SortDescriptions. You could make it a MarkupExtension. You could also just create the collection view in a viewmodel property.

Related

TabControl w/DataGrid: Cannot find governing FrameworkElement or FrameworkContentElement for target element

I'm getting an annoying binding error that's basically my last roadblock with this control:
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=SearchResults; DataItem=null; target element is 'CollectionViewSource' (HashCode=6017800); target property is 'Source' (type 'Object')
Here's the TabControl's markup:
<TabControl ItemsSource="{Binding Tabs}" SelectedItem="{Binding SelectedTab}">
<TabControl.ItemTemplate>
<!-- tab header template works as intended -->
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="controls:SearchResultsViewModel">
<DataTemplate.Resources>
<CollectionViewSource x:Key="GroupedSearchResults"
Source="{Binding SearchResults}">
<CollectionViewSource.SortDescriptions>
<!-- sort descriptions -->
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.GroupDescriptions>
<!-- group descriptions -->
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</DataTemplate.Resources>
<controls:GroupingGrid ShowGroupingItemCount="True"
ItemsSource="{Binding Source={StaticResource GroupedSearchResults}}"
SelectedItem="{Binding SelectedItem}">
<DataGrid.Columns>
<!-- column definitions -->
</DataGrid.Columns>
</controls:GroupingGrid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
The designer gives me no squiggly lines anywhere, which seems to indicate everything is ok; I only get the binding error at run-time, however the ViewModels (serach results) populate exactly as expected... they just don't bind into the CollectionViewSource for some reason.
What am I doing wrong?
Here's a workaround based on http://www.broculos.net/2014/04/wpf-how-to-bind-collectionviewsource.html. It works by introducing a StackPanel and moving the CollectionViewSource to the StackPanel resources:
<DataTemplate DataType="controls:SearchResultsViewModel">
<StackPanel>
<StackPanel.Resources>
<CollectionViewSource x:Name="ViewSource" x:Key="GroupedSearchResults" Source="{Binding Test}">
<CollectionViewSource.SortDescriptions>
<componentModel:SortDescription PropertyName="QualifiedMemberName.QualifiedModuleName.Name" />
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="QualifiedMemberName.QualifiedModuleName.Name" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</StackPanel.Resources>
<controls:GroupingGrid ShowGroupingItemCount="True" x:Name="TabGrid"
ItemsSource="{Binding Source={StaticResource GroupedSearchResults}}"
SelectedItem="{Binding SelectedItem}">
<DataGrid.Columns>
<DataGridTextColumn Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=SearchResults_ModuleName}" Binding="{Binding QualifiedMemberName.QualifiedModuleName}" />
<DataGridTextColumn Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=SearchResults_MemberName}" Binding="{Binding QualifiedMemberName.MemberName}" />
<DataGridTextColumn Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=SearchResults_Location}" Binding="{Binding Selection}" Width="*" />
</DataGrid.Columns>
</controls:GroupingGrid>
</StackPanel>
</DataTemplate>
No idea why an all-XAML solution can't work. I gave up and got it working by exposing a CollectionViewSource member on the ViewModel:
public SearchResultsViewModel(string header, IEnumerable<SearchResultItem> searchResults)
{
_header = header;
_searchResults = new ObservableCollection<SearchResultItem>(searchResults);
_searchResultsSource = new CollectionViewSource();
_searchResultsSource.Source = _searchResults;
_searchResultsSource.GroupDescriptions.Add(new PropertyGroupDescription("QualifiedMemberName.QualifiedModuleName.Name"));
_searchResultsSource.SortDescriptions.Add(new SortDescription("QualifiedMemberName.QualifiedModuleName.Name", ListSortDirection.Ascending));
_closeCommand = new DelegateCommand(ExecuteCloseCommand);
}
// not really needed anymore
private readonly ObservableCollection<SearchResultItem> _searchResults;
public ObservableCollection<SearchResultItem> SearchResults { get { return _searchResults; } }
private readonly CollectionViewSource _searchResultsSource;
public CollectionViewSource SearchResultsSource { get { return _searchResultsSource; } }
Then in the XAML, I changed the GroupingGrid's ItemsSource binding to the View member of the new SearchResultsSource property, like this:
<controls:GroupingGrid ShowGroupingItemCount="True" x:Name="TabGrid"
ItemsSource="{Binding SearchResultsSource.View}"
SelectedItem="{Binding SelectedItem}">
And finally got my grid to populate:

Change DataTemplate to use depending on condition

I have 3 user controls
Control 1
Control 2
Control 3
I have a stack panel that contains an ItemsControl
<UserControl.Resources>
<DataTemplate x:Key="Template1">
<my:UserControl1 Height="117"/>
</DataTemplate>
<DataTemplate x:Key="Template2">
<my:UserControl3 Height="117"/>
</DataTemplate>
<DataTemplate x:Key="Template3">
<my:UserControl3 Height="117"/>
</DataTemplate>
</UserControl.Resources>
<StackPanel Name="stackPanel3" Orientation="Vertical" VerticalAlignment="Bottom" Width="Auto">
<ItemsControl ItemsSource="{Binding BlocksForMonth.Blocks}" ItemTemplate="{StaticResource Template1}">
</ItemsControl>
</StackPanel>
BlocksForMonths.Blocks is a list of view models. The Blocks class has a property called ClipType. If the clipType is 1, I want to use Template1. If its 2 I want to use Template 2. If its 3 I want to use Template 3
These templates contain different user controls
How can I do this through binding?
I have considered 1 template with the 3 controls, but I dont know how to bind the visibility?
In this XAML I am binding to a list not a single item
Paul
I would put the 3 controls in the same template and use Visibility to display the correct one. What I would do is build an IValueConverter to convert the deciding value (your case it's ClipType) and compare that to the ConverterParameter. If they are equal, return Visibility.Visible, otherwise return Visibility.Collapsed.
<UserControl.Resources>
<my:ClipTypeToVisibilityConverter x:Key="converter"/>
<DataTemplate x:Key="Template">
<StackPanel>
<my:UserControl1 Height="117" Visibility={Binding ClipType, Converter={StaticResource converter}, ConverterParameter=1} />
<my:UserControl2 Height="117" Visibility={Binding ClipType, Converter={StaticResource converter}, ConverterParameter=2} />
<my:UserControl3 Height="117" Visibility={Binding ClipType, Converter={StaticResource converter}, ConverterParameter=3} />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<StackPanel Name="stackPanel3" Orientation="Vertical" VerticalAlignment="Bottom" Width="Auto">
<ItemsControl ItemsSource="{Binding BlocksForMonth.Blocks}" ItemTemplate="{StaticResource Template}">
</ItemsControl>
</StackPanel>
This example assumes the ClipType property is on each item view model in the list being displayed.
Here is a C# example converter.
public class ClipTypeToVisibilityConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var clipType = value.ToString();
if (clipType == (string)parameter))
return Visibility.Visible;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Sorry, everything was air-code. But I think you get the idea.

How can I add additional item templates to an extended WPF treeview

I'm trying to set up a Treeview descendent class that can be used as a common template for
all Treeview instances in my application, but with additional formatting and templates for each instance.
For the base, I have a UserControl that descends from Treeview, with the common styles and a single standard data template
<TreeView x:Class="BaseTreeView" ... >
<TreeView.ItemContainerStyle> ... </TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Children}" DataType="{x:Type local:BaseTreeViewItem}">
<TextBlock Text="{Binding Caption}" />
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
Then in each window, I use this extended Treeview and add additional data templates for the specific TreeviewItems I'm displaying.
e.g.
<Window x:Class="Window1" ... >
...
<BaseTreeView ItemsSource="{Binding RootTreeItems}" >
<MyTreeView.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Children}" DataType="{x:Type ExtendedTreeViewItem1}">
<StackPanel Orientation="Horizontal">
<Image Source="Images/Image1.png" />
<TextBlock Text="{Binding Caption}" />
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type ExtendedTreeViewItem2}">
<StackPanel Orientation="Horizontal">
<Image Source="Images/Image2.png" />
<TextBlock Text="{Binding Caption}" />
</StackPanel>
</DataTemplate>
</MyTreeView.Resources>
</BaseTreeView>
...
</Window>
This compiles fine, but at runtime I get an error
"'Set property 'System.Windows.ResourceDictionary.DeferrableContent' threw an exception.' Line number '27' and line position '59'."
"Cannot re-initialize ResourceDictionary instance."
Is there any way around this, or can someone suggest a better way to set up a base treeview template and multiple descedent versions.
You could try moving your templates to the <Window.Resources> instead of <MyTreeView.Resources>
If it doesn't work, maybe using a DataTemplateSelector suits your case best. You can create a DataTemplateSelector class like this:
public class ExtendedTreeViewTemplateSelector : DataTemplateSelector
{
public DataTemplate ExtendedTreeViewItem1Template { get; set; }
public DataTemplate ExtendedTreeViewItem2Template { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is ExtendedTreeViewItem1)
return ExtendedTreeViewItem1Template;
if (item is ExtendedTreeViewItem2)
return ExtendedTreeViewItem2Template;
}
}
And then use it in your XAML like this:
<Window x:Class="Window1" ... >
<Window.Resources>
<HierarchicalDataTemplate x:Key="extendedTreeViewItem1Template" ItemsSource="{Binding Children}" DataType="{x:Type ExtendedTreeViewItem1}">
<StackPanel Orientation="Horizontal">
<Image Source="Images/Image1.png" />
<TextBlock Text="{Binding Caption}" />
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate x:Key="extendedTreeViewItem2Template" DataType="{x:Type ExtendedTreeViewItem2}">
<StackPanel Orientation="Horizontal">
<Image Source="Images/Image2.png" />
<TextBlock Text="{Binding Caption}" />
</StackPanel>
</DataTemplate>
<selector:ExtendedTreeViewTemplateSelector x:Key="treeViewTemplateSelector"
ExtendedTreeViewItem1Template="{StaticResource extendedTreeViewItem1Template}"
ExtendedTreeViewItem2Template="{StaticResource extendedTreeViewItem2Template}" />
</Window.Resources>
...
<BaseTreeView ItemsSource="{Binding RootTreeItems}"
ItemTemplateSelector={StaticResource treeViewTemplateSelector}" />
...
</Window>

Binding a Silverlight TabControl to a complex object using a converter

I am trying to bind my data to a tab control. I have got the headers displaying fine but I'm not sure how I get the content of the tabs to bind correctly based on my item template shown below.
I think I'm missing something when I'm creating the tab item but I'm not sure how to bind my MyCustomObject to each of the TabItem's.
XAML:
<sdk:TabControl ItemsSource="{Binding Singles,Converter={StaticResource TabConverter}}">
<sdk:TabControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</sdk:TabControl.ItemsPanel>
<sdk:TabControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding Converter={StaticResource RoundNumberConverter}}" Margin="2" />
<ListBox x:Name="Matches" ItemsSource="{Binding}" Margin="2">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Converter={StaticResource SeedingConverter}, ConverterParameter=true}" Margin="2" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Converter={StaticResource SeedingConverter}, ConverterParameter=false}" Margin="2" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=Player1Name}" Margin="2" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=Player2Name}" Margin="2" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</sdk:TabControl.ItemTemplate>
</sdk:TabControl>
Converter:
public class TabConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
IEnumerable<IGrouping<string, MyCustomObject>> source = value as IEnumerable<IGrouping<string, MyCustomObject>>;
if (source != null)
{
var controlTemplate = (ControlTemplate)parameter;
List<TabItem> result = new List<TabItem>();
foreach (IGrouping<string, MyCustomObject> tab in source)
{
result.Add(new TabItem()
{
Header = tab.Key,
DataContext = tab //not sure this is right?
});
}
return result;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
You're most of the way there, the following works for me:
Strip out your DataTemplate and drop its contents straight into a new UserControl, for the example here, lets call it MatchesView.
Then in your TabConverter, amend the contents of the foreach loop to something like the following:
TabItem tabitem = new TabItem();
tabitem.Header = tab.Key;
MatchesView tabview = new MatchesView();
tabview.DataContext = parameter;
tabitem.Content = tabview;
result.Add(tabitem);
Note: this requires that you pass your ViewModel to the TabConverter as a parameter, eg:
<sdk:TabControl SelectedItem="{Binding YourSelectedObject}" ItemsSource="{Binding YourCollectionObject, Converter={StaticResource TabConverter}, ConverterParameter={StaticResource YourViewModel}, Mode=TwoWay}" />
Then, as you have your ViewModel in each instance of the new control, adjust your binding accordingly!
Note that the trick is that you have a separate binding for the single instance of the Selected object
checking your problem I think there is no support for that. The WPF's TabItem contains ItemTemplate & ContentTemplate. The first is the template for the Header, the second is the template for the "body". In silverlight we still have no ContentTemplate. Haven't seen an official statement yet, but this guy says it won't be supported till SL5.

Refreshing a binding that uses a value converter

I have a WPF UI that is bound to an object. I'm using a ValueConverter to convert a property to a specific image by a business rule:
public class ProposalStateImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var proposal = value as Proposal;
var basePath = "pack://application:,,,/ePub.Content;component/Images/General/Flag_{0}.png";
string imagePath;
if(proposal.Invoice != null)
{
imagePath = string.Format(basePath, "Good");
}
else
{
imagePath = string.Format(basePath, "Warning");
}
var uri = new Uri(imagePath);
var src = uri.GetImageSource(); //Extention method
return src;
}
}
The element is a TreeView where the image is on the 2nd level:
<TreeView x:Name="tree"
ItemsSource="{Binding People}"
SelectedItemChanged="OnTreeItemChanged">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type dmn:Person}"
ItemsSource="{Binding Proposals}">
<StackPanel Orientation="Horizontal" ToolTip="{Binding Path=Fullname}" Margin="3">
<Image Margin="5,0,5,0" Width="16" Height="16" Source="pack://application:,,,/ePub.Content;component/Images/General/Person_Active.png" />
<TextBlock Text="{Binding Path=Firstname}" />
<TextBlock Text="{Binding Path=Lastname}" Margin="5,0,0,0" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type dmn:Proposal}">
<StackPanel Orientation="Horizontal" Margin="3">
<Image x:Name="invoiceImage" Width="16" Height="16" Margin="5,0,5,0" Source="{Binding, Converter={StaticResource ProposalStateImageConverter}, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding DeliveryDate, Converter={StaticResource textCulturedDateConverter}}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
It is working fine, but later, when the object's state changes, I want to refresh the image and make the value converter reevaluate. How is this possible?
It looks like you're only using a single value inside the converter and you're just doing a simple switch between two values so you could instead just do this directly in XAML with a trigger. This method also switches to a Binding against the Invoice property so that any change notifications for that property will cause the Trigger to update.
<HierarchicalDataTemplate >
<StackPanel Orientation="Horizontal" Margin="3">
<Image x:Name="invoiceImage" Width="16" Height="16" Margin="5,0,5,0" Source="good.png"/>
<TextBlock ... />
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding Invoice}" Value="{x:Null}">
<Setter TargetName="invoiceImage" Property="Source" Value="warning.png"/>
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
Assuming you can't use INotifyPropertyChanged because you're binding to the whole object, you need to call BindingExpression.UpdateTarget.
The slight subtlety is in getting hold of the binding expression. This requires you to have a fairly intimate knowledge of the view: as far as I know, the only way to do this is to call BindingOperations.GetBindingExpression, passing the control and property whose binding you want to update, e.g.:
BindingOperations.GetBindingExpression(myImage, Image.SourceProperty).UpdateTarget();

Resources