I'm trying to select开发者_开发百科 a TreeViewItem by ID, but having problems getting it to work past the first (root) level. I've done so much reading round on this and am using the method below.
private static bool SetSelected(ItemsControl parent, INestable itemToSelect) {
if(parent == null || itemToSelect == null) {
return false;
}
foreach(INestable item in parent.Items) {
if(item.ID == itemToSelect.ID) { // just comparing instances failed
TreeViewItem container = parent.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
if(container != null) {
container.IsSelected = true;
container.Focus();
return true;
}
}
ItemsControl childControl = parent.ItemContainerGenerator.ContainerFromItem(item) as ItemsControl;
if(SetSelected(childControl, itemToSelect))
return true;
}
return false;
}
INestable is the base level interface, implemented by IGroup and IAccount:
public interface INestable {
string ID { get; set; }
...
}
public interface IAccount : INestable {
...
}
public interface IGroup : INestable {
public IList<INestable> Children
...
}
I think it must have something to do with the datatemplates (perhaps):
<HierarchicalDataTemplate DataType="{x:Type loc:IGroup}" ItemsSource="{Binding Children}" x:Key="TreeViewGroupTemplate">
<HierarchicalDataTemplate DataType="{x:Type loc:IAccount}" x:Key="TreeViewAccountTemplate">
The Template selector for the treeview returns thr group template for IGroups and the account template for IAccounts:
<conv:TreeTemplateSelector x:Key="TreeTemplateSelector" AccountTemplate="{StaticResource TreeViewAccountTemplate}" GroupTemplate="{StaticResource TreeViewGroupTemplate}"/>
<TreeView ItemTemplateSelector="{StaticResource TreeTemplateSelector}">
It works for all top level items, just nothing below that, and debugging confirms parent.ItemContainerGenerator does contain the items for all levels.
I know there's a lot of code but I'm burning through hours trying to get this to work. Thanks for any help. :)
The problem is that the nested ItemContainerGenerators
aren't generated all at the beginning, they are generated on-demand. And even more so, they are generated in a separate thread, so you have to listen to a StatusChanged
on the generator to be sure it is ready =(
Some people suggest to play with the Dispatcher
(like in this Bea's post). I tried to implement the Dispatcher solution, but it didn't work for some reason... the generators are still empty =(
So I ended up with another one, where you specifically ask the tree to update its layout, which causes the generation for the expanded nodes. Here's the final method... you might want to test it a little to verify that it suites your needs. It might collapse some nodes that were expanded before its run.
private static bool SetSelected(TreeView treeView, ItemsControl parentControl, INestable itemToSelect)
{
if (parentControl == null || itemToSelect == null)
{
return false;
}
foreach (INestable item in parentControl.Items)
{
TreeViewItem container = parentControl.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
if (item.ID == itemToSelect.ID)
{ // just comparing instances failed
container.IsSelected = true;
container.Focus();
return true;
}
container.IsExpanded = true;
treeView.UpdateLayout();
WaitForPriority(DispatcherPriority.Background);
if (SetSelected(treeView, container, itemToSelect))
return true;
else
container.IsExpanded = false;
}
return false;
}
I think it doesn't work since item is collapsed and its container is not instantiated. Thus trying to select TreeViewItem directly is definitely not the best way to go.
Instead we use MVVM approach. Every viewmodel object should have IsSelected property. Then you bind TreeViewItem.IsSelected propery to it.
In your case it would go like this
CS:
public interface INestable : INotifyPropertyChanged
{
string ID { get; set; }
// Make sure you invoke PropertyChanged in setter
bool IsSelected { get; set; }
event PropertyChangedEventHandler PropertyChanged;
...
}
XAML:
<TreeView ...>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
Now you can go through your model and set IsSelected
property there.
You may also want to track IsExpanded
property in a same way...
To get more information about TreeView read this wonderful article by Josh Smith: Simplifying the WPF TreeView by Using the ViewModel Pattern
Hope this helps.
Although the accepted answer will work most of the time. It could happen to not work because the object is created on another thread not controlled at all by the dispatcher.
Like mentionned earlier, the problem lie from the fact that the TreeViewItem is created in another thread from the dispatcher one.
I personnally think that the proper solution is more complex. I know it is bad to hear but I really think it is so. My code should work when virtualized or not. Also, you could remove any reference not needed (I did not verify).
My solution is based on a data model where every node inherits from the same root: MultiSimBase object, but it is not a requirement.
Everything start from SetSelectedTreeViewItem() which activate (+set focus and bring into view) the newly added item.
Hope it could help or inspired some... Happy coding !!!
Code of the form:
// ******************************************************************
private List<MultiSimBase> SetPathListFromRootToNode(MultiSimBase multiSimBase, List<MultiSimBase> listTopToNode = null)
{
if (listTopToNode == null)
{
listTopToNode = new List<MultiSimBase>();
}
listTopToNode.Insert(0, multiSimBase);
if (multiSimBase.Parent != null)
{
SetPathListFromRootToNode(multiSimBase.Parent, listTopToNode);
}
return listTopToNode;
}
// ******************************************************************
private void SetSelectedTreeViewItem(MultiSimBase multiSimBase)
{
List<MultiSimBase> listOfMultiSimBasePathFromRootToNode = SetPathListFromRootToNode(multiSimBase);
TreeViewStudy.SetItemHierarchyVisible(listOfMultiSimBasePathFromRootToNode, (tvi) =>
{
tvi.IsSelected = true;
tvi.Focus();
tvi.BringIntoView();
});
}
And now generic code:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;
namespace HQ.Util.Wpf.WpfUtil
{
public static class TreeViewExtensions
{
public delegate void OnTreeViewVisible(TreeViewItem tvi);
private static void SetItemHierarchyVisible(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 (listOfRootToNodePath.Count == 0)
{
if (onTreeViewVisible != null)
onTreeViewVisible(tvi);
}
else
{
if (!tvi.IsExpanded)
tvi.IsExpanded = true;
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.
/// </summary>
/// <param name="treeView">TreeView where an item has to be set visible</param>
/// <param name="collectionOfRootToNodePath">Any of collection that implement ICollection like a generic List.
/// The collection should have every objet of the path to the targeted item from the top 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, IList listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
{
ItemContainerGenerator icg = treeView.ItemContainerGenerator;
if (icg == null)
return; // Is tree loaded and initialized ???
SetItemHierarchyVisible(icg, listOfRootToNodePath, onTreeViewVisible);
}
And
using System;
namespace HQ.Util.Wpf.WpfUtil
{
// Requested to unsubscribe into an anonymous method that is a delegate used for a one time execution
// http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/df2773eb-0cc1-4f3a-a674-e32f2ef2c3f1/
public class ActionHolder
{
public void Execute()
{
if (Action != null)
{
Action();
}
}
public Action Action { get; set; }
}
}
精彩评论