Search code examples
c#mvvmtreeviewfody

How to show TreeView on ComboBox.SelectedItem != null


I am writing a Directory explorer using the MVVM-Pattern.

My UI consists of a ComboBox which contains a path to a directory and a TreeView which acts as a Directory explorer.

The thing I am struggling with is to show the TreeView only when a ComboBox item is selected but I have no real idea how to achieve that.

Here is my code.

MainWindow.xaml

<StackPanel>

        <TextBlock TextWrapping="Wrap" Text="Select a Repository Directory" Margin="10,0" FontSize="16"/>
        <ComboBox x:Name="cmbx" Margin="10,30,10,0" ItemsSource="{Binding CmbxItems}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}"/>

        <TreeView x:Name="FolderView" Height="250" Margin="10,50,10,0" ItemsSource="{Binding Items}" Visibility="{Binding IsItemSelected}">

            <TreeView.ItemContainerStyle>
                <Style TargetType="{x:Type TreeViewItem}">
                    <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
                </Style>
            </TreeView.ItemContainerStyle>

            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                    <StackPanel Orientation="Horizontal">
                        <Image Name="img" Width="20" Margin="5" 
                                           Source="{Binding Type,
                                                Converter={x:Static local:HeaderToImageConverter.ConverterInstance}}"/>
                        <TextBlock VerticalAlignment="Center" Text="{Binding Name}"/>
                    </StackPanel>

                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>

        </TreeView>

    </StackPanel>

ViewModel.cs

/// <summary>
    /// ViewModel for the main Directory view.
    /// </summary>
    class ViewModel : BaseViewModel
    {

        #region Properties

        public ObservableCollection<DirectoryStructureViewModel> Items { get; set; }

        public ObservableCollection<string> CmbxItems { get; set; }

        public bool _itemIsSelected = false;

        public bool ItemIsSelected
        {
            get
            {
                return _itemIsSelected;
            }
            set
            {
                _itemIsSelected = value;
            }
        }

        public string _selectedItem;

        public string SelectedItem
        {
            get
            {
                return _selectedItem;
            }
            set
            {               
                ItemIsSelected = true;
                MessageBox.Show("Selection changed");
                _selectedItem = value;
            }
        }

        #endregion

        #region Constructor

        /// <summary>
        /// Constructor.
        /// </summary>
        public ViewModel()
        {

            CmbxItems = new ObservableCollection<string>(){};

            CmbxItems.Add(DirectoryItem.rootPath);

            //Get initial directory.
            var children = DirectoryStructure.GetInitialDirectory();

            //Create view models from data.
            this.Items = new ObservableCollection<DirectoryStructureViewModel>(children.Select(dir => new DirectoryStructureViewModel(dir.FullPath, NodeTypes.Folder)));

        }

        #endregion
    }

BaseViewModel.cs

/// <summary>
    /// Base ViewModel that fires PropertyChanged events.
    /// </summary>
    public class BaseViewModel : INotifyPropertyChanged
    {
        //Event that is fired when aa child property changes its value.
        public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };
    }

DirectoryStructureViewModel.cs

public class DirectoryStructureViewModel : BaseViewModel
    {
        #region Properties

        public NodeTypes Type { get; set; }

        public string FullPath { get; set; }

        public string Name { get { return DirectoryStructure.GetDirectoryOrFileName(this.FullPath); } }

        /// <summary>
        /// List of all children contained in this item.  
        /// </summary>
        public ObservableCollection<DirectoryStructureViewModel> Children { get; set; }

        /// <summary>
        /// Indicates that this item can be expanded.
        /// </summary>
        public bool CanExpand { get { return this.Type != NodeTypes.File; } }

        /// <summary>
        /// Indicates if the current item is expanded.
        /// </summary>
        public bool IsExpanded
        {
            get
            {
                return this.Children?.Count(chldrn => chldrn != null) > 0;
            }
            set
            {
                if (value == true)
                {
                    Expand();
                }
                else
                {
                    this.ClearChildren();
                }
            }
        }

        public bool IsItemSelected { get; set; }

        #endregion

        #region Commands

        public ICommand ExpandCommand { get; set; }

        #endregion

        #region Constructor

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="fullPath">Path of this item.</param>
        /// <param name="type">Type of this item.</param>
        public DirectoryStructureViewModel(string fullPath, NodeTypes type)
        {
            this.ExpandCommand = new TreeViewRelayCommand(Expand);

            this.FullPath = fullPath;
            this.Type = type;

            this.ClearChildren();
        }

        #endregion

        #region Helper Methods

        //Removes all children from the list.
        private void ClearChildren()
        {
            this.Children = new ObservableCollection<DirectoryStructureViewModel>();

            if (this.Type != NodeTypes.File)
            {
                //Adds a dummy item to show the expand arrow.
                this.Children.Add(null);
            }
        }

        #endregion

        #region Functions

        /// <summary>
        /// Expands this directory and finds all children.
        /// </summary>
        private void Expand()
        {
            if (this.Type != NodeTypes.File)
            {

                //Find all children
                var children = DirectoryStructure.GetDirectoryContents(this.FullPath);
                this.Children = new ObservableCollection<DirectoryStructureViewModel>(children.Select(content => new DirectoryStructureViewModel(content.FullPath, content.Type)));
            }
            else
            {
                return;
            }
        }

        #endregion
    }

Commands.cs

class TreeViewRelayCommand : ICommand
    {
        #region Members

        private Action mAction;

        #endregion

        #region Events

        /// <summary>
        /// Event that is executed, when <see cref="CanExecute(object)"/> value has changed.
        /// </summary>
        public event EventHandler CanExecuteChanged = (sender, e) => { };

        #endregion

        #region Constructor

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="action"></param>
        public TreeViewRelayCommand(Action action)
        {
            mAction = action;
        }

        #endregion

        #region Command Methods

        public bool CanExecute(object parameter)
        {
            return true;
        }

        /// <summary>
        /// Executes the commands action.
        /// </summary>
        /// <param name="parameter"></param>
        public void Execute(object parameter)
        {
            mAction();
        }

        #endregion

    }

Edit: I am using FodyWeavers


Solution

  • You are almost there:

    <TreeView ... Visibility="{Binding IsItemSelected}">
    

    One problem is that you are binding Visibility to bool. There should be an binding error in Outputs window (check it now and always for various possible problems with WPF applications).

    You can use BoolToVisibilityConverter (using this answer):

    <someContainer.Resources>
        <BooleanToVisibilityConverter x:Key="converter" />
    </someContainer.Resources>
    ...
    <TreeView ... Visibility="{Binding IsItemSelected, Converter={StaticResource converter}}">
    

    Backing fields shouldn't be public.

    You should (normally) rise notifications for all properties used in bindings. Typically at the end of setter.

    I'd personally use getter only property:

    string _selectedItem;
    public string SelectedItem
    {
        get => _selectedItem;
        set
        {
            _selectedItem = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(IsItemSelected));
        }
    }
    
    public bool IsItemSelected => SelectedItem != null;
    

    Also you miss correct event rising method:

    public class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        // can be public if you want to rise event from outside
        protected void OnPropertyChanged([CallerMemberName] string property = "") =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
    
    }