getting winform treeview into wpf treeview

后端 未结 1 1840
说谎
说谎 2021-02-11 09:16

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 t

相关标签:
1条回答
  • 2021-02-11 09:48

    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.

    namespace DirectoryTree
    {
        public interface INode
        {
            string Name { get; }
            string Path { get; }
        }
    }
    

    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. The DirectoryNode 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 implement INotifyPropertyChange so that we can raise property changed events to anything that databinds to us. This will only by the Children property on this model. The rest of the properties will be read-only.

    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.IO;
    using System.Linq;
    using System.Runtime.CompilerServices;
    
    namespace DirectoryTree
    {
        public class DirectoryNode : INode, INotifyPropertyChanged
        {
            private ObservableCollection<INode> children;
    
            public DirectoryNode(DirectoryInfo directoryInfo)
            {
                this.Directory = directoryInfo;
                this.Children = new ObservableCollection<INode>();
            }
    
            public DirectoryNode(DirectoryInfo directoryInfo, DirectoryNode parent) : this(directoryInfo)
            {
                this.Parent = parent;
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            /// <summary>
            /// Gets the name of the folder associated with this node.
            /// </summary>
            public string Name
            {
                get
                {
                    return this.Directory == null ? string.Empty : this.Directory.Name;
                }
            }
    
            /// <summary>
            /// Gets the path to the directory associated with this node.
            /// </summary>
            public string Path
            {
                get
                {
                    return this.Directory == null ? string.Empty : this.Directory.FullName;
                }
            }
    
            /// <summary>
            /// Gets the parent directory for this node.
            /// </summary>
            public DirectoryNode Parent { get; private set; }
    
            /// <summary>
            /// Gets the directory that this node represents.
            /// </summary>
            public DirectoryInfo Directory { get; private set; }
    
            /// <summary>
            /// Gets or sets the children nodes that this directory node can have.
            /// </summary>
            public ObservableCollection<INode> Children
            {
                get
                {
                    return this.children;
                }
    
                set
                {
                    this.children = value;
                    this.OnPropertyChanged();
                }
            }
    
            /// <summary>
            /// Scans the current directory and creates a new collection of children nodes.
            /// The Children nodes collection can be filled with EmptyFolderNode, FileNode or DirectoryNode instances.
            /// The Children collection will always have at least 1 element within it.
            /// </summary>
            public void BuildChildrenNodes()
            {
                // Get all of the folders and files in our current directory.
                FileInfo[] filesInDirectory = this.Directory.GetFiles();
                DirectoryInfo[] directoriesWithinDirectory = this.Directory.GetDirectories();
    
                // Convert the folders and files into Directory and File nodes and add them to a temporary collection.
                var childrenNodes = new List<INode>();
                childrenNodes.AddRange(directoriesWithinDirectory.Select(dir => new DirectoryNode(dir, this)));
                childrenNodes.AddRange(filesInDirectory.Select(file => new FileNode(this, file)));
    
                if (childrenNodes.Count == 0)
                {
                    // If there are no children directories or files, we setup the Children collection to hold
                    // a single node that represents an empty directory.
                    this.Children = new ObservableCollection<INode>(new List<INode> { new EmptyFolderNode(this) });
                }
                else
                {
                    // We fill our Children collection with the folder and file nodes we previously created above.
                    this.Children = new ObservableCollection<INode>(childrenNodes);
                }
            }
    
            private void OnPropertyChanged([CallerMemberName] string propertyName = "")
            {
                var handler = this.PropertyChanged;
                if (handler == null)
                {
                    return;
                }
    
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
    

    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 parent DirectoryNode. This lets us easily support forward navigation (via the Children property) and backward navigation (via the Parent property) in our model. When we convert the collection of children DirectoryInfo items into a collection of DirectoryNode items, we pass ourself into each children DirectoryNode so it has access to its parent if needed.

    The Children collection is a collection of INode models. This means the DirectoryNode can hold various different kinds of nodes and can easily be extended to support more. You just need to update the BuildChildrenNodes 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.

    namespace DirectoryTree
    {
        public class EmptyFolderNode : INode
        {
            public EmptyFolderNode(DirectoryNode parent)
            {
                this.Parent = parent;
                this.Name = "Empty.";
            }
    
            public string Name { get; private set; }
    
            public string Path
            {
                get
                {
                    return this.Parent == null ? string.Empty : this.Parent.Path;
                }
            }
    
            public DirectoryNode Parent { get; private set; }
        }
    }
    

    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 a DirectoryNode be given to it. It also requires the FileInfo that this node represents.

    using System.IO;
    
    namespace DirectoryTree
    {
        public class FileNode : INode
        {
            public FileNode(DirectoryNode parent, FileInfo file)
            {
                this.File = file;
                this.Parent = parent;
            }
    
            /// <summary>
            /// Gets the parent of this node.
            /// </summary>
            public DirectoryNode Parent { get; private set; }
    
            /// <summary>
            /// Gets the file this node represents.
            /// </summary>
            public FileInfo File { get; private set; }
    
            /// <summary>
            /// Gets the filename for the file associated with this node.
            /// </summary>
            public string Name
            {
                get
                {
                    return this.File == null ? string.Empty : this.File.Name;
                }
            }
    
            /// <summary>
            /// Gets the path to the file that this node represents.
            /// </summary>
            public string Path
            {
                get
                {
                    return this.File == null ? string.Empty : this.File.FullName;
                }
            }
        }
    }
    

    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 is ICommand so that the view can tell the view model when more directories or files need to be loaded. I recommend abstracting the ICommand stuff out into an individual class that can be reused, or using an existing library like Prism or MVVMLight, both of which have commanding objects you can use.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.IO;
    using System.Runtime.CompilerServices;
    using System.Windows.Input;
    
    namespace DirectoryTree
    {
        public class MainWindowViewModel : INotifyPropertyChanged, ICommand
        {
            private IEnumerable<INode> rootNodes;
    
            private INode selectedNode;
    
            public MainWindowViewModel()
            {
                // We default the app to the Program Files directory as the root.
                string programFilesPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
    
                // Convert our Program Files path string into a DirectoryInfo, and create our initial DirectoryNode.
                var rootDirectoryInfo = new DirectoryInfo(programFilesPath);
                var rootDirectory = new DirectoryNode(rootDirectoryInfo);
    
                // Tell our root node to build it's children collection.
                rootDirectory.BuildChildrenNodes();
    
                this.RootNodes = rootDirectory.Children;
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
            public event EventHandler CanExecuteChanged;
    
            public IEnumerable<INode> RootNodes
            {
                get
                {
                    return this.rootNodes;
                }
    
                set
                {
                    this.rootNodes = value;
                    this.OnPropertyChanged();
                }
            }
    
            public bool CanExecute(object parameter)
            {
                // Only execute our command if we are given a selected item.
                return parameter != null;
            }
    
            public void Execute(object parameter)
            {
                // Try to cast to a directory node. If it returns null then we are
                // either a FileNode or an EmptyFolderNode. Neither of which we need to react to.
                DirectoryNode currentDirectory = parameter as DirectoryNode;
                if (currentDirectory == null)
                {
                    return;
                }
    
                // If the current directory has children, then the view is collapsing it.
                // In this scenario, we clear the children out so we don't progressively
                // consume system resources and never let go.
                if (currentDirectory.Children.Count > 0)
                {
                    currentDirectory.Children.Clear();
                    return;
                }
    
                // If the current directory does not have children, then we build that collection.
                currentDirectory.BuildChildrenNodes();
            }
    
            private void OnPropertyChanged([CallerMemberName] string propertyName = "")
            {
                var handler = this.PropertyChanged;
                if (handler == null)
                {
                    return;
                }
    
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
    

    The view model has a collection of RootNodes. This is the initial collection of INode 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, the Execute 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 the INode interface.

    <Window x:Class="DirectoryTree.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:DirectoryTree"
            xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
            Title="MainWindow" Height="350" Width="525">
    
        <!-- Assign a view model to the window. -->
        <Window.DataContext>
            <local:MainWindowViewModel />
        </Window.DataContext>
    
        <DockPanel>
            <TreeView x:Name="FileExplorerTreeview" 
                      ItemsSource="{Binding Path=RootNodes}">
    
                <!--    We use an interaction trigger to map the MouseDoubleClick event onto our view model.
                        Since the view model implements ICommand, we can just bind directly to the view model. 
    
                        This requires that you add the System.Windows.Interactivity.dll assembly to your project references.
                        You also must add the i: namespace to your XAML window, as shown above..
                -->
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseDoubleClick">
                        <!--    When the user double clicks on a folder, we will send the selected item into the view models Execute method as a argument.
                                The view model can then react to wether or not it's a DirectoryNode or a FileNode.
                        -->
                        <i:InvokeCommandAction Command="{Binding }" CommandParameter="{Binding ElementName=FileExplorerTreeview, Path=SelectedItem}" />
                    </i:EventTrigger>
                </i:Interaction.Triggers>
    
                <TreeView.Resources>
                    <!--    This template represents a DirectoryNode. This template databinds itself to the Children property on the DirectoryNode
                            so we can have nested folders and files as needed. 
                    -->
                    <HierarchicalDataTemplate DataType="{x:Type local:DirectoryNode}"
                                              ItemsSource="{Binding Path=Children}">
                        <StackPanel Orientation="Horizontal">
                            <Label Content="1"
                                   FontFamily="WingDings"
                                   FontWeight="Black" />
                            <!-- Need to replace w/ an image of a folder -->
                            <TextBlock Text="{Binding Path=Name}" />
                        </StackPanel>
                    </HierarchicalDataTemplate>
    
                    <!-- This template represents a FileNode. Since FileNodes can't have children, we make this a standard, flat, data template. -->
                    <DataTemplate DataType="{x:Type local:FileNode}">
                        <StackPanel Orientation="Horizontal">
                            <Label Content="2"
                                   FontFamily="WingDings"
                                   FontWeight="Black" />
                            <!-- Need to replace w/ an image of a file -->
                            <TextBlock Text="{Binding Path=Path}" />
                        </StackPanel>
                    </DataTemplate>
    
                    <!-- This template represents an EmptyFolderNode. Since EmptyFolderNode can't have children or siblings, we make this a standard, flat, data template. -->
                    <DataTemplate DataType="{x:Type local:EmptyFolderNode}">
                        <StackPanel Orientation="Horizontal">
                            <!-- Need to replace w/ an image of a file -->
                            <TextBlock Text="{Binding Path=Name}" 
                                       FontSize="10"
                                       FontStyle="Italic"/>
                        </StackPanel>
                    </DataTemplate>
                </TreeView.Resources>
            </TreeView>
        </DockPanel>
    </Window>
    

    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's Children collection.

    One last thing to show is the flexibility you now have in the view. Since the FileNode contains its parent DirectoryNode and the FileInfo 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 the FileNode 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.

    <DataTemplate DataType="{x:Type local:FileNode}">
        <StackPanel Orientation="Horizontal">
            <Label Content="2"
                    FontFamily="WingDings"
                    FontWeight="Black" />
            <!-- Need to replace w/ an image of a file -->
        <TextBlock Text="{Binding Path=Path}">
            <TextBlock.Style>
                <Style TargetType="TextBlock">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Path=File.Extension}"
                                        Value=".exe">
                            <Setter Property="Foreground"
                                    Value="Blue" />
                        </DataTrigger>
    
                        <DataTrigger Binding="{Binding Path=File.Extension}"
                                        Value=".dll">
                            <Setter Property="Foreground"
                                    Value="Red" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBlock.Style>
        </TextBlock>
        </StackPanel>
    </DataTemplate>
    

    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 the Execute 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.

    0 讨论(0)
提交回复
热议问题