I've build a function which generates a treeview in winforms. It includes subfolders and files with recursion. Now i want to translate this over to wpf.
I'm having trouble figuring out how to handle the classes. I know i have to make my own custom class 'treenode' which would have a property 'parent' similar to the winforms treenode.
However in wpf I need two different types of treenodes so i can properly bind the wpf by data type. I have a working example in wpf using familys, I'm just not sure how to get my winform version translated to wpf. Can someone help me get my winform version working in wpf?
Then end goal is getting my treeview in WPF to populate using a directories and files as seen in my winforms example. However the styling of the WPF version should maintain the 'icons' displaying for files and folders.
i hope someone can help me get this working properly. Any suggestions and comments are welcome.
ViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Linq;
namespace WpfApplication1
{
public class ViewModel : ObservableObject
{
// Properties
private ObservableCollection<DirectoryNode> directoryNodes;
public ObservableCollection<DirectoryNode> DirectoryNodes
{
get { return directoryNodes; }
set
{
directoryNodes = value;
NotifyPropertyChanged("DirectoryNodes");
}
}
private ObservableCollection<string> formats;
public ObservableCollection<string> Formats
{
get { return formats; }
set
{
formats = value;
NotifyPropertyChanged("Formats");
}
}
private ObservableCollection<string> directories;
public ObservableCollection<string> Directories
{
get { return directories; }
set
{
directories = value;
NotifyPropertyChanged("Directories");
}
}
// Creating data for testings
public ViewModel()
{
Formats = new ObservableCollection<string>();
Directories = new ObservableCollection<string>();
DirectoryNodes = new ObservableCollection<DirectoryNode>();
// create some dummy test data, eventually will be push to GUI
Formats.Add(".txt");
Formats.Add(".png");
Directories.Add(System.Environment.GetEnvironmentVariable("USERPROFILE"));
PopulateTree(Directories);
}
// Functions
static bool IsValidFileFormat(string filename, ObservableCollection<string> formats)
{
if (formats.Count == 0) return true;
string ext = Path.GetExtension(filename);
bool results = formats.Any(fileType => fileType.Equals(ext, StringComparison.OrdinalIgnoreCase));
return results;
}
public static DirectoryNode CreateDirectoryNode(DirectoryInfo directoryInfo)
{
DirectoryNode directoryNode = new DirectoryNode(){Filename=directoryInfo.Name};
foreach (var directory in directoryInfo.GetDirectories())
{
try
{
directoryNode.Children.Add(CreateDirectoryNode(directory));
}
catch (UnauthorizedAccessException) { }
}
foreach (var file in directoryInfo.GetFiles())
{
if (IsValidFileFormat(file.FullName, Formats))
{
FileNode node = new FileNode() { Filename = file.FullName };
directoryNode.Children.Add(node);
}
}
return directoryNode;
}
public void PopulateTree(ObservableCollection<string> directories)
{
foreach (string directoryPath in directories)
{
if (Directory.Exists(directoryPath))
{
DirectoryInfo directoryInfo = new DirectoryInfo(directoryPath);
DirectoryNodes.Add(CreateDirectoryNode(directoryInfo));
}
}
}
}
public class FileNode
{
public string Filepath { get; set; }
public string Filename { get; set; }
public DirectoryNode Parent { get; set; }
}
public class DirectoryNode
{
public string Filepath { get; set; }
public string Filename { get; set; }
public DirectoryNode Parent { get; set; }
public ObservableCollection<FileNode> Children { get; set; }
}
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
MainWindow.Xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:self="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="300"
WindowStartupLocation="CenterScreen">
<Window.DataContext>
<self:ViewModel/>
</Window.DataContext>
<Grid Margin="5">
<TreeView ItemsSource="{Binding Directories}" Grid.Row="1" Grid.ColumnSpan="2">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type self:DirectoryNode}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Label VerticalAlignment="Center" FontFamily="WingDings" Content="1"/>
<TextBlock Text="{Binding Filename}" />
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type self:FileNode}">
<StackPanel Orientation="Horizontal">
<Label VerticalAlignment="Center" FontFamily="WingDings" Content="2"/>
<TextBlock Text="{Binding Filename}" />
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
Working Winforms Example
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Linq;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public static List<string> formats = new List<string>();
public Form1()
{
InitializeComponent();
//add userfolder
List<string> Directories = new List<string>();
Directories.Add(System.Environment.GetEnvironmentVariable("USERPROFILE"));
// get formats accepted
formats.Add(".txt");
formats.Add(".png");
PopulateTree(Directories, formats);
}
static bool IsValidFileFormat(string filename, List<string> formats)
{
if (formats.Count == 0) return true;
string ext = Path.GetExtension(filename);
bool results = formats.Any(fileType => fileType.Equals(ext, StringComparison.OrdinalIgnoreCase));
return results;
}
public static TreeNode CreateDirectoryNode(DirectoryInfo directoryInfo)
{
TreeNode directoryNode = new TreeNode(directoryInfo.Name);
foreach (var directory in directoryInfo.GetDirectories())
{
try
{
directoryNode.Nodes.Add(CreateDirectoryNode(directory));
}
catch (UnauthorizedAccessException) { }
}
foreach (var file in directoryInfo.GetFiles())
{
if (IsValidFileFormat(file.FullName, formats))
{
TreeNode node = new TreeNode(file.FullName);
node.ForeColor = Color.Red;
directoryNode.Nodes.Add(node);
}
}
return directoryNode;
}
public void PopulateTree(List<string> directories, List<string> formats)
{
// main collection of nodes which are used to populate treeview
List<TreeNode> treeNodes = new List<TreeNode>();
foreach (string directoryPath in directories)
{
if (Directory.Exists(directoryPath))
{
DirectoryInfo directoryInfo = new DirectoryInfo(directoryPath);
treeNodes.Add(CreateDirectoryNode(directoryInfo));
}
}
treeView1.Nodes.AddRange(treeNodes.ToArray());
}
}
}
Looking at your example there, I'm not sure exactly what is happening. You can take a look in your output and see if the issue stems from bindings not being found at runtime.
I would recommend however that you split the logic up a bit more, moving some of this into your model. I would also recommend that you hide your models behind an interface. This allows your view model to hold a single collection, while the view renders the contents of that collection based on the type. Your current implementation is limited to only showing files, as children to a directory, instead of directories and files. Below is a working example for you.
The models
INode
Creating an
INode
interface will allow you to create different implementations of each content item you want to render to the Treeview.Our
INode
only needs two properties. One that represents the name of the node (typically the folder or file name) and one that represents the full path to the folder or file it represents.DirectoryNode
This is the root node for all of our nodes. In most cases, all other nodes are going to be associated with a
DirectoryNode
via a parent-child relationship. TheDirectoryNode
will be responsible for building its own collection of children nodes. This moves the logic into the model, where it can validate itself and create EmptyFolderNodes or generate a collection of FileNodes as needed. This cleans up the view model a bit, so that all it needs to do is facilitate the interactions with the view itself.The
DirectoryNode
will implementINotifyPropertyChange
so that we can raise property changed events to anything that databinds to us. This will only by theChildren
property on this model. The rest of the properties will be read-only.Few things to note here. One is that the model will always be given a reference to the
DirectoryInfo
it is representing as a node. Next, it can optionally be given a parentDirectoryNode
. This lets us easily support forward navigation (via theChildren
property) and backward navigation (via theParent
property) in our model. When we convert the collection of childrenDirectoryInfo
items into a collection ofDirectoryNode
items, we pass ourself into each childrenDirectoryNode
so it has access to its parent if needed.The
Children
collection is a collection ofINode
models. This means theDirectoryNode
can hold various different kinds of nodes and can easily be extended to support more. You just need to update theBuildChildrenNodes
method.EmptyFolderNode
The easiest nodel we will implement is an empty folder node. If you double click on a folder, and there isn't any contents, we will display a node to the user letting them know it's empty. This node will have a predefined
Name
, and will always belong to a parent directory.There isn't much going on here, we assign the name as "Empty" and default our path to the parent.
FileNode
The last model we need to build is the
FileNode
. This node represents a file in our hierarchy and requires aDirectoryNode
be given to it. It also requires theFileInfo
that this node represents.The contents of this model at this point should be pretty self-explanatory, so i won't spend any time on it.
The view model
Now that we have our models defined, we can set up the view model to interact with them. The view model needs to implement two interfaces. The first being
INotifyPropertyChanged
so that we can fire property changed notifications to the view. The second isICommand
so that the view can tell the view model when more directories or files need to be loaded. I recommend abstracting theICommand
stuff out into an individual class that can be reused, or using an existing library likePrism
orMVVMLight
, both of which have commanding objects you can use.The view model has a collection of
RootNodes
. This is the initial collection ofINode
instances the view will bind to. This initial collection will contain all of the files and folders within the Program Files directory.When the user double clicks on a
TreeViewItem
in the view, theExecute
method will fire off. This method will either clear the children collection of the selected directory, or build the collection of children. This way, as the user collapses folders in the view, we clean up after ourselves and empty the collection. This also means that the collection will always be refreshed as they open/close the directories.The View
The is the most complex item, but it's fairly simple once you look at it. Just like your example, there are templates for each node type. In our case, the Treeview is databound to our view models
INode
collection. We then havea template for each implementation of theINode
interface.The XAML code is documented to explain what's happening, so i'll not add to that.
The end result looks like this:
This should get you what you want. Let me know if it doesn't. If all you want is a single Directory->File relationship, then you can just update the
BuildChildrenNodes()
method to skip the Directory lookup when building it'sChildren
collection.One last thing to show is the flexibility you now have in the view. Since the
FileNode
contains its parentDirectoryNode
and theFileInfo
it represents, you can use data triggers to conditionally change how you display content in the view. Below, I show you two data-triggers on theFileNode
data template. One that turns the TextBlock to red if the file extension is .dll, and another that turns the TextBlock blue if the extension is .exe.The end result looks like this:
You can also do conditional logic within the
Execute
method to handle each different type of file differently. If theExecute
method is invoked, and the file extension is .exe, instead of ignoring the file like we are now, you could start the executable. You have a lot of flexibility at this point.