I have an application which shows a single View at a time in a ContentControl. I have a current solution, but was curious if there is a better one for memory management.
My current design creates new objects when they need to be displayed, and destroys them when they are no longer visible. I'm curious if this is the better approach, or maintaining references to each view and swapping between those references is better?
Here is a little more explanation of my application layout:
A very simplified version of my MainWindow.xaml looks like this:
<Window ... >
<Window.Resources>
<DataTemplate DataType="{x:Type vm:SplashViewModel}">
<view:SplashView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:MediaPlayerViewModel}">
<view:MediaPlayerView />
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content="{Binding ActiveModule}" />
</Grid>
</Window>
In my MainViewModel.cs I swap the ActiveModule parameter with a newly initialized ViewModels. For example, my pseudo-code logic check for swapping content would be something like:
if (logicCheck == "SlideShow")
ActiveModule = new SlideShowViewModel();
else if (logicCheck == "MediaPlayer")
ActiveModule = new MediaPlayerViewModel();
else
ActiveModule = new SplashScreenViewModel();
But, would just maintaining a reference be more appropriate in speed and memory usage?
Alt Option 1: Create static references to each ViewModel and swap between them...
private static ViewModelBase _slideShow = new SlideShowViewModel();
private static ViewModelBase _mediaPlayer = new MediaPlayerViewModel();
private static ViewModelBase _splashView = new SplashScreenViewModel();
private void SwitchModule(string logicCheck) {
if (logicCheck == "SlideShow")
ActiveModule = _slideShow;
else if (logicCheck == "MediaPlayer")
ActiveModule = _mediaPlayer;
else
ActiveModule = _splashView;
}
I'm not constantly creating/destroying here, but this approach appears to me to be wasteful of memory with unused modules just hanging out. Or... is there something special WPF is doing behind the scenes that avoids this?
Alt Option 2: Place each available module in the XAML and show/hide them there:
<Window ... >
<Grid>
<view:SplashScreenView Visibility="Visible" />
<view:MediaPlayerView Visibility="Collapsed" />
<view:SlideShowView Visibility="Collapsed" />
</Grid>
</Window>
Again, I'm curious about what memory management might be happening in the background that I'm not familiar with. When I collapse something, does it go fully into a sort of hibernation? I've read that some stuff does (no hittesting, events, key inputs, focus, ...) but what about animations and other stuff?
Thanks for any input!
I ran into that kind of situation once where my Views were pretty expensive to create, so I wanted to store them in memory to avoid having to re-create them anytime the user switched back and forth.
My end solution was to reuse an extended TabControl that I use to accomplish the same behavior (stop WPF from destroying TabItems when switching tabs), which stores the ContentPresenter when you switch tabs, and reloads it if possible when you switch back.
The only thing I needed to change was I had to overwrite the TabControl.Template so the only thing displaying was the actual SelectedItem part of the TabControl
My XAML ends up looking something like this:
<local:TabControlEx ItemsSource="{Binding AvailableModules}"
SelectedItem="{Binding ActiveModule}"
Template="{StaticResource BlankTabControlTemplate}" />
and the actual code for the extended TabControl looks like this:
// Extended TabControl which saves the displayed item so you don't get the performance hit of
// unloading and reloading the VisualTree when switching tabs
// Obtained from http://www.pluralsight-training.net/community/blogs/eburke/archive/2009/04/30/keeping-the-wpf-tab-control-from-destroying-its-children.aspx
// and made a some modifications so it reuses a TabItem's ContentPresenter when doing drag/drop operations
[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class TabControlEx : System.Windows.Controls.TabControl
{
// Holds all items, but only marks the current tab's item as visible
private Panel _itemsHolder = null;
// Temporaily holds deleted item in case this was a drag/drop operation
private object _deletedObject = null;
public TabControlEx()
: base()
{
// this is necessary so that we get the initial databound selected item
this.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
/// <summary>
/// if containers are done, generate the selected item
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
UpdateSelectedItem();
}
}
/// <summary>
/// get the ItemsHolder and generate any children
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel;
UpdateSelectedItem();
}
/// <summary>
/// when the items change we remove any generated panel children and add any new ones as necessary
/// </summary>
/// <param name="e"></param>
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (_itemsHolder == null)
{
return;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Reset:
_itemsHolder.Children.Clear();
if (base.Items.Count > 0)
{
base.SelectedItem = base.Items[0];
UpdateSelectedItem();
}
break;
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
// Search for recently deleted items caused by a Drag/Drop operation
if (e.NewItems != null && _deletedObject != null)
{
foreach (var item in e.NewItems)
{
if (_deletedObject == item)
{
// If the new item is the same as the recently deleted one (i.e. a drag/drop event)
// then cancel the deletion and reuse the ContentPresenter so it doesn't have to be
// redrawn. We do need to link the presenter to the new item though (using the Tag)
ContentPresenter cp = FindChildContentPresenter(_deletedObject);
if (cp != null)
{
int index = _itemsHolder.Children.IndexOf(cp);
(_itemsHolder.Children[index] as ContentPresenter).Tag =
(item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
}
_deletedObject = null;
}
}
}
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
_deletedObject = item;
// We want to run this at a slightly later priority in case this
// is a drag/drop operation so that we can reuse the template
this.Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
new Action(delegate()
{
if (_deletedObject != null)
{
ContentPresenter cp = FindChildContentPresenter(_deletedObject);
if (cp != null)
{
this._itemsHolder.Children.Remove(cp);
}
}
}
));
}
}
UpdateSelectedItem();
break;
case NotifyCollectionChangedAction.Replace:
throw new NotImplementedException("Replace not implemented yet");
}
}
/// <summary>
/// update the visible child in the ItemsHolder
/// </summary>
/// <param name="e"></param>
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
UpdateSelectedItem();
}
/// <summary>
/// generate a ContentPresenter for the selected item
/// </summary>
void UpdateSelectedItem()
{
if (_itemsHolder == null)
{
return;
}
// generate a ContentPresenter if necessary
TabItem item = GetSelectedTabItem();
if (item != null)
{
CreateChildContentPresenter(item);
}
// show the right child
foreach (ContentPresenter child in _itemsHolder.Children)
{
child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
}
}
/// <summary>
/// create the child ContentPresenter for the given item (could be data or a TabItem)
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
ContentPresenter CreateChildContentPresenter(object item)
{
if (item == null)
{
return null;
}
ContentPresenter cp = FindChildContentPresenter(item);
if (cp != null)
{
return cp;
}
// the actual child to be added. cp.Tag is a reference to the TabItem
cp = new ContentPresenter();
cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
cp.ContentTemplate = this.SelectedContentTemplate;
cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
cp.ContentStringFormat = this.SelectedContentStringFormat;
cp.Visibility = Visibility.Collapsed;
cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
_itemsHolder.Children.Add(cp);
return cp;
}
/// <summary>
/// Find the CP for the given object. data could be a TabItem or a piece of data
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
ContentPresenter FindChildContentPresenter(object data)
{
if (data is TabItem)
{
data = (data as TabItem).Content;
}
if (data == null)
{
return null;
}
if (_itemsHolder == null)
{
return null;
}
foreach (ContentPresenter cp in _itemsHolder.Children)
{
if (cp.Content == data)
{
return cp;
}
}
return null;
}
/// <summary>
/// copied from TabControl; wish it were protected in that class instead of private
/// </summary>
/// <returns></returns>
protected TabItem GetSelectedTabItem()
{
object selectedItem = base.SelectedItem;
if (selectedItem == null)
{
return null;
}
if (_deletedObject == selectedItem)
{
}
TabItem item = selectedItem as TabItem;
if (item == null)
{
item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
}
return item;
}
}
Also I'm not positive, but I think my blank TabControl template looked something like this:
<Style x:Key="BlankTabControlTemplate" TargetType="{x:Type local:TabControlEx}">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TabControlEx}">
<DockPanel>
<!-- This is needed to draw TabControls with Bound items -->
<StackPanel IsItemsHost="True" Height="0" Width="0" />
<Grid x:Name="PART_ItemsHolder" />
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
You could choose to continue with your current approach; provided :
ViewModel objects are easy/ light weight to construct. This is possible if you inject internal details of object from outside(Following Dependency Injection principle).
You can buffer/ store the internal details, and then inject as and when you construct view model objects.
Implement IDisposable on your viewmodels to make sure you clear intenal details during disposal.
One of the disadvantages of keeping viewmodels cached in memory is their binding. If you have to stop binding notifications to flow between view and viewmodel when view goes out of scope, then set viewmodel to null. If viewmodel construction is light weighted, you can quickly construct the viewmodels and assign back to view datacontext.
You can then cache the views as shown in approach 2. I believe there is no point constructing view repeatedly if viewmodels are plugged with proper data. When you set viewmodel to null, due to binding datacontext view will get all bindings cleanedup. Later on setting new viewmodel as datacontext, view will load with new data.
Note: Make sure viewmodels get disposed properly without memory leaks. Use SOS.DLLto keep check on viewmodel instance count through visual studio debugging.
Another option to consider: Is this a scenario for using something like an IoC container and a Dependency Injection framework? Often times the DI framework supports container managed lifetimes for objects. I suggest looking at Unity Application Block or at MEF if they strike your fancy.
Related
Main aim
I want to make my own UserControl in WPF, based on TreeView. My aim is to make so, to have the opportunity to change SelectedItem from code.
One way to solve
In MVVM pattern you can make "IsSelected" property in TreeItemViewModel and bind "IsSelected" in ItemContainerStyle like this:
XAML:
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded}"/>
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</TreeView.ItemContainerStyle>
TreeItemViewModel:
private bool _isExpanded;
public bool IsExpanded { get { return _isExpanded; } set { _isExpanded = value; OnPropertyChanged("IsExpanded"); } }
private bool _isSelected;
public bool IsSelected { get { return _isESelected; } set { _isSelected = value; OnPropertyChanged("IsSelected"); } }
By doing like this, you can watch through all your ObservableCollection<TreeItemViewModel> that you bind to ItemsSource, find needed element and change "IsExpanded" and "IsSelected" for it's parents.
What I want
I want my UserControl to have all this bindings inside it. My UserControl will inherits from TreeView, and I will make my own MyItemsSource that will take IEnumerable (like the original ItemsSource in TreeView). In my point of view, next stage of my plan is to wrap objects from IEnumerable, in new class that will have two more properties: IsSelected and IsExpanded. And then to bind this properties inside my UserControl.
Why I want this
Due to this in my future projects i want to be able not two add this two properties and change SelectedItem from code.
Main Question
How Can I wrap objects that I get from IEnumerable (without knowing class that i get, because it is UserControl) in new class with two additional properties?
More explanations
UserControl class:
public partial class UserControl : TreeView
{
public UserControl()
{
InitializeComponent();
}
public System.Collections.IEnumerable MyItemsSource
{
set
{
ObservableCollection<UserControlTreeItemViewModel> ItemsSourceWrapped = new ObservableCollection<UserControlTreeItemViewModel>();
// wrap objects in cycle
foreach(var item in value)
{
ItemsSourceWrapped.Add(new UserControlTreeItemViewModel(item));
}
this.ItemsSource = ItemsSourceWrapped;
}
}
}
And class to wrap objects:
public class UserControlTreeItemViewModel : Object
{
public UserControlTreeItemViewModel(object i)
{
// How to write constructor to wrap object that you get?
}
public bool IsSelected { get; set; }
public bool IsExpanded { get; set; }
}
Main question: Is there any way to wrap objects that you get from IEnumerable?
This is the base ViewModel class that I use for data items to be displayed in a TreeView control. It handles things like the child item collection, expansion, selection, checking (tick box) including updating parent and child items, disabled state, lazy loading of child items.
public class perTreeViewItemViewModelBase : perViewModelBase
{
// a dummy item used in lazy loading mode, ensuring that each node has at least one child so that the expand button is shown
private static perTreeViewItemViewModelBase LazyLoadingChildIndicator { get; }
= new perTreeViewItemViewModelBase { Caption = "Loading Data ..." };
private bool InLazyLoadingMode { get; set; }
private bool LazyLoadTriggered { get; set; }
private bool LazyLoadCompleted { get; set; }
private bool RequiresLazyLoad => InLazyLoadingMode && !LazyLoadTriggered;
// Has Children been overridden (e.g. to point at some private internal collection)
private bool LazyLoadChildrenOverridden => InLazyLoadingMode && !Equals(LazyLoadChildren, _childrenList);
private readonly perObservableCollection<perTreeViewItemViewModelBase> _childrenList
= new perObservableCollection<perTreeViewItemViewModelBase>();
/// <summary>
/// LazyLoadingChildIndicator ensures a visible expansion toggle button in lazy loading mode
/// </summary>
protected void SetLazyLoadingMode()
{
ClearChildren();
_childrenList.Add(LazyLoadingChildIndicator);
IsExpanded = false;
InLazyLoadingMode = true;
LazyLoadTriggered = false;
LazyLoadCompleted = false;
}
private string _caption;
public string Caption
{
get => _caption;
set => Set(nameof(Caption), ref _caption, value);
}
public void ClearChildren()
{
_childrenList.Clear();
}
/// <summary>
/// Add a new child item to this TreeView item
/// </summary>
/// <param name="child"></param>
public void AddChild(perTreeViewItemViewModelBase child)
{
if (LazyLoadChildrenOverridden)
{
throw new InvalidOperationException("Don't call AddChild for an item with LazyLoad mode set & LazyLoadChildren has been overridden");
}
if (_childrenList.Any() && _childrenList.First() == LazyLoadingChildIndicator)
{
_childrenList.Clear();
}
_childrenList.Add(child);
SetChildPropertiesFromParent(child);
}
protected void SetChildPropertiesFromParent(perTreeViewItemViewModelBase child)
{
child.Parent = this;
// if this node is checked then all new children added are set checked
if (IsChecked.GetValueOrDefault())
{
child.SetIsCheckedIncludingChildren(true);
}
ReCalculateNodeCheckState();
}
protected void ReCalculateNodeCheckState()
{
var item = this;
while (item != null)
{
if (item.Children.Any() && !Equals(item.Children.FirstOrDefault(), LazyLoadingChildIndicator))
{
var hasIndeterminateChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.HasValue);
if (hasIndeterminateChild)
{
item.SetIsCheckedThisItemOnly(null);
}
else
{
var hasSelectedChild = item.Children.Any(c => c.IsEnabled && c.IsChecked.GetValueOrDefault());
var hasUnselectedChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.GetValueOrDefault());
if (hasUnselectedChild && hasSelectedChild)
{
item.SetIsCheckedThisItemOnly(null);
}
else
{
item.SetIsCheckedThisItemOnly(hasSelectedChild);
}
}
}
item = item.Parent;
}
}
private void SetIsCheckedIncludingChildren(bool? value)
{
if (IsEnabled)
{
_isChecked = value;
RaisePropertyChanged(nameof(IsChecked));
foreach (var child in Children)
{
if (child.IsEnabled)
{
child.SetIsCheckedIncludingChildren(value);
}
}
}
}
private void SetIsCheckedThisItemOnly(bool? value)
{
_isChecked = value;
RaisePropertyChanged(nameof(IsChecked));
}
/// <summary>
/// Add multiple children to this TreeView item
/// </summary>
/// <param name="children"></param>
public void AddChildren(IEnumerable<perTreeViewItemViewModelBase> children)
{
foreach (var child in children)
{
AddChild(child);
}
}
/// <summary>
/// Remove a child item from this TreeView item
/// </summary>
public void RemoveChild(perTreeViewItemViewModelBase child)
{
_childrenList.Remove(child);
child.Parent = null;
ReCalculateNodeCheckState();
}
public perTreeViewItemViewModelBase Parent { get; private set; }
private bool? _isChecked = false;
public bool? IsChecked
{
get => _isChecked;
set
{
if (Set(nameof(IsChecked), ref _isChecked, value))
{
foreach (var child in Children)
{
if (child.IsEnabled)
{
child.SetIsCheckedIncludingChildren(value);
}
}
Parent?.ReCalculateNodeCheckState();
}
}
}
private bool _isExpanded;
public bool IsExpanded
{
get => _isExpanded;
set
{
if (Set(nameof(IsExpanded), ref _isExpanded, value) && value && RequiresLazyLoad)
{
TriggerLazyLoading();
}
}
}
private bool _isEnabled = true;
public bool IsEnabled
{
get => _isEnabled;
set => Set(nameof(IsEnabled), ref _isEnabled, value);
}
public void TriggerLazyLoading()
{
var unused = DoLazyLoadAsync();
}
private async Task DoLazyLoadAsync()
{
if (LazyLoadTriggered)
{
return;
}
LazyLoadTriggered = true;
var lazyChildrenResult = await LazyLoadFetchChildren()
.EvaluateFunctionAsync()
.ConfigureAwait(false);
LazyLoadCompleted = true;
if (lazyChildrenResult.IsCompletedOk)
{
var lazyChildren = lazyChildrenResult.Data;
foreach (var child in lazyChildren)
{
SetChildPropertiesFromParent(child);
}
// If LazyLoadChildren has been overridden then just refresh the check state (using the new children)
// and update the check state (in case any of the new children is already set as checked)
if (LazyLoadChildrenOverridden)
{
ReCalculateNodeCheckState();
}
else
{
AddChildren(lazyChildren); // otherwise add the new children to the base collection.
}
}
RefreshChildren();
}
/// <summary>
/// Get the children for this node, in Lazy-Loading Mode
/// </summary>
/// <returns></returns>
protected virtual Task<perTreeViewItemViewModelBase[]> LazyLoadFetchChildren()
{
return Task.FromResult(new perTreeViewItemViewModelBase[0]);
}
/// <summary>
/// Update the Children property
/// </summary>
public void RefreshChildren()
{
RaisePropertyChanged(nameof(Children));
}
/// <summary>
/// In LazyLoading Mode, the Children property can be set to something other than
/// the base _childrenList collection - e.g as the union ot two internal collections
/// </summary>
public IEnumerable<perTreeViewItemViewModelBase> Children => LazyLoadCompleted
? LazyLoadChildren
: _childrenList;
/// <summary>
/// How are the children held when in lazy loading mode.
/// </summary>
/// <remarks>
/// Override this as required in descendent classes - e.g. if Children is formed from a union
/// of multiple internal child item collections (of different types) which are populated in LazyLoadFetchChildren()
/// </remarks>
protected virtual IEnumerable<perTreeViewItemViewModelBase> LazyLoadChildren => _childrenList;
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
// if unselecting we don't care about anything else other than simply updating the property
if (!value)
{
Set(nameof(IsSelected), ref _isSelected, false);
return;
}
// Build a priority queue of operations
//
// All operations relating to tree item expansion are added with priority = DispatcherPriority.ContextIdle, so that they are
// sorted before any operations relating to selection (which have priority = DispatcherPriority.ApplicationIdle).
// This ensures that the visual container for all items are created before any selection operation is carried out.
//
// First expand all ancestors of the selected item - those closest to the root first
//
// Expanding a node will scroll as many of its children as possible into view - see perTreeViewItemHelper, but these scrolling
// operations will be added to the queue after all of the parent expansions.
var ancestorsToExpand = new Stack<perTreeViewItemViewModelBase>();
var parent = Parent;
while (parent != null)
{
if (!parent.IsExpanded)
{
ancestorsToExpand.Push(parent);
}
parent = parent.Parent;
}
while (ancestorsToExpand.Any())
{
var parentToExpand = ancestorsToExpand.Pop();
perDispatcherHelper.AddToQueue(() => parentToExpand.IsExpanded = true, DispatcherPriority.ContextIdle);
}
// Set the item's selected state - use DispatcherPriority.ApplicationIdle so this operation is executed after all
// expansion operations, no matter when they were added to the queue.
//
// Selecting a node will also scroll it into view - see perTreeViewItemHelper
perDispatcherHelper.AddToQueue(() => Set(nameof(IsSelected), ref _isSelected, true), DispatcherPriority.ApplicationIdle);
// note that by rule, a TreeView can only have one selected item, but this is handled automatically by
// the control - we aren't required to manually unselect the previously selected item.
// execute all of the queued operations in descending DispatcherPriority order (expansion before selection)
var unused = perDispatcherHelper.ProcessQueueAsync();
}
}
public override string ToString()
{
return Caption;
}
/// <summary>
/// What's the total number of child nodes beneath this one
/// </summary>
public int ChildCount => Children.Count() + Children.Sum(c => c.ChildCount);
}
IsExpanded and IsSelected are then bound to the TreeViewItem's properties in a global style.
<Style
x:Key="perTreeViewItemContainerStyle"
TargetType="{x:Type TreeViewItem}">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}" />
<!-- Link the properties of perTreeViewItemViewModelBase to the corresponding ones on the TreeViewItem -->
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
// ....
</Style>
<Style TargetType="{x:Type TreeView}">
<Setter Property="ItemContainerStyle" Value="{StaticResource perTreeViewItemContainerStyle}" />
</Style>
You can then make a wrapper class for each model item type that you want to display in a TreeView. for example
public class PersonTreeViewWrapper: perTreeViewItemViewModelBase
{
public PersonTreeViewWrapper(Person model)
{
Model = model;
}
public Person Model {get;}
}
I would not try to make this wrapping hidden inside the control, but instead publish a nested collection of PersonTreeViewWrapper items in the ViewModel to be bound as the ItemsSource of the TreeView.
One reason for this is if you want to have multiple item types displayed in the same TreeView control, you have to be able to specify the HierarchicalDataTemplate to be used for each item type.
<TreeView
Grid.Column="0"
ItemsSource="{Binding RootItemVms}">
<TreeView.Resources>
<HierarchicalDataTemplate
DataType="{x:Type vm.PersonTreeViewWrapper}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<CheckBox
VerticalAlignment="Center"
Focusable="False"
IsChecked="{Binding IsChecked, Mode=TwoWay}" />
<TextBlock
Margin="4,0,8,0"
VerticalAlignment="Center"
Text="{Binding Model.DisplayName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
You can also add a bindable SelectedItem property to a TreeView control using a Behavior class.
public class perTreeViewHelper : Behavior<TreeView>
{
public object BoundSelectedItem
{
get => GetValue(BoundSelectedItemProperty);
set => SetValue(BoundSelectedItemProperty, value);
}
public static readonly DependencyProperty BoundSelectedItemProperty =
DependencyProperty.Register("BoundSelectedItem",
typeof(object),
typeof(perTreeViewHelper),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnBoundSelectedItemChanged));
private static void OnBoundSelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.NewValue is perTreeViewItemViewModelBase item)
{
item.IsSelected = true;
}
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
base.OnDetaching();
}
private void OnTreeViewSelectedItemChanged(object obj, RoutedPropertyChangedEventArgs<object> args)
{
BoundSelectedItem = args.NewValue;
}
}
You can get more details of these classes and their usage on my blog post.
In my opinion, you have chosen the wrong direction to find a solution to your problem.
The TreeView name is a bit misleading.
A tree can only have one vertex and must not have cycles. That is, all the nodes (Item) in the tree are unique.
Check out the typical use of a TreeView in Windows Explorer: multiple entry points and the same file or folder may be on different branches.
Hence, simply setting the SelectedItem is not enough to select the item.
You need to provide the full path to the item.
To solve your problem, I would recommend that you create not your own UserControl, but create an Attached Property (let's say SelectItem) for the TreeView passing the path to the selected item.
You can specify the path in different ways: a list of items at the top, a string with a path, and other variants.
In the Attached Property logic, when the value changes, the path will be parsed and traversed through the TreeView Visual Dynamic Tree with the selection and expansion of the required nodes.
I have a ListView that is bound to an ItemsSource of Album instances. Because there can be many (>2,000) Album instances, I use a (horizontally scrolling) VirtualizingStackPanel as the ItemPanel:
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
As ItemTemplate, I use a custom UserControl, which is bound to the Albums in the ItemsSource:
<DataTemplate x:Name="albumItem" x:DataType="local2:Album">
<local3:AlbumControl x:Name="albumControl" Album="{x:Bind}" />
</DataTemplate>
The DependencyProperty Album on the AlbumControl, carries out some actions when an new binding occurs:
/// <summary>
/// Gets or sets the Album assigned to the control.
/// </summary>
public Album Album
{
get { return (Album)GetValue(AlbumProperty); }
set { SetValue(AlbumProperty, value); }
}
/// <summary>
/// Identifies the Album dependency property.
/// </summary>
public static readonly DependencyProperty AlbumProperty = DependencyProperty.Register(nameof(Album), typeof(Album), typeof(AlbumControl), new PropertyMetadata(null, HandleAlbumChange));
private static async void HandleAlbumChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is AlbumControl control)
{
if (e.NewValue is Album album)
{
control.DoStuff(album);
await control.DoStuffAsync(album);
control.DoMoreStuff(album);
}
}
}
Because the ItemsPanel is virtualized, during scrolling, it is possible that a new Album is bound to the AlbumControl after DoStuff() and before the DoStuffAsync() is finished. This causes problems.
I assume I need:
some kind of locking (DoStuff() contains a lock, but you cannot lock async calls), or
some kind of cancelling processing the old Album and just process the new Album, or
maybe it goes wrong at the awaited call, or
maybe my setup is entirely wrong.
The main question is: How to handle binding events that occur quicker than processing the binding?
========================================
In response to a comment, I'll add some more details of what I observed.
control.DoStuff(album) cleared an ObservableCollection-property of the AlbumControl. The AlbumControl shows this collection in a ListView.
await control.DoStuffAsync(album) was used to set the Source of an Image-control in the AlbumControl. An Album has the information for this source as byte[]. I need to convert this to a RandomAccessStream in order to be able to call await bmp.SetSourceAsync(stream) and then I can use bmp (a BitmapImage) as Source. The byte[] are not backed by a file on disk, so I cannot use a Uri to set the source of the Image-control.
After this awaited call, I repopulated the ObservableCollection (control.DoMoreStuff()). If I did it this way, the collection of the last AlbumControl contained listitems from multiple Albums. Indicating that during the awaited call a new Album was set. When the thread returns after the async of the first Album ends, it repopulates the collection, next the thread for the second Album returns and it also repopulates the collection. Resulting in listitems for both Albums to be in the collection. I "solved" this by moving the Clear() after the async method call, right before the repopulation step. But I was still wondering if I did something dubious.
I couldn't find a way to precompute the BitmapImage, so that I wouldn't need the async call. I kept getting the "Marshalled for a different thread"-errors meaning that I needed to create the RandomAccessStream on the UI thread.
=========================================================
Demo code available at https://antamista.visualstudio.com/_git/TestAlbumControl
I think we can start to change this in two ways:
Let the Album and Song classes inherit the INotifyPropertyChanged interface to respond to data changes in a timely manner.
public class Album : List<Song>,INotifyPropertyChanged
{
// ...
private string _title;
/// <summary>
/// The title of Album
/// </summary>
public string Title
{
get => _title;
set
{
_title = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Song:INotifyPropertyChanged
{
//...
private string _title;
/// <summary>
/// The song title
/// </summary>
public string Title
{
get => _title;
set
{
_title = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Block multiple assignments inside AlbumControl by identifier.
private bool _isLoading = false;
private static async void HandleAlbumChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is AlbumControl control)
{
if (control._isLoading)
return;
if (e.NewValue is Album album)
{
control._isLoading = true;
// handle code
control._isLoading = false;
}
else
{
control.Songs.Clear();
control.albumSleeve.Source = null;
}
}
}
DataTemplate
<DataTemplate x:DataType="local:Song">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Bind Track}" FontSize="24" Margin="8,0,16,0" VerticalAlignment="Center"/>
<StackPanel>
<TextBlock Text="{x:Bind Title,Mode=OneWay}" FontWeight="SemiBold" TextWrapping="Wrap"/>
<TextBlock Text="{x:Bind Album}" FontSize="12" TextWrapping="Wrap"/>
</StackPanel>
</StackPanel>
</DataTemplate>
Thanks.
The issue seems to be that I use the album that is boxed in e.NewValue of the DependencyPropertyChangedEventArgs e:
if (e.NewValue is Album album)
{
control.DoStuff(album);
await control.DoStuffAsync(album);
control.DoMoreStuff(album);
}
If I use control.Album the issues no longer show up. Apparently, when using the DependencyPropertyChangedEventArgs the synchorisation is off. Changing the code to:
control.DoStuff(control.Album);
await control.DoStuffAsync(control.Album);
control.DoMoreStuff(control.Album);
solves the synchronisation issues for the Album/Songs combination.
If you go to the demo code, you see that await control.DoStuffAsync(album) sets an image on the control. Sadly, the synchronisation between album/songs and image is still off. I'll leave it for now, since this is not explained in the original question. Still, it seems that using an async method inside the DependencyProperty eventhandler, combined with VirtualizingStackPanel is not a good idea.
I have a ListBox whose ItemTemplate looks like this:
<DataTemplate DataType="local:Column">
<utils:EditableTextBlock x:Name="editableTextBlock" Text="{Binding Name, Mode=TwoWay}"/>
</DataTemplate>
Column is a simple class which looks like this:
public Column(string name, bool isVisibleInTable)
{
Name = name;
IsVisibleInTable = isVisibleInTable;
}
public string Name { get; set; }
public bool IsVisibleInTable { get; set; }
The EditableTextBlock is a UserControl that turns into a TextBox when double clicked and turns back into a TextBlock when Lost Focus. It also has a Property called IsInEditMode which is by default false. When it is true, TextBox is shown.
The Question:
The ItemsSouce of the ListBox is an ObservableCollection<Column>. I have a button which adds new Columns to the collection. But my problem is that I want IsInEditMode to be turned true for newly added EditableTextBlocks by that Button. But I can only access Column in the ViewModel. How will I access the EditableTextBlock of the specified Column in the ItemsSource collection?
The only solution I can come up with is deriving a class from Column and adding a property for that (eg: name: IsInEditMode) (Or maybe a wrapper class. Here's a similar answer which suggestes using a wrapper class) and Binding to that property in the DataTemplate like so:
<DataTemplate DataType="local:DerivedColumn">
<utils:EditableTextBlock x:Name="editableTextBlock" Text="{Binding Name, Mode=TwoWay}"
IsInEditMode="{Binding IsInEditMode}"/>
</DataTemplate>
But I don't want this. I want some way to do this in XAML without deriving classes and adding unnecessary code. (And also adhering to MVVM rules)
If you have scope to add a new dependency property to the EditableTextBlock user control you could consider adding one that has the name StartupInEditMode, defaulting to false to keep the existing behavior.
The Loaded handler for the UserControl could then determine the status of StartupInEditMode to decide how to initially set the value of IsInEditMode.
//..... Added to EditableTextBlock user control
public bool StartupInEdit
{
get { return (bool)GetValue(StartupInEditProperty); }
set { SetValue(StartupInEditProperty, value); }
}
public static readonly DependencyProperty StartupInEditProperty =
DependencyProperty.Register("StartupInEdit", typeof(bool), typeof(EditableTextBlock ), new PropertyMetadata(false));
private void EditableTextBlock_OnLoaded(object sender, RoutedEventArgs e)
{
IsInEditMode = StartupInEditMode;
}
For controls already in the visual tree the changing value of StartupInEdit does not matter as it is only evaluated once on creation. This means you can populate the collection of the ListBox where each EditableTextBlock is not in edit mode, then swap the StartupInEditmMode mode to True when you start adding new items. Then each new EditableTextBlock control starts in the edit mode.
You could accomplish this switch in behavior by specifying a DataTemplate where the Binding of this new property points to a variable of the view and not the collection items.
<DataTemplate DataType="local:Column">
<utils:EditableTextBlock x:Name="editableTextBlock"
Text="{Binding Name, Mode=TwoWay}"
StartupInEditMode="{Binding ANewViewProperty, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
</DataTemplate>
You need to add a property to the parent Window (or Page or whatever is used as the containter for the view) called ANewViewProperty in this example. This value could be part of your view model if you alter the binding to {Binding DataContext.ANewViewProperty, RelativeSource={RelativeSource AncestorType={x:Type Window}}}.
This new property (ANewViewProperty) does not even need to implement INotifyPropertyChanged as the binding will get the initial value as it is creating the new EditableTextBlock control and if the value changes later it has no impact anyway.
You would set the value of ANewViewProperty to False as you load up the ListBox ItemSource initially. When you press the button to add a new item to the list set the value of ANewViewProperty to True meaning the control that will now be created starting up in edit mode.
Update: The C#-only, View-only alternative
The code-only, view-only alternative (similar to user2946329's answer)is to hook to the ListBox.ItemContainerGenerator.ItemsChanged handler that will trigger when a new item is added. Once triggered and you are now acting on new items (via Boolean DetectingNewItems) which finds the first descendant EditableTextBlock control for the appropriate ListBoxItem visual container for the item newly added. Once you have a reference for the control, alter the IsInEditMode property.
//.... View/Window Class
private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
{
MyListBox.ItemContainerGenerator.ItemsChanged += ItemContainerGenerator_ItemsChanged;
}
private void ItemContainerGenerator_ItemsChanged(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
if ((e.Action == NotifyCollectionChangedAction.Add) && DetectingNewItems)
{
var listboxitem = LB.ItemContainerGenerator.ContainerFromIndex(e.Position.Index + 1) as ListBoxItem;
var editControl = FindFirstDescendantChildOf<EditableTextBlock>(listboxitem);
if (editcontrol != null) editcontrol.IsInEditMode = true;
}
}
public static T FindFirstDescendantChildOf<T>(DependencyObject dpObj) where T : DependencyObject
{
if (dpObj == null) return null;
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(dpObj); i++)
{
var child = VisualTreeHelper.GetChild(dpObj, i);
if (child is T) return (T)child;
var obj = FindFirstChildOf<T>(child);
if (obj != null) return obj;
}
return null;
}
Update #2 (based on comments)
Add a property to the view that refers back to the the ViewModel assuming you keep a reference to the View Model in the DataContext:-
..... // Add this to the Window/Page
public bool DetectingNewItems
{
get
{
var vm = DataContext as MyViewModel;
if (vm != null)
return vm.MyPropertyOnVM;
return false;
}
}
.....
To get an element inside a template and change it's properties in code you need FrameworkTemplate.FindName Method (String, FrameworkElement) :
private childItem FindVisualChild<childItem>(DependencyObject obj)
where childItem : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is childItem)
return (childItem)child;
else
{
childItem childOfChild = FindVisualChild<childItem>(child);
if (childOfChild != null)
return childOfChild;
}
}
return null;
}
Then:
for (int i = 0; i < yourListBox.Items.Count; i++)
{
ListBoxItem yourListBoxItem = (ListBoxItem)(yourListBox.ItemContainerGenerator.ContainerFromIndex(i));
ContentPresenter contentPresenter = FindVisualChild<ContentPresenter>(yourListBoxItem);
DataTemplate myDataTemplate = contentPresenter.ContentTemplate;
EditableTextBlock editable = (EditableTextBlock) myDataTemplate.FindName("editableTextBlock", contentPresenter);
//Do stuff with EditableTextBlock
editable.IsInEditMode = true;
}
I have a windows say
PhotoAlbums (Parent window)
PhotoAlbumProperty (Child)
On Photo Albums window I have combo box of list of photo albums, NewAlbum button, Save button, property button.
I want to enable save button only when my PhotoAlbum is in edit mode.
PhotoAlbum will go in edit mode when I add new photos in a album OR if I change properties by clicking property button.
I have properties,
IsPhotoAlbumUpdated in PhotoAlbumVM
IsPhotoAlbumPropertyUpdated in PhotoAlbumPropertyVM
IsSaveEnabled
{
get return this.IsPhotoAlbumUpdated || this.SelectedAlbum.IsPhotoAlbumPropertyUpdated;
}
in PhotoAlbumVM
<Button Name="BtnSave" Command="{Binding Save}"
ToolTip="{x:Static resx:Resource.ToolTipSave}" Focusable="True"
IsEnabled="{Binding IsSaveEnabled}">
Now when this.SelectedAlbum.IsPhotoAlbumPropertyUpdated gets changed then how will my parent view model i.e. PhotoAlbumVM know this?
I was thinking to use prism events, but for doing such smaller thing I don't want to use prism events.
Please suggest me alternate logic.
You need to listen for the child item's OnPropertyChanged event. Each time SelectedAlbum is changed, in set remove the handler from the old album unless it's null using album.PropertyChanged -= MyPropertyChanged and assign the handler to the new value using value.PropertyChanged += MyPropertyChanged. In MyPropertyChanged force update new value of IsSaveEnabled.
You can subscribe to PropertyChanged events to collection items on CollectionChanged. Providing your UI is bound correctly to the items in the ObservableCollection, you shouldn't need to tell the UI to update when a property on an item in the collection changes. You can do the collection of specific object or use the below implementation to do it across application if you have to do make consistent behavior.
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Collections;
namespace VJCollections
{
/// <summary>
/// This class adds the ability to refresh the list when any property of
/// the objects changes in the list which implements the INotifyPropertyChanged.
/// </summary>
/// <typeparam name="T">
public class ItemsChangeObservableCollection<T> :
ObservableCollection<T> where T : INotifyPropertyChanged
{
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
RegisterPropertyChanged(e.NewItems);
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
UnRegisterPropertyChanged(e.OldItems);
}
else if (e.Action == NotifyCollectionChangedAction.Replace)
{
UnRegisterPropertyChanged(e.OldItems);
RegisterPropertyChanged(e.NewItems);
}
base.OnCollectionChanged(e);
}
protected override void ClearItems()
{
UnRegisterPropertyChanged(this);
base.ClearItems();
}
private void RegisterPropertyChanged(IList items)
{
foreach (INotifyPropertyChanged item in items)
{
if (item != null)
{
item.PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
}
}
private void UnRegisterPropertyChanged(IList items)
{
foreach (INotifyPropertyChanged item in items)
{
if (item != null)
{
item.PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
}
}
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
}
I might be suffering of Monday's dumbness, but I can't find a nice way of expanding all treeview nodes after I've added them in code behind (something like treeView.ExpandAll()).
Any quick help?
In xaml you could do it as follows :
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="TreeViewItem.IsExpanded" Value="True"/>
</Style>
</TreeView.ItemContainerStyle>
After playing around with all of the various methods for fully expanding and collapsing a tree view, by far the fastest method is the following. This method seems to work on very large trees.
Ensure your tree is virtualized, if it isn't virtualized then as soon as the tree gets to any kind of size it is going to become painfully slow whatever you do.
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
Assume that you have a view model backing your tree, each node on that view model that corresponds to a HierarchicalDataTemplate needs an IsExpanded property (it doesn't need to implement property changed). Assume these view models implement an interface like this:
interface IExpandableItem : IEnumerable
{
bool IsExpanded { get; set; }
}
The TreeViewItem style needs to be set as follows to bind the IsExpanded property in the view model to the view:
<Style
TargetType="{x:Type TreeViewItem}">
<Setter
Property="IsExpanded"
Value="{Binding
IsExpanded,
Mode=TwoWay}" />
</Style>
We are going to use this property to set the expansion state, but also, because the tree is virtualized this property is necessary to maintain the correct view state as the individual TreeViewItems get recycled. Without this binding nodes will get collapsed as they go out of view as the user browses the tree.
The only way to get acceptable speed on large trees is to work in code behind in the view layer. The plan is basically as follows:
Get hold of the current binding to the TreeView.ItemsSource.
Clear that binding.
Wait for the binding to actually clear.
Set the expansion state in the (now unbound) view model.
Rebind the TreeView.ItemsSource using the binding we cached in step 1.
Because we have virtualization enabled, performing a bind on TreeView.ItemsSource turns out to be very fast, even with a large view model. Likewise, when unbound updating the expansion state of the nodes should be very fast. This results in surprisingly fast updates.
Here is some code:
void SetExpandedStateInView(bool isExpanded)
{
var model = this.DataContext as TreeViewModel;
if (model == null)
{
// View model is not bound so do nothing.
return;
}
// Grab hold of the current ItemsSource binding.
var bindingExpression = this.TreeView.GetBindingExpression(
ItemsControl.ItemsSourceProperty);
if (bindingExpression == null)
{
return;
}
// Clear that binding.
var itemsSourceBinding = bindingExpression.ParentBinding;
BindingOperations.ClearBinding(
this.TreeView, ItemsControl.ItemsSourceProperty);
// Wait for the binding to clear and then set the expanded state of the view model.
this.Dispatcher.BeginInvoke(
DispatcherPriority.DataBind,
new Action(() => SetExpandedStateInModel(model.Items, isExpanded)));
// Now rebind the ItemsSource.
this.Dispatcher.BeginInvoke(
DispatcherPriority.DataBind,
new Action(
() => this.TreeView.SetBinding(
ItemsControl.ItemsSourceProperty, itemsSourceBinding)));
}
void SetExpandedStateInModel(IEnumerable modelItems, bool isExpanded)
{
if (modelItems == null)
{
return;
}
foreach (var modelItem in modelItems)
{
var expandable = modelItem as IExpandableItem;
if (expandable == null)
{
continue;
}
expandable.IsExpanded = isExpanded;
SetExpandedStateInModel(expandable, isExpanded);
}
}
WPF doesn't have an ExpandAll method. You'll need to loop through and set the property on each node.
See this question or this blog post.
I have done an ExpandAll that works also if your tree is set for virtualization (recycling items).
This is my code. Perhaps you should consider wrapping your hierarchy into a hierarchical model model view ?
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;
using HQ.Util.General;
namespace HQ.Util.Wpf.WpfUtil
{
public static class TreeViewExtensions
{
// ******************************************************************
public delegate void OnTreeViewVisible(TreeViewItem tvi);
public delegate void OnItemExpanded(TreeViewItem tvi, object item);
public delegate void OnAllItemExpanded();
// ******************************************************************
private static void SetItemHierarchyVisible(ItemContainerGenerator icg, IList listOfRootToNodeItemPath, OnTreeViewVisible onTreeViewVisible = null)
{
Debug.Assert(icg != null);
if (icg != null)
{
if (listOfRootToNodeItemPath.Count == 0) // nothing to do
return;
TreeViewItem tvi = icg.ContainerFromItem(listOfRootToNodeItemPath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, always better to verify
{
listOfRootToNodeItemPath.RemoveAt(0);
if (listOfRootToNodeItemPath.Count == 0)
{
if (onTreeViewVisible != null)
onTreeViewVisible(tvi);
}
else
{
if (!tvi.IsExpanded)
tvi.IsExpanded = true;
SetItemHierarchyVisible(tvi.ItemContainerGenerator, listOfRootToNodeItemPath, onTreeViewVisible);
}
}
else
{
ActionHolder actionHolder = new ActionHolder();
EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
{
var icgSender = sender as ItemContainerGenerator;
tvi = icgSender.ContainerFromItem(listOfRootToNodeItemPath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, it is always better to verify
{
SetItemHierarchyVisible(icg, listOfRootToNodeItemPath, onTreeViewVisible);
actionHolder.Execute();
}
};
actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
icg.StatusChanged += itemCreated;
return;
}
}
}
// ******************************************************************
/// <summary>
/// You cannot rely on this method to be synchronous. If you have any action that depend on the TreeViewItem
/// (last item of collectionOfRootToNodePath) to be visible, you should set it in the 'onTreeViewItemVisible' method.
/// This method should work for Virtualized and non virtualized tree.
/// The difference with ExpandItem is that this one open up the tree up to the target but will not expand the target itself,
/// while ExpandItem expand the target itself.
/// </summary>
/// <param name="treeView">TreeView where an item has to be set visible</param>
/// <param name="listOfRootToNodePath">Any collectionic List. The collection should have every objet of the path to the targeted item from the root
/// to the target. For example for an apple tree: AppleTree (index 0), Branch4, SubBranch3, Leaf2 (index 3)</param>
/// <param name="onTreeViewVisible">Optionnal</param>
public static void SetItemHierarchyVisible(this TreeView treeView, IEnumerable<object> listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
{
ItemContainerGenerator icg = treeView.ItemContainerGenerator;
if (icg == null)
return; // Is tree loaded and initialized ???
SetItemHierarchyVisible(icg, new List<object>(listOfRootToNodePath), onTreeViewVisible);
}
// ******************************************************************
private static void ExpandItem(ItemContainerGenerator icg, IList listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
{
Debug.Assert(icg != null);
if (icg != null)
{
if (listOfRootToNodePath.Count == 0) // nothing to do
return;
TreeViewItem tvi = icg.ContainerFromItem(listOfRootToNodePath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, always better to verify
{
listOfRootToNodePath.RemoveAt(0);
if (!tvi.IsExpanded)
tvi.IsExpanded = true;
if (listOfRootToNodePath.Count == 0)
{
if (onTreeViewVisible != null)
onTreeViewVisible(tvi);
}
else
{
SetItemHierarchyVisible(tvi.ItemContainerGenerator, listOfRootToNodePath, onTreeViewVisible);
}
}
else
{
ActionHolder actionHolder = new ActionHolder();
EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
{
var icgSender = sender as ItemContainerGenerator;
tvi = icgSender.ContainerFromItem(listOfRootToNodePath[0]) as TreeViewItem;
if (tvi != null) // Due to threading, it is always better to verify
{
SetItemHierarchyVisible(icg, listOfRootToNodePath, onTreeViewVisible);
actionHolder.Execute();
}
};
actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
icg.StatusChanged += itemCreated;
return;
}
}
}
// ******************************************************************
/// <summary>
/// You cannot rely on this method to be synchronous. If you have any action that depend on the TreeViewItem
/// (last item of collectionOfRootToNodePath) to be visible, you should set it in the 'onTreeViewItemVisible' method.
/// This method should work for Virtualized and non virtualized tree.
/// The difference with SetItemHierarchyVisible is that this one open the target while SetItemHierarchyVisible does not try to expand the target.
/// (SetItemHierarchyVisible just ensure the target will be visible)
/// </summary>
/// <param name="treeView">TreeView where an item has to be set visible</param>
/// <param name="listOfRootToNodePath">The collection should have every objet of the path, from the root to the targeted item.
/// For example for an apple tree: AppleTree (index 0), Branch4, SubBranch3, Leaf2</param>
/// <param name="onTreeViewVisible">Optionnal</param>
public static void ExpandItem(this TreeView treeView, IEnumerable<object> listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
{
ItemContainerGenerator icg = treeView.ItemContainerGenerator;
if (icg == null)
return; // Is tree loaded and initialized ???
ExpandItem(icg, new List<object>(listOfRootToNodePath), onTreeViewVisible);
}
// ******************************************************************
private static void ExpandSubWithContainersGenerated(ItemsControl ic, Action<TreeViewItem, object> actionItemExpanded, ReferenceCounterTracker referenceCounterTracker)
{
ItemContainerGenerator icg = ic.ItemContainerGenerator;
foreach (object item in ic.Items)
{
var tvi = icg.ContainerFromItem(item) as TreeViewItem;
actionItemExpanded(tvi, item);
tvi.IsExpanded = true;
ExpandSubContainers(tvi, actionItemExpanded, referenceCounterTracker);
}
}
// ******************************************************************
/// <summary>
/// Expand any ItemsControl (TreeView, TreeViewItem, ListBox, ComboBox, ...) and their childs if any (TreeView)
/// </summary>
/// <param name="ic"></param>
/// <param name="actionItemExpanded"></param>
/// <param name="referenceCounterTracker"></param>
public static void ExpandSubContainers(ItemsControl ic, Action<TreeViewItem, object> actionItemExpanded, ReferenceCounterTracker referenceCounterTracker)
{
ItemContainerGenerator icg = ic.ItemContainerGenerator;
{
if (icg.Status == GeneratorStatus.ContainersGenerated)
{
ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
}
else if (icg.Status == GeneratorStatus.NotStarted)
{
ActionHolder actionHolder = new ActionHolder();
EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
{
var icgSender = sender as ItemContainerGenerator;
if (icgSender.Status == GeneratorStatus.ContainersGenerated)
{
ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
// Never use the following method in BeginInvoke due to ICG recycling. The same icg could be
// used and will keep more than one subscribers which is far from being intended
// ic.Dispatcher.BeginInvoke(actionHolder.Action, DispatcherPriority.Background);
// Very important to unsubscribe as soon we've done due to ICG recycling.
actionHolder.Execute();
referenceCounterTracker.ReleaseRef();
}
};
referenceCounterTracker.AddRef();
actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
icg.StatusChanged += itemCreated;
// Next block is only intended to protect against any race condition (I don't know if it is possible ? How Microsoft implemented it)
// I mean the status changed before I subscribe to StatusChanged but after I made the check about its state.
if (icg.Status == GeneratorStatus.ContainersGenerated)
{
ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
}
}
}
}
// ******************************************************************
/// <summary>
/// This method is asynchronous.
/// Expand all items and subs recursively if any. Does support virtualization (item recycling).
/// But honestly, make you a favor, make your life easier en create a model view around your hierarchy with
/// a IsExpanded property for each node level and bind it to each TreeView node level.
/// </summary>
/// <param name="treeView"></param>
/// <param name="actionItemExpanded"></param>
/// <param name="actionAllItemExpanded"></param>
public static void ExpandAll(this TreeView treeView, Action<TreeViewItem, object> actionItemExpanded = null, Action actionAllItemExpanded = null)
{
var referenceCounterTracker = new ReferenceCounterTracker(actionAllItemExpanded);
referenceCounterTracker.AddRef();
treeView.Dispatcher.BeginInvoke(new Action(() => ExpandSubContainers(treeView, actionItemExpanded, referenceCounterTracker)), DispatcherPriority.Background);
referenceCounterTracker.ReleaseRef();
}
// ******************************************************************
}
}
And
using System;
using System.Threading;
namespace HQ.Util.General
{
public class ReferenceCounterTracker
{
private Action _actionOnCountReachZero = null;
private int _count = 0;
public ReferenceCounterTracker(Action actionOnCountReachZero)
{
_actionOnCountReachZero = actionOnCountReachZero;
}
public void AddRef()
{
Interlocked.Increment(ref _count);
}
public void ReleaseRef()
{
int count = Interlocked.Decrement(ref _count);
if (count == 0)
{
if (_actionOnCountReachZero != null)
{
_actionOnCountReachZero();
}
}
}
}
}
You have to include the following method in your project:
private void ExpandAllNodes(TreeViewItem treeItem)
{
treeItem.IsExpanded = true;
foreach (var childItem in treeItem.Items.OfType<TreeViewItem>())
{
ExpandAllNodes(childItem);
}
}
then, you only need to call it like this:
treeView.Items.OfType<TreeViewItem>().ToList().ForEach(ExpandAllNodes);