Quanto mais trabalho com o WPF, mais percebo quantas coisas estão faltando. Recentemente eu percebi que TreeView.SelectedItem
A propriedade é somente leitura e inadequada. Eu acho que não faz sentido explicar por que encadernação SelectedItem
Seria útil, então não deve haver surpresa na minha decepção.
Eu pesquisei no Google o problema e todos os recursos que encontrei estavam me orientando para lidá-lo com o código-behind ou adicionar um IsSelected
propriedade para minha classe modelo. Ambas as abordagens sofrem com o mesmo problema – um item não será selecionado se seus pais ainda não forem expandidos. Este foi um intervalo para mim, porque eu queria que a vista da árvore navegue até o item recém-selecionado, mesmo que não fosse imediatamente visível.
Resolvi esse problema escrevendo um pequeno comportamento que cuida disso para mim.
Comportamento personalizado
Percebi que, para resolver isso, teria que atravessar toda a hierarquia de nós de árvores, mas esse não era o único problema. Para acessar o IsSelected
e IsExpanded
propriedades que eu precisava para resolver uma referência a uma instância de TreeViewItem
que é um contêiner que envolve o modelo de dados.
Isso por si só pode ser realizado usando o TreeViewItem.ItemContainerGenerator.ContainerFromItem(...)
método. No entanto, se o nó ainda não estiver visível, o contêiner também não será inicializado, tornando o método retornar null
.
Para tornar visível nosso nó -alvo, precisamos expandir todos os seus nós ancestrais, um por um, começando do topo. Eu assumi ingenuamente que, ao expandir o nó do código, os contêineres de seus itens infantis ficarão imediatamente disponíveis, mas esse não é o caso, porque isso é tratado de forma assíncrona. Podemos, no entanto, assinar o Loaded
evento de cada item de dados, que será acionado assim que o controle for carregado.
Geralmente, a abordagem se parece com a seguinte:
- Assine o
Loaded
evento de todos os itens de dados usando um estilo - Quando
SelectedItem
Mudanças, passe por todos os nós da árvore carregada e tente localizar o nó de destino - Se conseguirmos encontrá -lo, selecione e saia cedo
- Se encontrarmos seus pais, expanda -o para que possamos continuar a pesquisa depois de carregar
- Quando um dos nós que expandimos é carregado, ele desencadeia um evento e começamos de novo do topo
Aqui está o comportamento que implementei:
public class TreeViewSelectionBehavior : Behavior<TreeView>
{
public delegate bool IsChildOfPredicate(object nodeA, object nodeB);
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(
nameof(SelectedItem),
typeof(object),
typeof(TreeViewSelectionBehavior),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnSelectedItemChanged
)
);
public static readonly DependencyProperty HierarchyPredicateProperty =
DependencyProperty.Register(
nameof(HierarchyPredicate),
typeof(IsChildOfPredicate),
typeof(TreeViewSelectionBehavior),
new FrameworkPropertyMetadata(null)
);
public static readonly DependencyProperty ExpandSelectedProperty =
DependencyProperty.Register(
nameof(ExpandSelected),
typeof(bool),
typeof(TreeViewSelectionBehavior),
new FrameworkPropertyMetadata(false)
);
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var behavior = (TreeViewSelectionBehavior) sender;
if (behavior._modelHandled) return;
if (behavior.AssociatedObject == null)
return;
behavior._modelHandled = true;
behavior.UpdateAllTreeViewItems();
behavior._modelHandled = false;
}
private readonly EventSetter _treeViewItemEventSetter;
private bool _modelHandled;
// Bindable selected item
public object SelectedItem
{
get => GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}
// Predicate that checks if two items are hierarchically related
public IsChildOfPredicate HierarchyPredicate
{
get => (IsChildOfPredicate) GetValue(HierarchyPredicateProperty);
set => SetValue(HierarchyPredicateProperty, value);
}
// Should expand selected?
public bool ExpandSelected
{
get => (bool) GetValue(ExpandSelectedProperty);
set => SetValue(ExpandSelectedProperty, value);
}
public TreeViewSelectionBehavior()
{
_treeViewItemEventSetter = new EventSetter(
FrameworkElement.LoadedEvent,
new RoutedEventHandler(OnTreeViewItemLoaded)
);
}
// Update state of all items starting with given, with optional recursion
private void UpdateTreeViewItem(TreeViewItem item, bool recurse)
{
if (SelectedItem == null)
return;
var model = item.DataContext;
// If we find the item we're looking for - select it
if (SelectedItem == model && !item.IsSelected)
{
item.IsSelected = true;
if (ExpandSelected)
item.IsExpanded = true;
}
// If we find the item's parent instead - expand it
else
{
// If HierarchyPredicate is not set, this will always be true
bool isParentOfModel = HierarchyPredicate?.Invoke(SelectedItem, model) ?? true;
if (isParentOfModel)
item.IsExpanded = true;
}
// Recurse into children in case some of them are already loaded
if (recurse)
{
foreach (var subitem in item.Items)
{
var tvi = item.ItemContainerGenerator.ContainerFromItem(subitem) as TreeViewItem;
if (tvi != null)
UpdateTreeViewItem(tvi, true);
}
}
}
// Update state of all items
private void UpdateAllTreeViewItems()
{
var treeView = AssociatedObject;
foreach (var item in treeView.Items)
{
var tvi = treeView.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
if (tvi != null)
UpdateTreeViewItem(tvi, true);
}
}
// Inject Loaded event handler into ItemContainerStyle
private void UpdateTreeViewItemStyle()
{
if (AssociatedObject.ItemContainerStyle == null)
{
var style = new Style(typeof(TreeViewItem),
Application.Current.TryFindResource(typeof(TreeViewItem)) as Style);
AssociatedObject.ItemContainerStyle = style;
}
if (!AssociatedObject.ItemContainerStyle.Setters.Contains(_treeViewItemEventSetter))
AssociatedObject.ItemContainerStyle.Setters.Add(_treeViewItemEventSetter);
}
private void OnTreeViewItemsChanged(object sender, NotifyCollectionChangedEventArgs args)
{
UpdateAllTreeViewItems();
}
private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> args)
{
if (_modelHandled) return;
if (AssociatedObject.Items.SourceCollection == null) return;
SelectedItem = args.NewValue;
}
private void OnTreeViewItemLoaded(object sender, RoutedEventArgs args)
{
UpdateTreeViewItem((TreeViewItem) sender, false);
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged += OnTreeViewItemsChanged;
UpdateTreeViewItemStyle();
_modelHandled = true;
UpdateAllTreeViewItems();
_modelHandled = false;
}
protected override void OnDetaching()
{
base.OnDetaching();
if (AssociatedObject != null)
{
AssociatedObject.ItemContainerStyle?.Setters?.Remove(_treeViewItemEventSetter);
AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged -= OnTreeViewItemsChanged;
}
}
}
Para facilitar a verificação se um nó é um filho de outro nó, eu defini uma propriedade chamada HierarchyPredicate
. Se não estiver definido, o comportamento apenas expandirá cegamente todos os nós até encontrar o item que estamos procurando. O predicado pode ajudar a otimizar esse processo.
Uma vez que esse comportamento seja anexado, ele chama UpdateTreeViewItemStyle()
para injetar um manipulador para o Loaded
evento dentro ItemContainerStyle
. Precisamos ouvir este evento para lidar com nós que foram expandidos. Para garantir a máxima compatibilidade, ele estende um estilo existente se conseguir encontrar um ou criar um novo.
Também liga UpdateAllTreeViewItems()
Depois de anexar. Isso passa por todas as crianças da vista da árvore e, por sua vez UpdateTreeViewItem(...)
neles.
Uso
Você pode anexar esse comportamento a uma visão de árvore como esta:
<TreeView ItemsSource="{Binding Items}">
<i:Interaction.Behaviors>
<behaviors:TreeViewSelectionBehavior ExpandSelected="True"
HierarchyPredicate="{Binding HierarchyPredicate}"
SelectedItem="{Binding SelectedItem}" />
</i:Interaction.Behaviors>
<TreeView.ItemTemplate>
<!-- ... -->
</TreeView.ItemTemplate>
</TreeView>
Quando SelectedItem
é alterado do modelo de vista, o comportamento atravessa a hierarquia enquanto utiliza HierarchyPredicate
Para encontrar o nó correto, selecionando -o. Um opcional ExpandSelected
O parâmetro determina se o item selecionado também deve ser expandido.
Se o usuário mudar SelectedItem
Na interface do usuário, ele funciona como você esperaria e propaga o novo valor ao modelo de visualização.

Luis es un experto en Inteligência Empresarial, Redes de Computadores, Gestão de Dados e Desenvolvimento de Software. Con amplia experiencia en tecnología, su objetivo es compartir conocimientos prácticos para ayudar a los lectores a entender y aprovechar estas áreas digitales clave.