Disconnecting an element from any/unspecified parent container in WPF - c#

I have a control that is a child of another control (as all non-root controls/elemts are in WPF).
If I want to move the control to another container I have to disconnect it from its current container first (otherwise an exception is thrown).
If I know what the parent is then I can just remove it from its Children collection, or Content or whatever. But what if I don't know what the parent container's type is - how do I remove the child control then?
In the code sample below: How would I be able to move "sp1" to another container without knowing the type of the parent (Panel, GroupBox...)?
// Add the child object "sp1" to a container (of any type).
StackPanel sp1 = new StackPanel();
SomeParentControl.Children.Add(sp1);
// Somewhere else in the code. I still have a reference to "sp1" but now I don't know what container it is in. I just want to move the "sp1" object to another parent container.
AnotherParentControl.Content = sp1; // Generates exception: "Must disconnect specified child from current parent Visual before attaching to new parent Visual."
Ideally I would just like to write something like:
sp1.Parent.RemoveChild(sp1);
But I haven't found anything like that.

You may write a helper class with an extension method:
public static class RemoveChildHelper
{
public static void RemoveChild(this DependencyObject parent, UIElement child)
{
var panel = parent as Panel;
if (panel != null)
{
panel.Children.Remove(child);
return;
}
var decorator = parent as Decorator;
if (decorator != null)
{
if (decorator.Child == child)
{
decorator.Child = null;
}
return;
}
var contentPresenter = parent as ContentPresenter;
if (contentPresenter != null)
{
if (contentPresenter.Content == child)
{
contentPresenter.Content = null;
}
return;
}
var contentControl = parent as ContentControl;
if (contentControl != null)
{
if (contentControl.Content == child)
{
contentControl.Content = null;
}
return;
}
// maybe more
}
}

NEW:
I propose to use base classes instead of all other listed. Try this code, this 3 classes are the most use cases for your needs. As I understand, it's almost the same as previos ^)
var parent = VisualTreeHelper.GetParent(child);
var parentAsPanel = parent as Panel;
if (parentAsPanel != null)
{
parentAsPanel.Children.Remove(child);
}
var parentAsContentControl = parent as ContentControl;
if (parentAsContentControl != null)
{
parentAsContentControl.Content = null;
}
var parentAsDecorator = parent as Decorator;
if (parentAsDecorator != null)
{
parentAsDecorator.Child = null;
}
OLD:
As far as I remember, you can use Visual type as parent type and try to call RemoveVisualChild method.

For completeness, I've added in the ItemsControl check, and an Add method that will put the child back. The child or parent may not yet be in the visual tree, so you have to check both the visual and logical trees:
/// <summary>
/// Adds or inserts a child back into its parent
/// </summary>
/// <param name="child"></param>
/// <param name="index"></param>
public static void AddToParent(this UIElement child, DependencyObject parent, int? index = null)
{
if (parent == null)
return;
if (parent is ItemsControl itemsControl)
if (index == null)
itemsControl.Items.Add(child);
else
itemsControl.Items.Insert(index.Value, child);
else if (parent is Panel panel)
if (index == null)
panel.Children.Add(child);
else
panel.Children.Insert(index.Value, child);
else if (parent is Decorator decorator)
decorator.Child = child;
else if (parent is ContentPresenter contentPresenter)
contentPresenter.Content = child;
else if (parent is ContentControl contentControl)
contentControl.Content = child;
}
/// <summary>
/// Removes the child from its parent collection or its content.
/// </summary>
/// <param name="child"></param>
/// <param name="parent"></param>
/// <returns></returns>
public static bool RemoveFromParent(this UIElement child, out DependencyObject parent, out int? index)
{
parent = child.GetParent(true);
if (parent == null)
parent = child.GetParent(false);
index = null;
if (parent == null)
return false;
if (parent is ItemsControl itemsControl)
{
if (itemsControl.Items.Contains(child))
{
index = itemsControl.Items.IndexOf(child);
itemsControl.Items.Remove(child);
return true;
}
}
else if (parent is Panel panel)
{
if (panel.Children.Contains(child))
{
index = panel.Children.IndexOf(child);
panel.Children.Remove(child);
return true;
}
}
else if (parent is Decorator decorator)
{
if (decorator.Child == child)
{
decorator.Child = null;
return true;
}
}
else if (parent is ContentPresenter contentPresenter)
{
if (contentPresenter.Content == child)
{
contentPresenter.Content = null;
return true;
}
}
else if (parent is ContentControl contentControl)
{
if (contentControl.Content == child)
{
contentControl.Content = null;
return true;
}
}
return false;
}
public static DependencyObject GetParent(this DependencyObject depObj, bool isVisualTree)
{
if (isVisualTree)
{
if(depObj is Visual || depObj is Visual3D)
return VisualTreeHelper.GetParent(depObj);
return null;
}
else
return LogicalTreeHelper.GetParent(depObj);
}

My version for #Clemens solution:
/// <summary>
/// Disconnects <paramref name="child"/> from it's parent if any.
/// </summary>
/// <param name="child"></param>
public static void DisconnectIt(this FrameworkElement child)
{
var parent = child.Parent;
if (parent == null)
return;
if (parent is Panel panel)
{
panel.Children.Remove(child);
return;
}
if (parent is Decorator decorator)
{
if (decorator.Child == child)
decorator.Child = null;
return;
}
if (parent is ContentPresenter contentPresenter)
{
if (contentPresenter.Content == child)
contentPresenter.Content = null;
return;
}
if (parent is ContentControl contentControl)
{
if (contentControl.Content == child)
contentControl.Content = null;
return;
}
//if (parent is ItemsControl itemsControl)
//{
// itemsControl.Items.Remove(child);
// return;
//}
}

Related

In a WPF, how do you scroll to the bottom of a FlowDocumentScrollViewer?

A normal ScrollViewer appears to have a method that accomplishes this. A FlowDocumentScrollViewer does not. So how do I go around setting a FlowDocumentScrollViewer to the bottom.
Or you can use the provided answer here, and search for the ScrollViewer:
public static void ScrollToEnd(FlowDocumentScrollViewer fdsv)
{
ScrollViewer sc = FindVisualChildren<ScrollViewer>(fdsv).First() as ScrollViewer;
if (sc != null)
sc.ScrollToEnd();
}
public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
You can access scrollviewer from flowdocumentscrollviewer. Here is example
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var scrollViewer = FindScrollViewer(flowDocumentScrollViewer);
scrollViewer.ScrollToBottom();
}
public static ScrollViewer FindScrollViewer(FlowDocumentScrollViewer flowDocumentScrollViewer)
{
if (VisualTreeHelper.GetChildrenCount(flowDocumentScrollViewer) == 0)
{
return null;
}
// Border is the first child of first child of a ScrolldocumentViewer
DependencyObject firstChild = VisualTreeHelper.GetChild(flowDocumentScrollViewer, 0);
if (firstChild == null)
{
return null;
}
Decorator border = VisualTreeHelper.GetChild(firstChild, 0) as Decorator;
if (border == null)
{
return null;
}
return border.Child as ScrollViewer;
}

UserControl owner of a DialogBox, is it possible? [duplicate]

I have a user control that I load into a MainWindow at runtime. I cannot get a handle on the containing window from the UserControl.
I have tried this.Parent, but it's always null. Does anyone know how to get a handle to the containing window from a user control in WPF?
Here is how the control is loaded:
private void XMLLogViewer_MenuItem_Click(object sender, RoutedEventArgs e)
{
MenuItem application = sender as MenuItem;
string parameter = application.CommandParameter as string;
string controlName = parameter;
if (uxPanel.Children.Count == 0)
{
System.Runtime.Remoting.ObjectHandle instance = Activator.CreateInstance(Assembly.GetExecutingAssembly().FullName, controlName);
UserControl control = instance.Unwrap() as UserControl;
this.LoadControl(control);
}
}
private void LoadControl(UserControl control)
{
if (uxPanel.Children.Count > 0)
{
foreach (UIElement ctrl in uxPanel.Children)
{
if (ctrl.GetType() != control.GetType())
{
this.SetControl(control);
}
}
}
else
{
this.SetControl(control);
}
}
private void SetControl(UserControl control)
{
control.Width = uxPanel.Width;
control.Height = uxPanel.Height;
uxPanel.Children.Add(control);
}
Try using the following:
Window parentWindow = Window.GetWindow(userControlReference);
The GetWindow method will walk the VisualTree for you and locate the window that is hosting your control.
You should run this code after the control has loaded (and not in the Window constructor) to prevent the GetWindow method from returning null. E.g. wire up an event:
this.Loaded += new RoutedEventHandler(UserControl_Loaded);
I'll add my experience. Although using the Loaded event can do the job, I think it may be more suitable to override the OnInitialized method. Loaded occurs after the window is first displayed. OnInitialized gives you chance to make any changes, for example, add controls to the window before it is rendered.
Use VisualTreeHelper.GetParent or the recursive function below to find the parent window.
public static Window FindParentWindow(DependencyObject child)
{
DependencyObject parent= VisualTreeHelper.GetParent(child);
//CHeck if this is the end of the tree
if (parent == null) return null;
Window parentWindow = parent as Window;
if (parentWindow != null)
{
return parentWindow;
}
else
{
//use recursion until it reaches a Window
return FindParentWindow(parent);
}
}
I needed to use the Window.GetWindow(this) method within Loaded event handler. In other words, I used both Ian Oakes' answer in combination with Alex's answer to get a user control's parent.
public MainView()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(MainView_Loaded);
}
void MainView_Loaded(object sender, RoutedEventArgs e)
{
Window parentWindow = Window.GetWindow(this);
...
}
If you are finding this question and the VisualTreeHelper isn't working for you or working sporadically, you may need to include LogicalTreeHelper in your algorithm.
Here is what I am using:
public static T TryFindParent<T>(DependencyObject current) where T : class
{
DependencyObject parent = VisualTreeHelper.GetParent(current);
if( parent == null )
parent = LogicalTreeHelper.GetParent(current);
if( parent == null )
return null;
if( parent is T )
return parent as T;
else
return TryFindParent<T>(parent);
}
This approach worked for me but it is not as specific as your question:
App.Current.MainWindow
How about this:
DependencyObject parent = ExVisualTreeHelper.FindVisualParent<UserControl>(this);
public static class ExVisualTreeHelper
{
/// <summary>
/// Finds the visual parent.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sender">The sender.</param>
/// <returns></returns>
public static T FindVisualParent<T>(DependencyObject sender) where T : DependencyObject
{
if (sender == null)
{
return (null);
}
else if (VisualTreeHelper.GetParent(sender) is T)
{
return (VisualTreeHelper.GetParent(sender) as T);
}
else
{
DependencyObject parent = VisualTreeHelper.GetParent(sender);
return (FindVisualParent<T>(parent));
}
}
}
I've found that the parent of a UserControl is always null in the constructor, but in any event handlers the parent is set correctly. I guess it must have something to do with the way the control tree is loaded. So to get around this you can just get the parent in the controls Loaded event.
For an example checkout this question WPF User Control's DataContext is Null
Another way:
var main = App.Current.MainWindow as MainWindow;
It's working for me:
DependencyObject GetTopLevelControl(DependencyObject control)
{
DependencyObject tmp = control;
DependencyObject parent = null;
while((tmp = VisualTreeHelper.GetParent(tmp)) != null)
{
parent = tmp;
}
return parent;
}
This didn't work for me, as it went too far up the tree, and got the absolute root window for the entire application:
Window parentWindow = Window.GetWindow(userControlReference);
However, this worked to get the immediate window:
DependencyObject parent = uiElement;
int avoidInfiniteLoop = 0;
while ((parent is Window)==false)
{
parent = VisualTreeHelper.GetParent(parent);
avoidInfiniteLoop++;
if (avoidInfiniteLoop == 1000)
{
// Something is wrong - we could not find the parent window.
break;
}
}
Window window = parent as Window;
window.DragMove();
If you just want to get a specific parent, not only the window, a specific parent in the tree structure, and also not using recursion, or hard break loop counters, you can use the following:
public static T FindParent<T>(DependencyObject current)
where T : class
{
var dependency = current;
while((dependency = VisualTreeHelper.GetParent(dependency) ?? LogicalTreeHelper.GetParent(dependency)) != null
&& !(dependency is T)) { }
return dependency as T;
}
Just don't put this call in a constructor (since the Parent property is not yet initialized). Add it in the loading event handler, or in other parts of your application.
DependencyObject parent = ExVisualTreeHelper.FindVisualParent<UserControl>(this);
DependencyObject GetTopParent(DependencyObject current)
{
while (VisualTreeHelper.GetParent(current) != null)
{
current = VisualTreeHelper.GetParent(current);
}
return current;
}
DependencyObject parent = GetTopParent(thisUserControl);
The Window.GetWindow(userControl) will return the actual window only after the window was initialized (InitializeComponent() method finished).
This means, that if your user control is initialized together with its window (for instance you put your user control into the window's xaml file), then on the user control's OnInitialized event you will not get the window (it will be null), cause in that case the user control's OnInitialized event fires before the window is initialized.
This also means that if your user control is initialized after its window, then you can get the window already in the user control's constructor.
Gold plated edition of the above (I need a generic function which can infer a Window within the context of a MarkupExtension:-
public sealed class MyExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider) =>
new MyWrapper(ResolveRootObject(serviceProvider));
object ResolveRootObject(IServiceProvider serviceProvider) =>
GetService<IRootObjectProvider>(serviceProvider).RootObject;
}
class MyWrapper
{
object _rootObject;
Window OwnerWindow() => WindowFromRootObject(_rootObject);
static Window WindowFromRootObject(object root) =>
(root as Window) ?? VisualParent<Window>((DependencyObject)root);
static T VisualParent<T>(DependencyObject node) where T : class
{
if (node == null)
throw new InvalidOperationException("Could not locate a parent " + typeof(T).Name);
var target = node as T;
if (target != null)
return target;
return VisualParent<T>(VisualTreeHelper.GetParent(node));
}
}
MyWrapper.Owner() will correctly infer a Window on the following basis:
the root Window by walking the visual tree (if used in the context of a UserControl)
the window within which it is used (if it is used in the context of a Window's markup)
Different approaches and different strategies. In my case I could not find the window of my dialog either through using VisualTreeHelper or extension methods from Telerik to find parent of given type. Instead, I found my my dialog view which accepts custom injection of contents using Application.Current.Windows.
public Window GetCurrentWindowOfType<TWindowType>(){
return Application.Current.Windows.OfType<TWindowType>().FirstOrDefault() as Window;
}

Setting ItemsPanel's properties from itemsControl

I am using custom panel from my custom Items control( DisplayPanelControl which is derived from List box) the style is some thing similar to following XAML
<Style x:Key="ContainerStyle" TargetType="{x:Type local:DisplayPanelControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<local:CustomePanel Background="AliceBlue" IsItemsHost="True">
</local:CustomePanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
the CustomePanel has a property Edgblending. I want to set this property through my Items control, so I have overridden OnApplyTemplate() method and used VisualTreeHelper to find my customPanel as set the desired property.
I want to ask if there is a better solution for setting properties on ItemsPanel through Itemscontrol?
This is one of the possible work around which will work in under 2 situation, Have used the method on Items controls OnApplyTemplate() method.
When we specify Panel in itemsControls template by setting IsItemsHost property on the Panel
When we set the Items panel via "ItemsPanelTemplate" markup.
Have come to this workaround by the explanation given by Ian Griffiths Find Control Inside ListBox? answer.
private T GetItemsPanel<T>(ItemsControl itemsControl) where T : Panel
{
T _Panel = UIHelper.FindVisualChild<T>(itemsControl);
if (_Panel == null)
{
ItemsPresenter itemsPresenter = UIHelper.FindVisualChild<ItemsPresenter>(itemsControl);
if (itemsPresenter != null)
{
itemsPresenter.ApplyTemplate();
_Panel = VisualTreeHelper.GetChild(itemsPresenter, 0) as T;
}
}
return _Panel;
}
The implementation for UiHelper class is nothing but finding the object in visual tree and implementation is as below(I have copied this as well from some blog post but cant remember to find the link)
public static class UIHelper
{
/// <summary>
/// Finds a parent of a given item on the visual tree.
/// </summary>
/// <typeparam name="T">The type of the queried item.</typeparam>
/// <param name="child">A direct or indirect child of the queried item.</param>
/// <returns>The first parent item that matches the submitted type parameter.
/// If not matching item can be found, a null reference is being returned.</returns>
public static T FindVisualParent<T>(DependencyObject child)
where T : DependencyObject
{
// get parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
// we’ve reached the end of the tree
if (parentObject == null) return null;
// check if the parent matches the type we’re looking for
T parent = parentObject as T;
if (parent != null)
{
return parent;
}
else
{
// use recursion to proceed with next level
return FindVisualParent<T>(parentObject);
}
}
public static T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = FindVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
}

Why VisualTreeHelper.GetChildrenCount() returns 0 for Popup?

I move focus to the Popup on its opening:
wcl:FocusHelper.IsFocused="{Binding RelativeSource={RelativeSource Self}, Path=IsOpen}"
FocusHelper class code:
public static class FocusHelper
{
public static readonly DependencyProperty IsFocusedProperty =
DependencyProperty.RegisterAttached("IsFocused", typeof(bool?), typeof(FocusHelper), new FrameworkPropertyMetadata(IsFocusedChanged));
public static bool? GetIsFocused(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (bool?)element.GetValue(IsFocusedProperty);
}
public static void SetIsFocused(DependencyObject element, bool? value)
{
if (element == null)
throw new ArgumentNullException("element");
element.SetValue(IsFocusedProperty, value);
}
private static void IsFocusedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var fe = (FrameworkElement)d;
if (e.OldValue == null)
{
fe.GotFocus += ElementGotFocus;
fe.LostFocus += ElementLostFocus;
fe.IsVisibleChanged += ElementIsVisibleChanged;
}
if (e.NewValue == null)
{
fe.GotFocus -= ElementGotFocus;
fe.LostFocus -= ElementLostFocus;
fe.IsVisibleChanged -= ElementIsVisibleChanged;
return;
}
if ((bool)e.NewValue)
{
fe.SetFocusWithin();
}
}
private static void ElementIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var fe = (FrameworkElement)sender;
if (fe.IsVisible
&& (bool)(((FrameworkElement) sender).GetValue(IsFocusedProperty))) // Bring focus to just become visible element.
fe.Focus();
}
private static void ElementGotFocus(object sender, RoutedEventArgs e)
{
((FrameworkElement)sender).SetCurrentValue(IsFocusedProperty, true);
}
private static void ElementLostFocus(object sender, RoutedEventArgs e)
{
((FrameworkElement)sender).SetCurrentValue(IsFocusedProperty, false);
}
/// <summary>
/// Tries to set focus to the element or any other element inside this one.
/// Tab index is respected
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static bool SetFocusWithin(this DependencyObject element)
{
if (element == null)
throw new ArgumentNullException("element");
var inputElement = element as IInputElement;
if (inputElement == null || !inputElement.Focus())
{
var children = element.GetVisualChildrenSortedByTabIndex().Where(child => !(child is Control) || (bool)child.GetValue(Control.IsTabStopProperty) );
return children.Any(SetFocusWithin);
}
return true;
}
}
ElementTreeHelper class part :
public static IEnumerable<DependencyObject> GetVisualChildren(this DependencyObject parent)
{
if (parent == null)
throw new ArgumentNullException("parent");
var count = VisualTreeHelper.GetChildrenCount(parent);
for (var i = 0; i < count; i++)
yield return VisualTreeHelper.GetChild(parent, i);
}
public static IEnumerable<DependencyObject> GetVisualChildrenSortedByTabIndex(this DependencyObject parent)
{
if (parent == null)
throw new ArgumentNullException("parent");
return parent.GetVisualChildren().OrderBy(KeyboardNavigation.GetTabIndex);
}
The problem is that
var count = VisualTreeHelper.GetChildrenCount(parent) == 0 when parent is Popup.
UPDATE
The answer is here
The Popup doesn't host the Child within itself. Instead it creates a PopupRoot (internal class) that is the root visual of a new HWND created to host the popup's content within a separate top level window (or child window in xbap). The Child of the Popup is hosted within an AdornerDecorator within that PopupRoot.
I'm a bit late to the party, but AndrewS's answer helped me to give up on using GetChildrenCount on my popup. I did notice that the Child property of the Popup is correctly populated so i made a separate code path for finding a specific child type of a Popup object. Here's the code i use to find a specific child by type (and optionally by name):
public static TChildItem FindVisualChild<TChildItem>(this DependencyObject dependencyObject, String name) where TChildItem : DependencyObject
{
// Search immediate children first (breadth-first)
var childrenCount = VisualTreeHelper.GetChildrenCount(dependencyObject);
//http://stackoverflow.com/questions/12304904/why-visualtreehelper-getchildrencount-returns-0-for-popup
if (childrenCount == 0 && dependencyObject is Popup)
{
var popup = dependencyObject as Popup;
return popup.Child != null ? popup.Child.FindVisualChild<TChildItem>(name) : null;
}
for (var i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
var nameOfChild = child.GetValue(FrameworkElement.NameProperty) as String;
if (child is TChildItem && (name == String.Empty || name == nameOfChild))
return (TChildItem)child;
var childOfChild = child.FindVisualChild<TChildItem>(name);
if (childOfChild != null)
return childOfChild;
}
return null;
}
public static TChildItem FindVisualChild<TChildItem>(this DependencyObject dependencyObject) where TChildItem : DependencyObject
{
return dependencyObject.FindVisualChild<TChildItem>(String.Empty);
}

Treeview ContainerFromItem always returns null

I've read a few threads on this subject but couldn't find anything to do what I'm trying to do. I have a treeview that is bound to a hierarchical set of objects. Each of these objects represents an icon on a map. When the user clicks one of the icons on the map, I want to select the item in the tree view, focus on it, and scroll it into view. The map object has a list of the objects that are bound to the treeview. In the example, Thing is the type of object bound to the tree.
public void ScrollIntoView(Thing t)
{
if (t != null)
{
t.IsSelected = true;
t.IsExpanded = true;
TreeViewItem container = (TreeViewItem)(masterTreeView
.ItemContainerGenerator.ContainerFromItem(t));
if (container != null)
{
container.Focus();
LogicalTreeHelper.BringIntoView(container);
}
}
}
So far, no matter what I've tried, container is always null. Any ideas?
Is the item actually a child of the masterTreeView?
This might actually be quite difficult since TreeViewItems are ItemsControls with their own ItemContainerGenerator which means you should only be able to get the container from the immediate parent's ItemContainerGenerator and not from the root.
Some recursive function which first goes up the hierarchy to the root and then reverses this route on the ui level always getting the container of the items might work out but your data-items need a reference to their logical parent data object.
The issue is that each TreeViewItem is itself an ItemsControl so they each manage their own containers for their children.
You have 3 choices:
You disable the virtualization of items: <TreeView VirtualizingStackPanel.IsVirtualizing="False">, but this could have impact on performance
You manage ItemContainerGenerator status for each item (some code provided as examples). Pretty complex.
You add a hierarchical view model to your hierarchy and implement a IsExpanded property to it for each node level. The best solution.
Disable virtualization:
<TreeView VirtualizingStackPanel.IsVirtualizing="False">
Good Luck...
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 delegate void CountToZeroAction();
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();
}
}
}
}
}

Categories