By Xiaodong Liang
In the last post, we have seen how to create an application of WPF with .NET control. As mentioned, WPF could be looked as the successor of Win Form. But it is much more powerful and flexible. One of its unique features is Data Binding. WPF has a built-in set of data services to enable application developers to bind and manipulate data within applications. If you have some codes of Win Form to build the hierarchy tree of Navisworks model like the Selection Tree in UI, you must have to iterate each items and build the nodes & child nodes recursively. While with WPF, the job is much easier.
The whole project is available at Download ADN-NwWPFTree
Firstly, add a tree view control to the form. You could either drag it from toolbox, or add the elements directly in the xaml. I adjusted the width of Navisworks view control to make the tree control wider.
<Window x:Class="NwWPFControlApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
xmlns:NwApi="clr-namespace:Autodesk.Navisworks.Api;assembly=Autodesk.Navisworks.Api"
xmlns:NwControls="clr-namespace:Autodesk.Navisworks.Api.Controls;assembly=Autodesk.Navisworks.Controls"
xmlns:my="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration" Closed="Window_Closed">
<DockPanel>
<Button Content="Open File" Height="23" Name="button1" Width="75" Click="button1_Click" />
<WindowsFormsHost Width="198">
<!--x:Uid used in XAML context-->
<!--x:Name governed by the XAML namescope-->
<NwControls:ViewControl x:Uid="viewControl"
x:Name="viewControl"
Dock="Fill" />
</WindowsFormsHost>
<TreeView Height="308" Name="treeView1" Width="223">
</TreeView>
</DockPanel>
</Window>
Next, we need to tell the tree view which source to bind. I am not a WPF expert, I’d appreciate many posts in internet which guides me how to get started with. I’d recommend this article below. It helped me to know the basic elements I need for building the tree.
http://blogs.msdn.com/b/mikehillberg/archive/2009/10/30/treeview-and-hierarchicaldatatemplate-step-by-step.aspx
In short, the key steps with tree binding are:
1. tell the tree view control which data source you want to bind. In general, this is the top of the hierarchy. In our case, it is the root item of the tree. e.g.
Document oDoc = documentControl.Document;
treeView1.DataContext = oDoc.Models[0].RootItem;
2. bind the data source to the tree view.
<!--tell the tree the collection of the hierarchy. in our case it is RootItem.Children-->
<TreeView Height="308" Name="treeView1" Width="223" ItemsSource="{Binding Path=Children}">
<!--define the item (tree node) template, i.e. each ModelItem corresponds to a node-->
<TreeView.ItemTemplate >
<!--in order to have a hierarchical tree, it also requires to bind ModeItem.Children. Thus
WPF knows to iterate the child node-->
<HierarchicalDataTemplate ItemsSource="{Binding Path=Children }">
<!--TextBlock defines the final text displayed with the tree node. e.g. we tell WPF to display
ModelItem.DsiplayName-->
<TextBlock Foreground="Red" Text="{Binding Path=DisplayName}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
The tree is built.But you may quickly find some nodes have no name! What’s up? Actually, ModelItem.DisplayName is the name displayed for the end user. It is supplied by the original CAD designer. e.g. in this model which is from a DWG file, the layer has name, the block definition has name, however, insert or the end geometries have no name. While I guess Navisworks organized the names when it is loading the CAD file. So it looks well in its Selection Tree. ModelItem.ClassName or ModelItem.ClassDisplayName are not empty in default. But they are not meaningful to the end user. I do not suggest to bind them to the tree.
Well, does this mean we have no way to give the desired name by binding? No, WPF is much flexible. It allows you with conditional statements. again, the tutorial on internet tells more:
http://stackoverflow.com/questions/11395349/writing-conditional-statements-in-xaml-code
I chose the way IValueConverter. I do not want to spend time to explain the theory of IValueConverter. Please refer to MSDN for details. In our case, we need firstly create our own conversion class. e.g.
// conversion class for tree node
public class NwTreeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// since we bind DisplayName to the node, the value is a string
//<TextBlock Foreground="Red" Text="{Binding Path=DisplayName}" />
if (value == "")
// if it is empty, return an arbitrary string
return "dummy";
else
// if it is not empty, return it
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// we do not need to convert back at this moment
throw new NotImplementedException();
}
}
Some more lines in the xmal are required, in order to tell the tree view control to use the class.
<Window x:Class="NwWPFControlApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
xmlns:NwApi="clr-namespace:Autodesk.Navisworks.Api;assembly=Autodesk.Navisworks.Api"
xmlns:NwControls="clr-namespace:Autodesk.Navisworks.Api.Controls;assembly=Autodesk.Navisworks.Controls"
xmlns:my="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration" Closed="Window_Closed"
xmlns:src="clr-namespace: NwWPFControlApp">
<!--import the conversion class-->
<Control.Resources>
<src:NwTreeConverter x:Key="NwTreeConverter"/>
</Control.Resources>
<DockPanel>
<Button Content="Open File" Height="23" Name="button1" Width="75" Click="button1_Click" />
<WindowsFormsHost Width="198">
<!--x:Uid used in XAML context-->
<!--x:Name governed by the XAML namescope-->
<NwControls:ViewControl x:Uid="viewControl"
x:Name="viewControl"
Dock="Fill" />
</WindowsFormsHost>
<!--tell the tree the collection of the hierarchy. in our case it is RootItem.Children-->
<TreeView Height="308" Name="treeView1" Width="223" ItemsSource="{Binding Path=Children}" SelectedItemChanged="treeView1_SelectedItemChanged">
<!--define the item (tree node) template, i.e. each ModelItem corresponds to a node-->
<TreeView.ItemTemplate >
<!--in order to have a hierarchical tree, it also requires to bind ModeItem.Children. Thus
WPF knows to iterate the child node-->
<HierarchicalDataTemplate ItemsSource="{Binding Path=Children }">
<!--TextBlock defines the final text displayed with the tree node. e.g. we tell WPF to display
ModelItem.DsiplayName-->
<!--tell the textbox to use the string returned from the conversion class-->
<TextBlock Foreground="Red" Text="{Binding Path=PropertyCategories, Converter={StaticResource NwTreeConverter}}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</DockPanel>
</Window>
Now, if we open the model, the tree looks like as below.
But it is not elegant to hard-code the name of the node with “dummy”. I have been racking my brain trying the way to provide more meaningful name. Finally, I can only think of to bind the ModelItem.PropertyCategories thus we could get any information in the properties and use them as the node name. So the conversion class is tuned to:
// conversion class for tree node
public class NwTreeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// <TextBlock Foreground="Red" Text="{Binding Path=PropertyCategories,
// Converter={StaticResource NwTreeConverter}}" />
// the value is PropertyCategoryCollection
PropertyCategoryCollection oColl = value as PropertyCategoryCollection;
if (oColl == null)
{
// can be null? that must be a terrible error!
return "error!";
}
else
{
// try to check if there is name with the item
DataProperty oDP = oColl.FindPropertyByDisplayName("Item", "Name");
if (oDP != null)
{
//use "name" as node name
return oDP.Value.ToDisplayString();
}
else
{
// no "name", then check "type"
oDP = oColl.FindPropertyByDisplayName("Item", "Type");
if (oDP != null)
{
//use "type" as node name
return oDP.Value.ToDisplayString();
}
else
{
// can be null? that must be a terrible error!
return "Error!";
}
}
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
And tell the xmal to bind PropertyCategories to the text of tree item.
<TextBlock Foreground="Red" Text="{Binding Path=PropertyCategories, Converter={StaticResource NwTreeConverter}}" />
Now our tree view is:
looks not bad, isn’t it? A little pity is I did not find a workaround to provide more meaningful name for the Insert node like UI does. But at least, it tells the end user what the node type is.
OK, I’d say the post is completed….wait! you must be wondering how the tree item can connect to the real model item since they are bound. yeah! You are correct! see below :-)
private void treeView1_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
ModelItem oCurrentNode= (ModelItem)e.NewValue;
if (oCurrentNode != null)
{
ModelItemCollection oMC = new ModelItemCollection();
oMC.Add(oCurrentNode);
Document oDoc = documentControl.Document;
oDoc.CurrentSelection.CopyFrom(oMC);
}
}
I am thinking the next topic about WPF. If you have any suggestion, please share with me.