OK - here is a programmatic solution to the problem. I hope this might save someone else 3 or 4 days of experimentation.
1. The first thing is to set up a class to represent the data that is going to be viewed - the fact that this control is called a TreeView is, I think, no accident. It is quite different to a Win32/WinForms control. As my original question concerned email
let's stick with that. Here is the class that we'll use.
// this will be displayed in the Silverlight TreeView
public class EmailItem
{
// constructor takes various arguments as one might expect
public EmailItem(string Title, string From,DateTime Date)
{
// by default a root item so no parent
this.Parent = null;
// the observable collection is very important here, it means that an instance of this class
// will get notified if the collection changes,
// as well as the container getting notified of changes - see the Add() code later.
this.Items = new ObservableCollection<EmailItem>();
// this is for sanity checking and general proof of understanding
this.Items.CollectionChanged +=
new NotifyCollectionChangedEventHandler(Items_CollectionChanged);
// assign the constructor parameters
this.Title = Title;
this.From = From;
this.Date = Date;
}
// the collection has changed - just spew a debug message for the moment
void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (sender != null)
{
Debug.WriteLine("Items_CollectionChanged: " + sender.ToString() + "(" + e.Action.ToString() + ")");
}
}
// our email properties.
public string Title { get; set; }
public string From { get; set; }
public DateTime Date { get; set; }
public EmailItem Parent { get; set;} // the parent emailItem - enables us to work back up through the hierarchy
// this is special - this collection will contain all of the
// emails which are children in the hierachy. Imagine them to be
// the collected replies to a specific email. The depth of the
// hierarchy is bounded only by systems limits.
public ObservableCollection<EmailItem> Items { get; set; }
}
2. Now we need to tell the tree view what is it going to display. The XAML here totallly dictates the appearance of the item in the view. This XAML snippet places a tree view on the layout and determines the content. Somewhere you will need to have added
a namespace to the XAML referencing the Silverlight toolkit (SLTK)
In English then, we've got a TreeView called trvEmails with an event handler called ctlTree_SelectedItemChanged that is invoked when the selection changes in the tree.
Next comes the clever/tricky bit - the ItemTemplate. This contains a HierarchicalDataTemplate declaration which has as it source binding a property called Items. This is bound directly to the Items property in the EmailItem class. Importantly Items is not
directly viewable - the bits of the email message we wish to display is specified by the StackPanel. Note I've chosen to orientate this horizontally - this means we get a columnar layout for the EmailItem with the content described by the 3 labels that follow.
Note that each label is bound to a separate property of the EmailItem class - in turn Title, From and Date. Just to show we are in control, the date is drawn as greyed text.
That, as they say, is that. Let's move on to the code.
3. Now if you have added the C# code and the XAML markup to a project, the tree, at runtime, will be empty. Here's some code to populate the tree:
// I've added this code to the page constructor
public Page()
{
InitializeComponent();
// add the first new email item to the root collection
EmailItem emi = new EmailItem("Email 1","Root", DateTime.Now);
trvEmails.Items.Add(emi);
// add a 'reply' to the first email
emi.Items.Add(new EmailItem("Email 1.1", "abc", DateTime.Now));
// add another root item
emi = new EmailItem("Email 2", "Root", DateTime.Now);
// insert it into the view collection
trvEmails.Items.Add(emi);
// and add a reply to the second item
emi.Items.Add(new EmailItem("Email 2.1", "abc",DateTime.Now));
// finally a third root item
trvEmails.Items.Add(new EmailItem("Email 3", "def", DateTime.Now));
}
This should be pretty self explanatory - what you get then is a tree that initially looks like this - note the arrows indicating that the item has children. Magic!
and here is the tree with the items expanded:
4. Neat. Let's code up the event handler that is called when the tree selection changes:
private void ctlTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
Debug.WriteLine("ctlTree_SelectedItemChanged");
EmailItem oldItem = e.OldValue as EmailItem;
EmailItem newItem = e.NewValue as EmailItem;
// _selectedEmailItem is declared within the class as:
// private EmailItem _selectedEmailItem;
_selectedEmailItem = newItem;
// see next paragraph
btnAdd.IsEnabled = (_selectedEmailItem != null);
if (oldItem != null)
{
Debug.WriteLine("Old: " + oldItem.Title);
}
if (newItem != null)
{
EmailItem emi = newItem;
while (emi != null)
{
Debug.WriteLine("Walking back to root: " + emi.Title);
emi = emi.Parent;
}
}
}
This handler updates an EmailItem instance (_selectedEmailItem) that is declared within the page code. This makes life a bit simpler in a moment,as I hope to show. Once we are sure we've updated the _selectedEmailItem we do a little bit more work to ensure
we've actually tamed the beast and it is working as we expect. Caveat: I'm not sure this is 100% neccessary. There may be a more generic navigation mechanism built into the TreeViewItem but I've not found it yet. What we do when the selection changes is to
walk back up the tree until the item under examination has a null parent - at each stage we print out the email title.
5. Now, in the case of the items we have inserted so far, we have not set the Parent property. I've skipped over it entirely This might be useful if you want to refer back to the email in your reply for example. Assume a button called btnAdd has been added
to the XAML and an suitable handler added to the C# code. Here's what the handler will do:
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
// invariant - cannot be null if button enabled
if (_selectedEmailItem != null)
{
// this new email is from jerry
EmailItem emi = new EmailItem("re:" + _selectedEmailItem.Title,"Jerry", DateTime.Now);
// and its parent is the current selection
emi.Parent = _selectedEmailItem;
// add the new child to the collection in the parent
_selectedEmailItem.Items.Add(emi);
}
}
This should be adequately commented - we add a new item to the tree as a child of the current selection. What is pretty clever is that at no point is the TreeView or any TreeViewItems involved. But, despite this absence of an explicit link, the action of
adding the email will ensure the view is updated, with the modified item now displaying the 'i've got child items' arrow. See green callouts.
6. Finally. That covers the essentials and answers my initial question. Hope you find this useful. Please feel free to comment/query/criticise as you see fit. I'd appreciate it if the latter was constructive in nature!
Unfortunately in Silverlight there are some things you can only do in XAML and not code. You can't currently create
DataTemplates (or
HierarchicalDataTemplates, etc.) in code (except for using
XamlReader.Load on a string of XAML).
You can still do most of your work in code if you prefer though. The trick is to create a regular
DataTemplate that defines how you like your items rendered and stick it in the
Resources of a named control. Then you can look it up with
DataTemplate emailTemplate = myControl.Resources["MyTemplate"] as DataTemplate;. Just create all your
TreeViewItems, set the
Header to be your email, and set the
HeaderTemplate to be
emailTemplate. You can add children directly to the
TreeViewItems.Items collection. It's a little more work to set up, but you don't have to worry about whether items are in the visual tree when you construct them yourself.
Thanks,
Ted
TreeViewTreeViewItem
This posting is provided "AS IS" with no warranties, and confers no rights.
Thanks for the comment. May I suggest this gets added to the samples/documentation? Is it possible to knock up a code snippet that does this with an HDataTemplate?
One thing - the problem with DataTemplates is that they violate the separation of design and code. All of a sudden I have to fill a not very easy to segment XAML document with a load of stuff that may or may not make sense to a Blend designer. A very unfortunate
side effect is that you now have (effectively) duplicated class declarations, one in a C# file, the other in the XAML. It is, however, an intractable problem - after all you do have to tell the control what to display.
While you are here: what is the best way to add a column header to a multi-column tree view like I have in my screen snaps?
This is a nice tutorial, Jerry. Unfortunately I'm still stumped in the scenario when you only want to load the top level items upfront - you can't see the expand-o button unless there is a child node, so there is a classic chicken-and-egg problem.
Any ideas?
--
Anye Mercy
AnyeDotNet.blogspot.com
Please "Mark as Answer" the posts that help you - this lets others know the problem has been solved and helps others having the same problem know which solution works. Thanks!
Anye - to clarify then the initial situation is that your tree has 3 root items. None of these has any children but you want to populate on demand? Here is one possible solution that assumes you've something similar to my EmailItem class. So:
1. If its a root node then its parent is null - by definition.
2. If nodes are to be populated on demand then the user is going to have to indicate which node is to be loaded and displayed.
3. 2. implies you could use the selectionChanged event on the tree - so we have a couple of extra lines of code:
EmailItem emi = newItem;
// has the item got any children? There are any number of variations on this theme - one could have a loaded flag in the item for example
if (emi.Items.Count == 0)
{
EmailItem ema = new EmailItem("re: " + emi.Title, "1.0", DateTime.Now);
// this correctly sets up the visual state too.
emi.Items.Add(ema);
}
Does that help? I know it means the initial nodes will not display a >
until they are clicked but that is not too horrible to contemplate. You could flag this with textual or color changes.
The alternative is to modify the code and add the ability to set the visual state to 'expandable' even if the collection of child items is empty. I hope that this is one of a number of improvements that will get added to this important control - trees have
become essential elements in most of the apps I write these days.
Thanks, Jerry.. after I posted it occured to me that I could probably fiddle with the TreeView source to make the button appear even if there is no child. I think I will try that.
The reason I didn't want to just use the selectedChanged is that my TreeViewItem templates have either checkboxes or radio buttons depending on where they are being used - so I don't necessarily want them "selected" when expanding :)
Cheers,
Anye
--
Anye Mercy
AnyeDotNet.blogspot.com
Please "Mark as Answer" the posts that help you - this lets others know the problem has been solved and helps others having the same problem know which solution works. Thanks!
Another option would be to Load the top level of the Hierarchy up front and then load the data needed to expand each of the top level nodes asynchrounoulsly in the background. This way once the backgound task to get the data for a given node has completed
and updated the ObservableCollection for that node to have children, the ui would automatically display the Expand-o button.
If you have only a few top-level nodes with just one level of nested nodes this may be useful. You show the user the top level nodes quickly, and as the data is retrived for each of thos nodes the Expander button appears.
Yes, I have. The top level nodes display once the control is loaded, and then the 'expand-o' images start to appear one ate a time on each top level node.
I'm calling a WCF service to fill my top level. nodes. When I get the results from that call I'm looping through the results adding nodes to my ObservableCollection and also -- for each node-- calling another service to get the nodes children.
Not sure if this is the right way to do this, but it looks really cool watching the expand-o icons light up one after another going down the page :-}
Je8
Member
54 Points
50 Posts
Re: Toolkit treeview questions
Nov 01, 2008 06:23 PM | LINK
OK - here is a programmatic solution to the problem. I hope this might save someone else 3 or 4 days of experimentation.
1. The first thing is to set up a class to represent the data that is going to be viewed - the fact that this control is called a TreeView is, I think, no accident. It is quite different to a Win32/WinForms control. As my original question concerned email let's stick with that. Here is the class that we'll use.
// this will be displayed in the Silverlight TreeView
public class EmailItem
{
// constructor takes various arguments as one might expect
public EmailItem(string Title, string From,DateTime Date)
{
// by default a root item so no parent
this.Parent = null;
// the observable collection is very important here, it means that an instance of this class
// will get notified if the collection changes,
// as well as the container getting notified of changes - see the Add() code later.
this.Items = new ObservableCollection<EmailItem>();
// this is for sanity checking and general proof of understanding
this.Items.CollectionChanged +=
new NotifyCollectionChangedEventHandler(Items_CollectionChanged);
// assign the constructor parameters
this.Title = Title;
this.From = From;
this.Date = Date;
}
// the collection has changed - just spew a debug message for the moment
void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (sender != null)
{
Debug.WriteLine("Items_CollectionChanged: " + sender.ToString() + "(" + e.Action.ToString() + ")");
}
}
// our email properties.
public string Title { get; set; }
public string From { get; set; }
public DateTime Date { get; set; }
public EmailItem Parent { get; set;} // the parent emailItem - enables us to work back up through the hierarchy
// this is special - this collection will contain all of the
// emails which are children in the hierachy. Imagine them to be
// the collected replies to a specific email. The depth of the
// hierarchy is bounded only by systems limits.
public ObservableCollection<EmailItem> Items { get; set; }
}
2. Now we need to tell the tree view what is it going to display. The XAML here totallly dictates the appearance of the item in the view. This XAML snippet places a tree view on the layout and determines the content. Somewhere you will need to have added a namespace to the XAML referencing the Silverlight toolkit (SLTK)
xmlns:controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls"
So any SLTK controls are prefixed with a controls: namespace qualifier
<controls:TreeView Margin="8,80,102,78" x:Name="trvEmails" SelectedItemChanged="ctlTree_SelectedItemChanged">
<controls:TreeView.ItemTemplate>
<controls:HierarchicalDataTemplate x:Name="EmailItemTemplate" ItemsSource="{Binding Items}">
<StackPanel Orientation="Horizontal">
<controls:Label Width="80" Content="{Binding Title}"/>
<controls:Label Width="80" Content="{Binding From}"/>
<controls:Label Foreground="Gray" Content="{Binding Date}"/>
</StackPanel>
</controls:HierarchicalDataTemplate>
</controls:TreeView.ItemTemplate>
In English then, we've got a TreeView called trvEmails with an event handler called ctlTree_SelectedItemChanged that is invoked when the selection changes in the tree.
Next comes the clever/tricky bit - the ItemTemplate. This contains a HierarchicalDataTemplate declaration which has as it source binding a property called Items. This is bound directly to the Items property in the EmailItem class. Importantly Items is not directly viewable - the bits of the email message we wish to display is specified by the StackPanel. Note I've chosen to orientate this horizontally - this means we get a columnar layout for the EmailItem with the content described by the 3 labels that follow. Note that each label is bound to a separate property of the EmailItem class - in turn Title, From and Date. Just to show we are in control, the date is drawn as greyed text.
That, as they say, is that. Let's move on to the code.
3. Now if you have added the C# code and the XAML markup to a project, the tree, at runtime, will be empty. Here's some code to populate the tree:
// I've added this code to the page constructor
public Page()
{
InitializeComponent();
// add the first new email item to the root collection
EmailItem emi = new EmailItem("Email 1","Root", DateTime.Now);
trvEmails.Items.Add(emi);
// add a 'reply' to the first email
emi.Items.Add(new EmailItem("Email 1.1", "abc", DateTime.Now));
// add another root item
emi = new EmailItem("Email 2", "Root", DateTime.Now);
// insert it into the view collection
trvEmails.Items.Add(emi);
// and add a reply to the second item
emi.Items.Add(new EmailItem("Email 2.1", "abc",DateTime.Now));
// finally a third root item
trvEmails.Items.Add(new EmailItem("Email 3", "def", DateTime.Now));
}
This should be pretty self explanatory - what you get then is a tree that initially looks like this - note the arrows indicating that the item has children. Magic!
and here is the tree with the items expanded:
4. Neat. Let's code up the event handler that is called when the tree selection changes:
private void ctlTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
Debug.WriteLine("ctlTree_SelectedItemChanged");
EmailItem oldItem = e.OldValue as EmailItem;
EmailItem newItem = e.NewValue as EmailItem;
// _selectedEmailItem is declared within the class as:
// private EmailItem _selectedEmailItem;
_selectedEmailItem = newItem;
// see next paragraph
btnAdd.IsEnabled = (_selectedEmailItem != null);
if (oldItem != null)
{
Debug.WriteLine("Old: " + oldItem.Title);
}
if (newItem != null)
{
EmailItem emi = newItem;
while (emi != null)
{
Debug.WriteLine("Walking back to root: " + emi.Title);
emi = emi.Parent;
}
}
}
This handler updates an EmailItem instance (_selectedEmailItem) that is declared within the page code. This makes life a bit simpler in a moment,as I hope to show. Once we are sure we've updated the _selectedEmailItem we do a little bit more work to ensure we've actually tamed the beast and it is working as we expect. Caveat: I'm not sure this is 100% neccessary. There may be a more generic navigation mechanism built into the TreeViewItem but I've not found it yet. What we do when the selection changes is to walk back up the tree until the item under examination has a null parent - at each stage we print out the email title.
5. Now, in the case of the items we have inserted so far, we have not set the Parent property. I've skipped over it entirely This might be useful if you want to refer back to the email in your reply for example. Assume a button called btnAdd has been added to the XAML and an suitable handler added to the C# code. Here's what the handler will do:
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
// invariant - cannot be null if button enabled
if (_selectedEmailItem != null)
{
// this new email is from jerry
EmailItem emi = new EmailItem("re:" + _selectedEmailItem.Title,"Jerry", DateTime.Now);
// and its parent is the current selection
emi.Parent = _selectedEmailItem;
// add the new child to the collection in the parent
_selectedEmailItem.Items.Add(emi);
}
}
This should be adequately commented - we add a new item to the tree as a child of the current selection. What is pretty clever is that at no point is the TreeView or any TreeViewItems involved. But, despite this absence of an explicit link, the action of adding the email will ensure the view is updated, with the modified item now displaying the 'i've got child items' arrow. See green callouts.
6. Finally. That covers the essentials and answers my initial question. Hope you find this useful. Please feel free to comment/query/criticise as you see fit. I'd appreciate it if the latter was constructive in nature!
Jerry
tree view c# cod insert runtime
Ted Glaza [M...
Member
50 Points
10 Posts
Re: Toolkit treeview questions
Nov 04, 2008 06:21 AM | LINK
Hi Jerry,
Unfortunately in Silverlight there are some things you can only do in XAML and not code. You can't currently create DataTemplates (or HierarchicalDataTemplates, etc.) in code (except for using XamlReader.Load on a string of XAML).
You can still do most of your work in code if you prefer though. The trick is to create a regular DataTemplate that defines how you like your items rendered and stick it in the Resources of a named control. Then you can look it up with DataTemplate emailTemplate = myControl.Resources["MyTemplate"] as DataTemplate;. Just create all your TreeViewItems, set the Header to be your email, and set the HeaderTemplate to be emailTemplate. You can add children directly to the TreeViewItems.Items collection. It's a little more work to set up, but you don't have to worry about whether items are in the visual tree when you construct them yourself.
Thanks,
Ted
TreeView TreeViewItem
Je8
Member
54 Points
50 Posts
Re: Toolkit treeview questions
Nov 04, 2008 12:11 PM | LINK
Hello Ted,
Thanks for the comment. May I suggest this gets added to the samples/documentation? Is it possible to knock up a code snippet that does this with an HDataTemplate?
One thing - the problem with DataTemplates is that they violate the separation of design and code. All of a sudden I have to fill a not very easy to segment XAML document with a load of stuff that may or may not make sense to a Blend designer. A very unfortunate side effect is that you now have (effectively) duplicated class declarations, one in a C# file, the other in the XAML. It is, however, an intractable problem - after all you do have to tell the control what to display.
While you are here: what is the best way to add a column header to a multi-column tree view like I have in my screen snaps?
ATB
Jerry
treeview header
anyeone
Participant
826 Points
199 Posts
Re: Re: Toolkit treeview questions
Nov 04, 2008 02:49 PM | LINK
This is a nice tutorial, Jerry. Unfortunately I'm still stumped in the scenario when you only want to load the top level items upfront - you can't see the expand-o button unless there is a child node, so there is a classic chicken-and-egg problem.
Any ideas?
Anye Mercy
AnyeDotNet.blogspot.com
Please "Mark as Answer" the posts that help you - this lets others know the problem has been solved and helps others having the same problem know which solution works. Thanks!
Je8
Member
54 Points
50 Posts
Re: Re: Toolkit treeview questions
Nov 04, 2008 03:30 PM | LINK
Anye - to clarify then the initial situation is that your tree has 3 root items. None of these has any children but you want to populate on demand? Here is one possible solution that assumes you've something similar to my EmailItem class. So:
1. If its a root node then its parent is null - by definition.
2. If nodes are to be populated on demand then the user is going to have to indicate which node is to be loaded and displayed.
3. 2. implies you could use the selectionChanged event on the tree - so we have a couple of extra lines of code:
EmailItem emi = newItem;
// has the item got any children? There are any number of variations on this theme - one could have a loaded flag in the item for example
if (emi.Items.Count == 0)
{
EmailItem ema = new EmailItem("re: " + emi.Title, "1.0", DateTime.Now);
// this correctly sets up the visual state too.
emi.Items.Add(ema);
}
Does that help? I know it means the initial nodes will not display a > until they are clicked but that is not too horrible to contemplate. You could flag this with textual or color changes.
The alternative is to modify the code and add the ability to set the visual state to 'expandable' even if the collection of child items is empty. I hope that this is one of a number of improvements that will get added to this important control - trees have become essential elements in most of the apps I write these days.
HTH + ATB
Jerry
anyeone
Participant
826 Points
199 Posts
Re: Re: Re: Toolkit treeview questions
Nov 04, 2008 04:06 PM | LINK
Thanks, Jerry.. after I posted it occured to me that I could probably fiddle with the TreeView source to make the button appear even if there is no child. I think I will try that.
The reason I didn't want to just use the selectedChanged is that my TreeViewItem templates have either checkboxes or radio buttons depending on where they are being used - so I don't necessarily want them "selected" when expanding :)
Cheers,
Anye
Anye Mercy
AnyeDotNet.blogspot.com
Please "Mark as Answer" the posts that help you - this lets others know the problem has been solved and helps others having the same problem know which solution works. Thanks!
Je8
Member
54 Points
50 Posts
Re: Re: Re: Toolkit treeview questions
Nov 04, 2008 04:13 PM | LINK
Anye, maybe it is really easy to set the right visual state. Is not all this stuff in the XAML template?
russgove
Member
60 Points
23 Posts
Re: Re: Re: Toolkit treeview questions
Nov 05, 2008 04:41 PM | LINK
Another option would be to Load the top level of the Hierarchy up front and then load the data needed to expand each of the top level nodes asynchrounoulsly in the background. This way once the backgound task to get the data for a given node has completed and updated the ObservableCollection for that node to have children, the ui would automatically display the Expand-o button.
If you have only a few top-level nodes with just one level of nested nodes this may be useful. You show the user the top level nodes quickly, and as the data is retrived for each of thos nodes the Expander button appears.
Je8
Member
54 Points
50 Posts
Re: Re: Re: Toolkit treeview questions
Nov 05, 2008 08:17 PM | LINK
Hello Russ, interesting idea - have you tried this?
russgove
Member
60 Points
23 Posts
Re: Re: Re: Toolkit treeview questions
Nov 06, 2008 01:59 AM | LINK
Yes, I have. The top level nodes display once the control is loaded, and then the 'expand-o' images start to appear one ate a time on each top level node.
I'm calling a WCF service to fill my top level. nodes. When I get the results from that call I'm looping through the results adding nodes to my ObservableCollection and also -- for each node-- calling another service to get the nodes children.
Not sure if this is the right way to do this, but it looks really cool watching the expand-o icons light up one after another going down the page :-}
TreeView