Introduction
I was developing a project in Silverlight in which there was a requirement to populate auto-complete data as a treeview as they were hierarchical data.
The data was like Country / State / City / Area... and user could select country or state or city or area. In short, data was hierarchical and
the user could select data from any level from the treeview. It should work like an auto-complete control which is readily available in Silverlight 3.0.
First, I tried to identify any such code available on the net, but with no luck, and then I divided the problem and tried to solve it and get the output which I wanted.
Finally, when I was through, I felt I should share it with you people as it will not only serve as an auto-complete control, but also be useful in a number
of other issues which I faced during the development of this control.
Step 1: Use the ViewModel Concept in Treeview
As our basic requirement is to select a particular record/object from a treeview which the user types, we need to make that object highlighted (selected)
and also expand all the nodes so that the particular object is visible. To make this work, first, you need to use the concept of viewmodel, so we have to bind
the TreeViewItem
's "IsExpanded
" and "IsSelected
" properties with our data class. This we can do by modifying the control template
as the popular method of setter does not work in WPF.
So, inherit TreeView
as well the TreeViewItem
class and override the GetContainerForItemOverride
method.
public class SilverlightTreeView : TreeView
{
protected override DependencyObject GetContainerForItemOverride()
{
SilverlightTreeViewItem tvi = new SilverlightTreeViewItem();
Binding expandedBinding = new Binding("IsExpanded");
expandedBinding.Mode = BindingMode.TwoWay;
tvi.SetBinding(SilverlightTreeViewItem.IsExpandedProperty, expandedBinding);
Binding selectedBinding = new Binding("IsSelected");
selectedBinding.Mode = BindingMode.TwoWay;
tvi.SetBinding(SilverlightTreeViewItem.IsSelectedProperty, selectedBinding);
return tvi;
}
}
Step 2: Create a Hierarchical Data Class
Here, you have to create a data class which contains your data properties as well as the "IsExpanded
" and "IsSelected
" properties.
Also, you need a self referencing ObservableCollection
(or any other collection) property inside your class.
Also implement the INotifyChanged
interface so you can bind two way.
public class HierarchicalCity : INotifyPropertyChanged
private bool isSelected;
public bool IsSelected
{
get { return isSelected; }
set
{
isSelected = value;
OnPropertyChanged("IsSelected");
}
}
private ObservableCollection<hierarchicalcity> _SubClass =
new ObservableCollection<hierarchicalcity>();
public ObservableCollection<hierarchicalcity> SubClass
{
get { return _SubClass; }
set { _SubClass = value; OnPropertyChanged("SubClass"); }
}
Step 3: Select an Item in the TreeView Which is Similar to What the User Types in the Textbox
As soon as the user types in the textbox, we will search the typed text in the treeview in a recursive loop; here, I have used the "StartsWith
" string function to check
the "cityname
" property with the user's data. After identifying the first match, we can stop searching further; I have just set the selected object
in the "selectedTV
" variable.
Now, we also need to expand all the parent nodes so our selected or searched node can be visible; for this, we are using
the "ExpandAllParents
" function. Inside it, I am again recursively looping the treeview and then setting the "ISExpanded
" property to true
;
so due to the binding which we have seen earlier, it will expand the treeview node automatically. Also, set the IsSelected
property
to true
for the selected object.
private void ReturnChildconditionsRecursively(
ObservableCollection<HierarchicalCity> lst, string val)
{
for (int j = 0; j < lst.Count; j++)
{
if (lst[j].CityName.StartsWith(val,
StringComparison.OrdinalIgnoreCase))
{
selectedTV = lst[j];
ExpandAllParents(lst[j]);
break;
}
else if (lst[j].SubClass != null)
{
ReturnChildconditionsRecursively(lst[j].SubClass,val);
}
}
}
private bool ApplyActionToSuperclasses(HierarchicalCity itemToLookFor,
Action<HierarchicalCity> itemAction)
{
if (itemToLookFor == this)
{
return true;
}
else
{
foreach (HierarchicalCity subclass in this.SubClass)
{
bool foundItem = subclass.ApplyActionToSuperclasses(itemToLookFor,
itemAction);
if (foundItem)
{
itemAction(this);
return true;
}
}
return false;
}
}
Step 4: On Press of Key (Down/Up), Move Within the Treeview
To do this, first we have to handle the KeyDown
event of the textbox and then we have to shift our focus from the textbox to the treeview node.
For this, I require an object of the treeview node. If we are selecting an item by typing it in the textbox, it will be available easily, but if we
are moving inside a treeview or directly shifting our focus to the treeview by using the mouse, then we will get the data object in the selected item changed event,
so we need to find the container of the data object, i.e., the treeview node.
Now to do this, I have identified that extension methodz are readily available. I have just used one of them in my current solution, but other
methods are also equally useful in other scenarios.
private static TreeViewItem ContainerFromItem(
ItemContainerGenerator parentItemContainerGenerator,
ItemCollection itemCollection, object item)
{
foreach (object curChildItem in itemCollection)
{
TreeViewItem parentContainer = (
TreeViewItem)parentItemContainerGenerator.ContainerFromItem(curChildItem);
if (parentContainer == null)
return null;
TreeViewItem containerThatMightContainItem = (
TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(item);
if (containerThatMightContainItem != null)
return containerThatMightContainItem;
TreeViewItem recursionResult = ContainerFromItem(
parentContainer.ItemContainerGenerator, parentContainer.Items, item);
if (recursionResult != null)
return recursionResult;
}
return null;
}
Step 5: Close Treeview After Selecting Item or on Press of Esc Key
Similar to the function ExpandAllParents
, I have a function "CollapseAll
" which I use to collapse an entire treeview.
Also, I have used a popup control which is available in Silverlight 3.0.
private void ApplyActionToAllItems()
{
Stack<HierarchicalCity> dataItemStack = new Stack<HierarchicalCity>();
dataItemStack.Push(this);
while (dataItemStack.Count != 0)
{
HierarchicalCity currentItem = dataItemStack.Pop();
ActionCollapse(currentItem);
foreach (HierarchicalCity childItem in currentItem.SubClass)
{
dataItemStack.Push(childItem);
}
}
}
Step 6: Automatically Close Treeview if Focus Shifted to Any Other Control
If the user directly shifts focus from the mouse or by using tab to any other control available in the UI, it is required to close the treeview. For this,
I have used a FocusManager
and checked the next control which is going to get the focus. If it is not the treeview or our textbox and if the popup control is open, then hide it.
DependencyObject dp = ((
DependencyObject)System.Windows.Input.FocusManager.GetFocusedElement());
if ((DateTime.Now.Ticks - LastTicks) < 2310000)
{
//Code here
}
Features
- Supports hierarchical data.
- User can select data from any level from a treeview.
- On typing in the textbox, the treeview will populate and move to the first record starting from the same word.
- Treeview will automatically close if the user typed data is not available in
the treeview data.
- City will be selected: on pressing the Enter key in the textbox, or on double click inside the treeview, or on pressing the Enter key in the treeview.
- On pressing the Esc Key, we can move from the treeview to the textbox, same for pressing Enter key in the treeview.
- On Key down or Key up in the textbox, we will move to the treeview.
- If users move to any other control other than the treeview or textbox, the treeview will automatically close itself.
Conclusion
I hope this will help you people having similar requirements. I have tried to make this program as
a one stop solution for several difficulties
I have faced during this development. The code I am submitting
is not well commented, and also can be improved at certain places, so forgive me for that.
Any suggestion /critics/ feedback are highly appreciated.