I have a program with a TreeView Item that holds child nodes with numerical headers. When adding child nodes to the TreeViewItem I would like to add them numerically, but I don't know how to add nodes in between other ones.
I'm guessing I would call to a function that would sort through the child nodes, convert the headers to integers, compare the header with the entered value, and then stop when a node header is greater than the value entered. The new node would have to be entered before the node with the header greater than the new node being added.
Is this the best approach, or is there a better one? Please demonstrate the easiest way to go about this. FYI, my TreeViewItem is in my main window, and being worked on from a separate window.
This should give you an idea of how I add child nodes to my TreeViewItem:
//Global boolean
// bool isDuplicate;
//OKAY - Add TreeViewItems to location & Checks if location value is numerical
private void button2_Click(object sender, RoutedEventArgs e)
{
//Checks to see if TreeViewItem is a numerical value
string Str = textBox1.Text.Trim();
double Num;
bool isNum = double.TryParse(Str, out Num);
//If not numerical value, warn user
if (isNum == false)
MessageBox.Show("Value must be Numerical");
else //else, add location
{
//close window
this.Close();
//Query for Window1
var mainWindow = Application.Current.Windows
.Cast<Window1>()
.FirstOrDefault(window => window is Window1) as Window1;
//declare TreeViewItem from mainWindow
TreeViewItem locations = mainWindow.TreeViewItem;
//Passes to function -- checks for DUPLICATE locations
CheckForDuplicate(TreeViewItem.Items, textBox1.Text);
//if Duplicate exists -- warn user
if (isDuplicate == true)
MessageBox.Show("Sorry, the number you entered is a duplicate of a current node, please try again.");
else //else -- create node
{
//Creates TreeViewItems for Location
TreeViewItem newNode = new TreeViewItem();
//Sets Headers for new locations
newNode.Header = textBox1.Text;
//Add TreeView Item to Locations & Blocking Database
mainWindow.TreeViewItem.Items.Add(newNode);
}
}
}
//Checks to see whether the header entered is a DUPLICATE
private void CheckForDuplicate(ItemCollection treeViewItems, string input)
{
for (int index = 0; index < treeViewItems.Count; index++)
{
TreeViewItem item = (TreeViewItem)treeViewItems[index];
string header = item.Header.ToString();
if (header == input)
{
isDuplicate = true;
break;
}
else
isDuplicate = false;
}
}
Thank you.
Here is a little example for your special case using ModelView, Binding and CollectionView
ModelViews
public class MainViewModel
{
private readonly ObservableCollection<TreeItemViewModel> internalChildrens;
public MainViewModel(string topLevelHeader) {
this.TopLevelHeader = topLevelHeader;
this.internalChildrens = new ObservableCollection<TreeItemViewModel>();
var collView = CollectionViewSource.GetDefaultView(this.internalChildrens);
collView.SortDescriptions.Add(new SortDescription("Header", ListSortDirection.Ascending));
this.Childrens = collView;
}
public string TopLevelHeader { get; set; }
public IEnumerable Childrens { get; set; }
public bool AddNewChildren(double num) {
var numExists = this.internalChildrens.FirstOrDefault(c => c.Header == num) != null;
if (!numExists) {
this.internalChildrens.Add(new TreeItemViewModel() {Header = num});
}
return numExists;
}
}
public class TreeItemViewModel
{
public double Header { get; set; }
}
Xaml
<Window x:Class="WpfStackOverflowSpielWiese.Window21"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window21"
Height="300"
Width="300"
x:Name="tvExample">
<Grid DataContext="{Binding ElementName=tvExample, Path=ViewModel, Mode=OneWay}">
<StackPanel Orientation="Vertical">
<TextBox x:Name="tb" />
<Button Content="Add"
Click="AddNewItemOnClick" />
<TreeView>
<TreeViewItem Header="{Binding TopLevelHeader, Mode=OneWay}"
ItemsSource="{Binding Childrens, Mode=OneWay}">
<TreeViewItem.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Header}" />
</HierarchicalDataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
</TreeView>
</StackPanel>
</Grid>
</Window>
Xaml code behind
public partial class Window21 : Window
{
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel", typeof(MainViewModel), typeof(Window21), new PropertyMetadata(default(MainViewModel)));
public MainViewModel ViewModel {
get { return (MainViewModel)this.GetValue(ViewModelProperty); }
set { this.SetValue(ViewModelProperty, value); }
}
public Window21() {
this.InitializeComponent();
this.ViewModel = new MainViewModel("TopLevel");
}
private void AddNewItemOnClick(object sender, RoutedEventArgs e) {
double Num;
var isNum = double.TryParse(this.tb.Text, out Num);
//If not numerical value, warn user
if (isNum == false) {
MessageBox.Show("Value must be Numerical");
return;
}
var isDuplicate = this.ViewModel.AddNewChildren(Num);
if (isDuplicate) {
MessageBox.Show("Sorry, the number you entered is a duplicate of a current node, please try again.");
return;
}
}
}
hope that helps
Related
I have a listview which contains the customer informations. There is a search text box above the that listview. When you type anything into the textbox then it higlights the matched item in the listview. But , the problem is that it makes search only in the visual side of the listview. It doesn't search in the not scrolled side of the listview(buttom of the listview). My code is below. Please have a look.
private void FindListViewItem(DependencyObject obj)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
ListViewItem lv = obj as ListViewItem;
if (lv != null)
{
HighlightText(lv);
}
FindListViewItem(VisualTreeHelper.GetChild(obj as DependencyObject, i));
}
}
private void HighlightText(Object itx)
{
if (itx != null)
{
if (itx is TextBlock)
{
Regex regex = new Regex("(" +TxtSearch.Text + ")", RegexOptions.IgnoreCase);
TextBlock tb = itx as TextBlock;
if (TxtSearch.Text.Length == 0)
{
string str = tb.Text;
tb.Inlines.Clear();
tb.Inlines.Add(str);
return;
}
string[] substrings = regex.Split(tb.Text);
tb.Inlines.Clear();
foreach (var item in substrings)
{
if (regex.Match(item).Success)
{
Run runx = new Run(item);
runx.Background = Brushes.Lime;
tb.Inlines.Add(runx);
if (tb.IsMouseOver)
{
tb.IsEnabled = false;
}
}
else
{
tb.Inlines.Add(item);
tb.IsEnabled = false;
}
}
return;
}
else
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(itx as DependencyObject); i++)
{
HighlightText(VisualTreeHelper.GetChild(itx as DependencyObject, i));
}
}
}
}
This happens because the ListView, by default, uses virtualization for its content. This means that the ListViewItems are created when they are needed. If you didn't scroll the ListView, some ListViewItems will not be created and VisualTreeHelper.GetChildrenCount will not be able to return those ListViewItems.
To achieve what you want, you can:
disable ListView virtualization by setting: VirtualizingStackPanel.IsVirtualizing="False" on your ListView (not recommended if you have many items in your list).
you can enforce the creation of the ListViewItem which are not visible by calling IItemContainerGenerator.GenerateNext and IItemContainerGenerator.PrepareItemContainer (not recommended at all). (also take a look at this)
find a better logic to highlight your ListViewItems :) (recommended). (for example search on your collection for the items you want to highlight instead of searching on the UI elements that are only displaying your items. Then mark the items found as highlighted and base on this, display the ListViewItems accordingly (with a different template or style))
You can do this in several ways. Here is one way that i think would work in your scenario and partially with your code and still use virtualization.
Use a data template for the list view item, and create an event handler for loaded event, something like:
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Loaded="FrameworkElement_OnLoaded"/>
</DataTemplate>
</ListView.ItemTemplate>
In the OnLoaded event handler call your HighlightText method on the sender:
HighlightText(sender)
In order to trigger the loaded event you'll need to refresh the list view each time the search string will change. Something like ListView.Items.Refresh() should do it.
You could improve this a bit by adding a small timer on the search text changed, so the user will be able to finish typing when it's searching for something.
There are other, more elegant ways to handle this, but for your case i think this should work.
In Addition to my Comment:
Use a Property and a Observable Collection and directly filter on that Collection.
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ObservableCollection<Entry> MyCollection {get;set;}
public MainWindow()
{
InitializeComponent();
MyCollection = new ObservableCollection<Entry>();
MyCollection.Add(new Entry() { Name = "Test" });
MyCollection.Add(new Entry() { Name = "ABCD" });
MyCollection.Add(new Entry() { Name = "TESTABC" });
MyCollection.Add(new Entry() { Name = "BCDtest" });
this.MyListView.DataContext = this;
}
private void searchTerm_KeyUp(object sender, KeyEventArgs e)
{
String term = ((TextBox)sender).Text;
foreach (Entry entry in this.MyCollection)
{
if (entry.Name.Contains(term))
entry.Highlight();
else
entry.UnHighlight();
}
}
}
public class Entry : INotifyPropertyChanged
{
public String Name { get; set; }
public Color BGColor { get; set; }
public SolidColorBrush BGBrush
{
get
{
return new SolidColorBrush(this.BGColor);
}
}
public Entry()
{
this.UnHighlight();
}
public void Highlight()
{
this.BGColor = Colors.Yellow;
this.NotifyPropertyChanged("BGBrush");
}
public void UnHighlight()
{
this.BGColor = Colors.White;
this.NotifyPropertyChanged("BGBrush");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
along with
<Grid>
<DockPanel>
<TextBox DockPanel.Dock="Top" Name="searchTerm" KeyUp="searchTerm_KeyUp"></TextBox>
<ListView Name="MyListView" ItemsSource="{Binding MyCollection}" >
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Background="{Binding BGBrush}" Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DockPanel>
</Grid>
And you are done. No need to manually touch the listview at any time. (To Increase Performance: For the Raising of the PropertyChanged Event you may want to add a check, if its really changing, or if it has been set to white from white etc.)
I am trying to implement a list that contains items of a certain type, a Session. Each Session contains a list that contains the type Note. I want to display these Notes in the list under their respective Session header.
Currently I have tried two different methods. The first way was to use ItemsControls as ControlTemplate for the ListBoxItems. This is what I used in the picture below and it is how I want the list to look like. Each red rectangle shows a Session, the items below the header are the Notes. The problem then is that the selection from the ListBox selects ItemsControls instead of each separate Note.
The other way I tried to implement the list is to give each Note a property of which Session it belongs to in order to use a GroupStyle on the ListBox. If I then set the ItemsSource of the ListBox to a list of Notes instead of Sessions I'll get a list that looks like the picture and that has selection of notes. The problem now is that I want the list to show Sessions that doesn't contain any Notes as well.
Does anyone know what I should use to implement a list with selection and that works the way I have described?
MainWindow.xaml:
<TreeView ItemsSource="{Binding}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Session}" ItemsSource="{Binding Path=Notes}">
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:Note}">
<Expander Header="{Binding Path=Notek}">
<TextBlock Foreground="Red" Text="{Binding Path=Details}" />
</Expander>
</DataTemplate>
</TreeView.Resources>
</TreeView>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<Session> sessions = new List<Session>();
for (int i = 0; i < 5; i++)
{
List<Note> notes = new List<Note>();
for (int j = i * 5; j < (i + 1) * 5; j++)
{
Note note = new Note()
{
Notek = string.Format("Note {0}", j),
Details = string.Format("Note j = {0}{1}j*j = {2}", j, System.Environment.NewLine, j*j)
};
notes.Add(note);
}
Session session = new Session()
{
Name = string.Format("Session # {0}", i),
Notes = notes
};
sessions.Add(session);
}
DataContext = sessions;
}
}
public class Session
{
public string Name { get; set; }
public List<Note> Notes { get; set; }
}
public class Note
{
public string Notek { get; set; }
public string Details { get; set; }
}
I think that you can style your HierarchicalDataTemplate as you want. I just show you the example. I think its easier rather than ItemsControl with event handlers.
To create the answer I will assume the following data model:
class Session
{
public IEnumerable<Note> Notes { get; }
}
class Note { }
This requires some coding to sync up the list boxes. I have created an attached property called 'ListBoxGroup'. All listboxes with the same group name can only have a single shared selected item. It is quite a lot of code so it's at the bottom.
Important to note: The listboxgroup for a listbox cannot be changed after originally set, and it doesn't support removal of items, doesn't check for nulls etc. So if you need to change sessions at runtime you should remove items from their groups, check if a listbox is removed from the visual tree, etc.
First the XAML for the page:
xmlns:local="clr-namespace:YourApplication.YourNamespace"
<!-- ItemsControl does not have selection -->
<ItemsControl ItemsSource="{Binding SessionList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<!-- Header for the session -->
<Border Background="Gray">
<TextBlock Text="{Binding Name}" />
</Border>
<!-- listbox for notes -->
<ListBox ItemsSource="{Binding Notes}" local:ListBoxGroup.GroupName="Group1">
<ListBox.ItemTemplate>
<DataTemplate>
<!-- Template for a single note -->
<TextBlock Text="{Binding Description}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Below is C# code for the ListBoxGroup property:
public static class ListBoxGroup
{
public static string GetGroupName(DependencyObject obj)
{
return (string)obj.GetValue(GroupNameProperty);
}
public static void SetGroupName(DependencyObject obj, string value)
{
obj.SetValue(GroupNameProperty, value);
}
// Using a DependencyProperty as the backing store for GroupName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GroupNameProperty =
DependencyProperty.RegisterAttached("GroupName", typeof(string), typeof(ListBoxGroup), new UIPropertyMetadata(null, ListBoxGroupChanged));
private static Dictionary<string, List<ListBox>> _listBoxes = new Dictionary<string, List<ListBox>>();
private static void ListBoxGroupChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
string newValue = e.NewValue as string;
ListBox listBox = obj as ListBox;
if (newValue == null || listBox == null) return;
if (_listBoxes.ContainsKey(newValue))
{
_listBoxes[newValue].Add(listBox);
}
else
{
_listBoxes.Add(newValue, new List<ListBox>() { listBox });
}
listBox.SelectionChanged += new SelectionChangedEventHandler(listBox_SelectionChanged);
listBox.PreviewKeyUp += new System.Windows.Input.KeyEventHandler(listBox_KeyUp);
}
static void listBox_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
ListBox listBox = sender as ListBox;
if (e.Key == System.Windows.Input.Key.Up && listBox.SelectedIndex == 0)
{
//move to previous
string groupName = GetGroupName(listBox);
List<ListBox> group = _listBoxes[groupName];
int senderIndex = group.IndexOf(listBox);
if (senderIndex != 0)
{
listBox.SelectedItem = null;
ListBox beforeSender = group[senderIndex - 1];
int index = beforeSender.Items.Count - 1;
beforeSender.SelectedIndex = index;
var container = beforeSender.ItemContainerGenerator.ContainerFromIndex(index);
(container as FrameworkElement).Focus();
}
}
else if (e.Key == System.Windows.Input.Key.Down
&& listBox.SelectedIndex == listBox.Items.Count - 1)
{
//move to next
string groupName = GetGroupName(listBox);
List<ListBox> group = _listBoxes[groupName];
int senderIndex = group.IndexOf(listBox);
if (senderIndex != group.Count - 1)
{
listBox.SelectedItem = null;
ListBox afterSender = group[senderIndex + 1];
afterSender.SelectedIndex = 0;
var container = afterSender.ItemContainerGenerator.ContainerFromIndex(0);
(container as FrameworkElement).Focus();
}
}
}
static void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
ListBox listBox = sender as ListBox;
string groupName = GetGroupName(listBox);
foreach (var item in _listBoxes[groupName])
{
if (item != listBox)
{
item.SelectedItem = null;
}
}
}
}
}
I have a listbox containing checkboxes. I want to get all the checkbox checked items content in a string array. How can I get this?
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ListBox x:Name="TrackingView1" Margin="9,0,2,5">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox x:Name="fileNameLinkButton" Content="{Binding BindsDirectlyToSource=True}" FontFamily="Segoe WP Semibold" Foreground="Black" FontSize="20" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
You can use VisualTreeHelper to retrieve the datatemplate items,
use this method to get the first item of datatemplate
//method for finding first element of the listbox data template
private T FindFirstElementInVisualTree<T>(DependencyObject parentElement) where T : DependencyObject
{
var count = VisualTreeHelper.GetChildrenCount(parentElement);
if (count == 0)
return null;
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(parentElement, i);
if (child != null && child is T)
{ return (T)child; }
else
{
var result = FindFirstElementInVisualTree<T>(child);
if (result != null)
return result;
}
}
return null;
}
then use this code in the event you want to, for example on a button click
int itemsCount = this.TrackingView1.Items.Count;
List<string> myList = new List<string>();
for (int i = 0; i < itemsCount; i++)
{
ListBoxItem item = (ListBoxItem)this.TrackingView1.ItemContainerGenerator.ContainerFromIndex(i);
CheckBox tagregCheckBox = FindFirstElementInVisualTree<CheckBox>(item);
if((bool)tagregCheckBox.IsChecked)
myList.Add(tagregCheckBox.Content.ToString());
}
In a classic TextBook approach, You could bind "TrackingView1" to a IList, where PersonClass looks some thing like
public class PersonClass
{
public string Name { get; set; }
public bool IsSelected { get; set; }
public PersonClass(string name, bool isSelected)
{
this.Name = name;
this.IsSelected = isSelected;
}
}
and at the point where you want to collect your data,
string[] checkedNames = (from name in Names
where name.IsSelected
select name.Name).ToArray();
I would actually avoid the ToArray(), and accessing the UI directly
My code is as below.
<ListBox x:Name="lstBoxMarket" BorderThickness="0" Height="Auto" HorizontalAlignment="Center" Width="200" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
<ListBox.ItemTemplate>
<HierarchicalDataTemplate>
<CheckBox IsChecked="{Binding Checked}" CommandParameter="{Binding MarketId}" Tag="{Binding MarketId}" Content="{Binding Market}" Foreground="#FF3D66BE" Name="chkMarket"/>
</HierarchicalDataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I want to access the selected and deselected checkboxes in the list on click of save button . I am unable to access chkMarket straight away. Can anyone help?
Starting from your code I tried something like that
// find all T in the VisualTree
public static IEnumerable<T> FindVisualChildren<T>(DependencyObject parent)
where T : DependencyObject
{
List<T> foundChilds = new List<T>();
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
T childType = child as T;
if (childType == null)
{
foreach(var other in FindVisualChildren<T>(child))
yield return other;
}
else
{
yield return (T)child;
}
}
}
Then in your MainWindow
private void button1_Click(object sender, RoutedEventArgs e)
{
// find all checkboxes in my window
IEnumerable<CheckBox> myBoxes = FindVisualChildren<CheckBox>(this);
int numChecked = 0;
foreach(CheckBox cb in myBoxes)
{
if(cb.Name != "chkMarket")
continue;
if (cb.IsChecked == true)
numChecked++;
}
MessageBox.Show("Checked items = " + numChecked);
}
My viewmodel code is
public class ViewModel
{
public ViewModel()
{
_persons = new ObservableCollection<Person>();
_persons.Add(new Person() { Name = "Paul", Checked = false });
_persons.Add(new Person() { Name = "Brian", Checked = true });
}
private ObservableCollection<Person> _persons;
public ObservableCollection<Person> Persons
{
get { return _persons; }
}
}
public class Person
{
public String Name { get; set; }
public Boolean Checked { get; set; }
}
You should be able to see the message "Checked items=1".
Hope this helps
Since it was 2 way binding i could access the values selected by the checkboxes from the item source of listbox.
DataTable lstBoxMarketItemSourceDT = ((DataView)lstBoxMarket.ItemsSource).ToTable();
"Checked" column in the data table retrieved gives the updated check box values.
I'm trying to select a TreeViewItem. Now, I have access to the containing TreeViewItem and have told it to expand so I can select its kid. If it's already expanded all is well, if it's not then I run this code:
EventHandler selector = new EventHandler(delegate
{
if (selectedDirectoryTreeItem.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
TreeViewItem want = selectedDirectoryTreeItem.ItemContainerGenerator.ContainerFromItem(dirWeWantSelected) as TreeViewItem;
if (want == null)
return;
want.IsSelected = true;
// selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged -= selector;
}
});
selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged += selector;
So my question is, why wont it select? want is always null. I'm scouring the interwebs looking for another way of doing this but it would be cool if somebody could explain this to me
I've personally always found it easiest to stick a Selected property into my model object and then just bind the TreeViewItem Selected property to the Selected property of the model. Here is some code:
Model
public class Data : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Data()
{
DataItems = new List<Data>();
}
public string Name { get; set; }
private bool _selected;
public bool Selected
{
get { return _selected; }
set
{
_selected = value;
OnPropertyChanged("Selected");
}
}
public List<Data> DataItems { get; set; }
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:controls="clr-namespace:MyControls;assembly=MyControls"
Title="Window1">
<Window.Resources>
<Style x:Key="CustomTreeViewItem" TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding Path=Selected, Mode=TwoWay}" />
<Setter Property="IsExpanded" Value="True" />
</Style>
</Window.Resources>
<DockPanel Background="Transparent">
<TreeView x:Name="_tvTest" DockPanel.Dock="Left" ItemContainerStyle="{StaticResource CustomTreeViewItem}" Width="300" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Data}" ItemsSource="{Binding DataItems}">
<TextBlock Text="{Binding Name}" Padding="2" />
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Padding="2" />
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<Button Content="Select Random TreeView Item" Click="Button_Click" Height="50" Width="200" />
</DockPanel>
</Window>
Code behind
public partial class Window1 : Window
{
private Random _random;
private List<Data> _dataItems;
public Window1()
{
InitializeComponent();
_dataItems = Init();
_tvTest.ItemsSource = _dataItems;
_random = new Random(5);
}
private List<Data> Init()
{
List<Data> dataItems = new List<Data>();
for (int i = 1; i <= 10; i++)
{
Data d1 = new Data();
d1.Name = "Data:" + i.ToString();
for (int j = 1; j <= 4; j++)
{
Data d2 = new Data();
d2.Name = "Data:" + i.ToString() + j.ToString();
d1.DataItems.Add(d2);
}
dataItems.Add(d1);
}
return dataItems;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
int index = _random.Next(0, 9);
int subIndex = _random.Next(0, 3);
if (subIndex == 0)
_dataItems[index].Selected = true;
else
_dataItems[index].DataItems[subIndex - 1].Selected = true;
}
}
In my opinion, this is a bug in WPF, but I have run into the same issue several times. I never trust the ItemContainerGenerator's Status and instead loop on separate thread as such:
private void _selectTreeViewHelper(object directory) {
TreeViewItem want = null;
bool broke = false; //probably some sort of wait timeout instead, but works for sake of example
while (true) {
Dispatcher.Invoke(new Action(delegate {
want = selectedDirectoryTreeItem.ItemContainerGenerator.ContainerFromItem(directory) as TreeViewItem;
if (want != null && want.IsLoaded) {
want.IsSelected = true;
broke = true;
}
}));
if (broke) { break; }
Thread.Sleep(100);
}
}
Then call it:
var thread = new Thread(new ParameterizedThreadStart(_selectTreeViewHelper));
thread.Start(dirWeWantSelected);
Pain I know, but it works.
Thanks for the help, I figured it out. Well it works anyway but i'm not entirely sure why it didn't before... if anyone can tell me it would be lovely... I kinda combined elements of the two responses above and then added on a little bit to make it work. For some reason, no matter what i do I cant select the TVI (TreeViewElement) if its parent wasn't already expanded, even if the parent TVI said its contents were generated and, in fact, had contents i could iterate through. My solution was thus to wait for the contents to be generated and then itrate through them and find the one i wanted. It's really weird to me that I couldn't just grab a container given its contents. Meh. My code could stand to be refactored a little bit but here it is: (works perfectly)
public void listItemClickClick(object sender, RoutedEventArgs e)
{
try
{
UserFile fil = (UserFile)(sender as ListBoxItem).DataContext;
MessageBox.Show("to do: download stuff");
return;
}
catch (InvalidCastException)
{
}
try
{
dirWeWantSelected = (Directory)(sender as ListBoxItem).DataContext;
}
catch (InvalidCastException)
{
MessageBox.Show("this should never happen");
}
selectedDirectoryTreeItem.IsExpanded = true;
TreeViewItem want = null;
try
{
want = selectedDirectoryTreeItem.ItemContainerGenerator.ContainerFromItem(dirWeWantSelected) as TreeViewItem;
}
catch
{
MessageBox.Show("weird error");
}
if (want != null)
{
want.IsSelected = true;
}
else
{
selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
}
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (selectedDirectoryTreeItem.ItemContainerGenerator.Status
== System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
{
selectedDirectoryTreeItem.ItemContainerGenerator.StatusChanged
-= ItemContainerGenerator_StatusChanged;
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
new Action(DelayedAction));
}
}
void DelayedAction()
{
selectedDirectoryTreeItem.Items.MoveCurrentToFirst();
Directory curr;
do
{
curr = (Directory)selectedDirectoryTreeItem.Items.CurrentItem;
if (curr.id == dirWeWantSelected.id)
{
curr.Selected = true;
return;
}
selectedDirectoryTreeItem.Items.MoveCurrentToNext();
}
while (selectedDirectoryTreeItem.Items.CurrentItem != null);
}