set Treeview HierarchicalDataTemplate via a property - c#

This one is a bit complicated. I'm trying to create a usercontrol which has a treeview and a few other controls, to create a reusable control that will be useful for other implementations.
The problem that I'm having is that I cannot figure out how to insert a HierarchicalDataTemplate defined outside the control into the treeview that is inside the control.
Outside the control here is my WPF
<Grid>
<Grid.Resources>
<HierarchicalDataTemplate x:Key="HierarchicalDataTemplate" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
</Grid.Resources>
<masterTreeUserControl:MasterTreeUserControl
HierarchicalDataTemplate="{StaticResource HierarchicalDataTemplate}"
ItemsSource="{Binding Path=SelectiveListViewModel.Items, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=SelectiveListViewModel.SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
EnableAdd="False"
ItemType="{x:Type viewmodels:LocationItem}"
/>
</Grid>
The MasterTreeUserControl has a HierarchicalDataTemplate DependencyProperty
HierarchicalDataTemplateProperty = DependencyProperty.Register("HierarchicalDataTemplate",typeof(HierarchicalDataTemplate),typeof(MasterTreeUserControl));
private static readonly DependencyProperty HierarchicalDataTemplateProperty;
public HierarchicalDataTemplate HierarchicalDataTemplate
{
get
{
return (HierarchicalDataTemplate)GetValue(HierarchicalDataTemplateProperty);
}
set
{
SetValue(HierarchicalDataTemplateProperty, value);
}
}
And so far the Treeview inside the control looks like this.
<TreeView Name="ItemListView"
Grid.Row="2"
Margin="0,5,0,0"
ItemsSource="{Binding Source={StaticResource ItemsCvs}}">
In other implementations of a treeview what I would normally do is HierarchicalDataTemplate in a fashion similar to this.
<TreeView.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="Hello"/>
</HierarchicalDataTemplate>
</TreeView.Resources>
However, as the HierarchicalDataTemplate is inside a dependency property, I need to somehow bind it into the Treeview. I've had a look a round the internet (and will continue doing so) but can't having found anything relevent.
How can you inject a dependency property containing the HierarchicalDataTemplate into the treeview?

Following should work:
Add PropertyChangedCallback to your HierarchicalDataTemplateProperty.
In the handler, add template to control resources:
private static void OnTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var This = d as MasterTreeUserControl;
var template = e.NewValue as HierarchicalDataTemplate;
if(template != null)
{
This.ItemListView.Resources[new DataTemplateKey(template.DataType)] = template;
}
}
The only issue is that your HierarchicalDataTemplate has to have DataType set to your templated type. You should enforce that in some way (validation etc..)

Related

Remember WPF DataGrid sort order

This is a continuation of this question.
1. Setup
I have a wpf window that contains a TabControl with dynamically created items. The ItemSource of this TabControl is bound to a list of Groups. These groups contain a list of elements that is displayed in a DataGrid on the TabPage.
XAML:
<Window x:Name="window" x:Class="TestWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:TestWpf"
Title="MainWindow" Height="350" Width="525">
<TabControl x:Name="tabControl" BorderThickness="0" ItemsSource ="{Binding Groups, ElementName=window, NotifyOnSourceUpdated=True}">
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:Group}">
<TextBlock Padding="2" Text="{Binding Name}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate x:Name="contentTemplate" DataType="{x:Type vm:Group}">
<DataGrid x:Name="dgElements" ItemsSource="{Binding Elements, BindsDirectlyToSource=True}" DockPanel.Dock="Top" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTextColumn x:Name="clmName" Header="Name" Binding="{Binding Name}" IsReadOnly="True" CanUserReorder="False" />
<DataGridTextColumn x:Name="clmDesc" Header="Description" Binding="{Binding Description}" IsReadOnly="True" CanUserReorder="False" />
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Window>
This all works fine.
2. The Question
The DataGrid allows multi-column sort out of the box. But unfortunatly, if I switch between the tabpages, the sorting is lost.
I want that the multi-column sorting is maintained for each tabpage/datagrid.
It is possible to bind the SortDirection of DataGridTextColumn to a property, but that does not remember the sort order of the columns. And debugging I found that when I switch the tabpage, the SortDirection is reset before the new ItemSource is set, so I can't store this.
The question is: How can I keep the multi-column sort settings per tabpage/datagrid?
After reading a lot of articles only almost solving my problem, I finally found a solution.
I inherited my own class from DataGrid and stored the SortDescriptions per ItemSource in a Dictionary:
public class SortKeepingDataGrid : DataGrid
{
// Dictionary to keep SortDescriptions per ItemSource
private readonly Dictionary<object, List<SortDescription>> m_SortDescriptions =
new Dictionary<object, List<SortDescription>>();
protected override void OnSorting(DataGridSortingEventArgs eventArgs)
{
base.OnSorting(eventArgs);
UpdateSorting();
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
ICollectionView view = CollectionViewSource.GetDefaultView(newValue);
view.SortDescriptions.Clear();
// reset SortDescriptions for new ItemSource
if (m_SortDescriptions.ContainsKey(newValue))
foreach (SortDescription sortDescription in m_SortDescriptions[newValue])
{
view.SortDescriptions.Add(sortDescription);
// I need to tell the column its SortDirection,
// otherwise it doesn't draw the triangle adornment
DataGridColumn column = Columns.FirstOrDefault(c => c.SortMemberPath == sortDescription.PropertyName);
if (column != null)
column.SortDirection = sortDescription.Direction;
}
}
// Store SortDescriptions in dictionary
private void UpdateSorting()
{
ICollectionView view = CollectionViewSource.GetDefaultView(ItemsSource);
m_SortDescriptions[ItemsSource] = new List<SortDescription>(view.SortDescriptions);
}
}
So, basically, whenever the user changes the sorting, I call UpdateSorting and store the current SortDescriptions in the per ItemSource dictionary.
When the ItemSource has changed, I look up the SortDescriptions and reset them in the correct order.
The tricky part is finding the correct DataGridColumn to set its SortDirection. This is necessary to draw the triangled adornment. I here rely on the equality of SortMemberPath and PropertyName. Eventually a more generic approach could be necessary.
In the XAML I replaced the DataGrid with my SortKeepingDataGrid and now the sorting is stored per tabpage.
Since I could not find any other solution, maybe this will help others too.

Best way to bind a set of same-type ViewModels to a TabControl in MVVM / WPF

I have an existing ViewModel and View in an MVVM project. Effectively this View presents a collection of items in a particular, styled way. I'll call this existing ViewModel "CollectionPresenter".
Up to now, this has been presented as as follows in XAML:
<Grid>
<ns:CollectionPresenter />
</Grid>
Now, I want to have a dynamic collection of these "CollectionPresenter" view models made available ideally in a tab view.
My approach has been to define an observable collection of these "CollectionPresenters", creating them first on construction of the parent view model. The XAML above then changed to look something like this:
<TabControl ItemsSource="{TemplateBinding CollectionPresenters}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding CollectionPresenterTitle}">
</DataTemplate>
<TabControl.ItemTemplate>
<TabControl.ContentTemplate>
... this is where things get confusing
</TabControl.ContentTemplate>
<TabControl>
You can see above my problem is the ContentTemplate.
When I load this up, I get a tab control and it has as many tabs as my observable collection of "CollectionPresenter" objects.
However, the content of the tab control is always empty.
Is this approach correct - and is there a better way regardless?
EDIT: ADDING SOME EXTRA THINGS TO MAKE IT CLEARER
I've tried the below, but it doesn't work. The XAML with the Tab Control (the binding to "Things" works fine):
<TabControl ItemsSource="{TemplateBinding Things}">
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModels:Thing}">
<TextBlock Text="{Binding ThingName}" Width="200" Background="Blue" Foreground="White"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type viewModels:Thing}">
<TextBlock Text="{Binding ThingName}" Width="500" Height="500" Background="Blue" Foreground="White"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
The definition for the "Things" observable collection (which is inside the templated parent (ParentObject) of the XAML with the tab control):
public static readonly DependencyProperty ThingsProperty =
DependencyProperty.Register("Things", typeof(ObservableCollection<Thing>), typeof(ParentObject), new PropertyMetadata(null));
public ObservableCollection<Thing> Things
{
get { return (ObservableCollection<Thing>)GetValue(ThingsProperty); }
set { SetValue(ThingsProperty, value); }
}
Stripped down version of the "Thing" view model:
public class Thing : ViewModelBase
{
public Thing()
{
}
public void Initialise(ObservableCollection<Thing> things, string thingName)
{
Things = things;
ThingName = thingName;
}
public static readonly DependencyProperty ThingNameProperty =
DependencyProperty.Register("ThingName", typeof(string), typeof(Thing), new PropertyMetadata(null));
public string ThingName
{
get { return (string)GetValue(ThingNameProperty); }
set { SetValue(ThingNameProperty, value); }
}
}
Looking at my answer to the WPF MVVM navigate views question, you can see this:
<DataTemplate DataType="{x:Type ViewModels:MainViewModel}">
<Views:MainView />
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModels:PersonViewModel}">
<Views:PersonView />
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModels:CompanyViewModel}">
<Views:CompanyView />
</DataTemplate>
Now, wherever we use an instance from one of these types in our application, these DataTemplates will tell the framework to display the related view instead.
Therefore, your solution is to simply not hard-code one single DataTemplate to the TabControl.ItemTemplate property, but to leave that blank instead. If you use multiple DataTemplates without providing x:Key values, then they will implicitly be applied when each data object is to be rendered in the TabControl.
UPDATE >>>
Using these DataTemplates should leave your TabControl looking like this:
<TabControl ItemsSource="{TemplateBinding Things}" />
I'm not sure why you're using a TemplateBinding there though as you don't need to define any new templates to get this working... therefore, you should be using a plain old Binding instead.
One other thing that you need to do is to use different data types for each item in the collection that you want to display differently. You could derive custom classes from your Thing class and so the collection could still be of type ObservableCollection<Thing>.

Partial TreeView refresh after EF model ItemsSource update

I have an Entity Framework model and TreeView which has a binding with model.
in constructor:
Context.TestCategory.Load();
my TreeView:
<TreeView x:Name="DbTree" ItemsSource="{Binding Context.TestCategory.Local}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Tests}" >
<TextBlock Text="{Binding Name}" ContextMenuOpening="ContextMenu_ContextMenuOpening">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="Добавить тест" Click="TestAdd" CommandParameter="{Binding
RelativeSource={RelativeSource
AncestorType={x:Type ContextMenu}}}"></MenuItem>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
........................................
other items
........................................
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
After updating items in ItemsSource I need to update the related TreeView branch, what is the best way to do this?
I'm not sure binding your ItemsSource directly to a context property is a great thing to do.
What you're supposed to do with binding is bind to a property that can do RaisePropertyChanged when it has changed, and then the UI elements will update if needed. I use MVVM so my binding is all from a ViewModel which is the datacontext for the View, and which implements the INotifyPropertyChanged interface. Then within that my property setter can look like this:
private List<TestCategories>_myItemSourceList;
public List<TestCategories> MyItemSourceList
{
get { return _myItemSourceList; }
set
{
if (value != _myItemSourceList)
{
_myItemSourceList= value;
RaisePropertyChanged(() => MyItemSourceList);
}
}
}
And the XAML would look like ...
<TreeView x:Name="DbTree" ItemsSource="{Binding MyItemSourceList}">
Obviously because you're not using MVVM your code will be slightly different, but the principle remains: bind your ItemSource to a property that can RaisePropertyChanged.
EDIT: My RaisePropertyChanged is within the Prism framework, so is using a lambda, but the 'standard' was is with a string like this RaisePropertyChanged("MyItemSourceList")

How to set a PlacementTarget for a WPF tooltip without messing up the DataContext?

I have a typical MVVM setup of Listbox and vm + DataTemplate and item vm's. The data templates have tooltips, which have elements bound to the item vm's. All works great.
Now, I'd like to have the tooltip placed relative to the listbox itself. It's fairly large and gets in the way when casually mousing over the listbox. So I figured I'd do something like this in the DataTemplate:
<Grid ...>
<TextBlock x:Name="ObjectText"
ToolTipService.Placement="Left"
ToolTip="{StaticResource ItemToolTip}"
ToolTipService.PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}">
</TextBlock>
...
...with the static resource...
<ToolTip x:Key="ItemToolTip">
<StackPanel>
<TextBlock Text="{Binding DisplayName.Name}"/>
<TextBlock Text="{Binding Details}" FontStyle="Italic"/>
...
</StackPanel>
</ToolTip>
Here's my problem. When I use that PlacementTarget I get a binding error that the DisplayName.Name and Details are not binding. The object it's trying to bind to is not the item vm but the overall Listbox vm.
So my question is: how can I set the ToolTipService.PlacementTarget for a tooltip yet keep the DataContext inherited from its owner?
Ok, a friend at work mostly figured it out for me. This way is super clean, doesn't feel hacky.
Here's the basic problem: as user164184 mentioned, tooltips are popups and therefore not part of the visual tree. So there's some magic that WPF does. The DataContext for the popup comes from the PlacementTarget, which is how the bindings work most of the time, despite the popup not being part of the tree. But when you change the PlacementTarget this overrides the default, and now the DataContext is coming from the new PlacementTarget, whatever it may be.
Totally not intuitive. It would be nice if MSDN had, instead of spending hours building all those pretty graphs of where the different tooltips appear, said one sentence about what happens with the DataContext.
Anyway, on to the SOLUTION! As with all fun WPF tricks, attached properties come to the rescue. We're going to add two attached properties so we can directly set the DataContext of the tooltip when it's generated.
public static class BindableToolTip
{
public static readonly DependencyProperty ToolTipProperty = DependencyProperty.RegisterAttached(
"ToolTip", typeof(FrameworkElement), typeof(BindableToolTip), new PropertyMetadata(null, OnToolTipChanged));
public static void SetToolTip(DependencyObject element, FrameworkElement value) { element.SetValue(ToolTipProperty, value); }
public static FrameworkElement GetToolTip(DependencyObject element) { return (FrameworkElement)element.GetValue(ToolTipProperty); }
static void OnToolTipChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
{
ToolTipService.SetToolTip(element, e.NewValue);
if (e.NewValue != null)
{
((ToolTip)e.NewValue).DataContext = GetDataContext(element);
}
}
public static readonly DependencyProperty DataContextProperty = DependencyProperty.RegisterAttached(
"DataContext", typeof(object), typeof(BindableToolTip), new PropertyMetadata(null, OnDataContextChanged));
public static void SetDataContext(DependencyObject element, object value) { element.SetValue(DataContextProperty, value); }
public static object GetDataContext(DependencyObject element) { return element.GetValue(DataContextProperty); }
static void OnDataContextChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
{
var toolTip = GetToolTip(element);
if (toolTip != null)
{
toolTip.DataContext = e.NewValue;
}
}
}
And then in the XAML:
<Grid ...>
<TextBlock x:Name="ObjectText"
ToolTipService.Placement="Left"
ToolTipService.PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
mystuff:BindableToolTip.DataContext="{Binding}">
<mystuff:BindableToolTip.ToolTip>
<ToolTip>
<StackPanel>
<TextBlock Text="{Binding DisplayName.Name}"/>
<TextBlock Text="{Binding Details}" FontStyle="Italic"/>
...
</StackPanel>
</ToolTip>
</mystuff:BindableToolTip.ToolTip>
</TextBlock>
...
Just switch the ToolTip over to BindableToolTip.ToolTip instead, then add a new BindableToolTip.DataContext that points at whatever you want. I'm just setting it to the current DataContext, so it ends up inheriting the viewmodel bound to the DataTemplate.
Note that I embedded the ToolTip instead of using a StaticResource. That was a bug in my original question. Obviously has to be generated unique per item. Another option would be to use a ControlTemplate Style trigger thingy.
One improvement could be to have BindableToolTip.DataContext register for notifications on the ToolTip changing, then I could get rid of BindableToolTip.ToolTip. A task for another day!
ToolTips are not part of the visual tree as they are popup based. So your placement target biding (which uses Visual Tree search) to get the relative ancestor wont work. Why not use ContentHacking instead? This way one hacks into the visual tree from the logical elements such as ContextMenu, Popups, ToolTip etc...
Declare a StaticResource which is any FrameworkElement (we need support for data context).
<UserControl.Resources ...>
<TextBlock x:Key="ProxyElement" DataContext="{Binding}" />
</UserControl.Resources>
Supply a content control in the Visual Tree and set this static resource "ProxyElement" as its content.
<UserControl ...>
<Grid ...>
<ItemsControl x:Name="MyItemsControl"
ItemsTemplate="{StaticResource blahblah}" .../>
<ContentControl Content="{StaticResource ProxyElement}"
DataContext="{Binding ElementName=MyItemsControl}" Visibility="Collapsed"/>
What the above steps have done that "ProxyElement" has been connected to the ItemsControl (which serves as a DataContext) and it is available as a SaticResource to be used anywhere.
Now use this StaticResource as a source for any bindings which are failing in your tooltip...
<Grid ...>
<TextBlock x:Name="ObjectText"
ToolTipService.Placement="Left"
ToolTip="{StaticResource ItemToolTip}"
PlacementTarget="{Binding Source={StaticResource ProxyElement}, Path=DataContext}" ... /> <!-- This sets the target as the items control -->
and
<ToolTip x:Key="ItemToolTip">
<StackPanel DataContext="{Binding Source={StaticResource ProxyElement}, Path=DataContext.DataContext}"><!-- sets data context of the items control -->
<TextBlock Text="{Binding DisplayName.Name}"/>
<TextBlock Text="{Binding Details}" FontStyle="Italic"/> ...
</StackPanel>
</ToolTip>
Let me know if this helps...
As I understand [but I probably wrong (no harm in trying)], you can initialize your items with reference to objects which were used in ancestor DataContext, i.e.
public class ItemsVM<T> : VMBase
{
public T parentElement;
public ItemsVM (T _parentElement)
{
this.parentElement = _parentElement;
}
...
}

WPF Databinding Treeview to List does not update

I have a List bound to a TreeView like:
XAML:
<TreeView Name="browserTree"
BorderBrush="DarkSlateGray"
BorderThickness="1"
Grid.Row="2"
Margin="0,3,0,0"
ItemsSource="{Binding UpdateSourceTrigger=PropertyChanged}">
<TreeView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
C#:
browserTree.DataContext = treeList;
I update the list via:
void QueryChange(string s)
{
rCM.SetCommand(s);
treeList.Clear();
SqlDataReader sr = rCM.ExecuteReader(System.Data.CommandBehavior.Default);
while (sr.Read())
{
treeList.Add((string)sr["tree_hdr"]);
}
sr.Close();
}
The List<string> is just a placeholder at the moment for a more meaningful data class I have yet to implement. But right now I need to know why the TreeView is not updating to reflect the changes made to the list.
Try making the treelist an ObservableCollection.
Please check the type of your treeList which you set as DataContext. It has to be an ObservableCollection to reflect your collection changes in the UI
Or else for quick workaround, just set the DataContext again after you filled the List.

Categories

Resources