0

I'm trying to recreate the File Explorer using WPF and MVVM. I got a TreeView and ListBox, both of which work properly, as long as the only thing I do is browse through the folders. As soon as I try copying and pasting files, it doesn't redraw the screen and I need to unselect and reselect the folder to be able to see the updated list of items.

(To clarify, I add extra code because I suspect that my problem is not directly related to the ObservableCollections and maybe it is more related to how I implemented everything.)

The overview of my project is the following:

I represent each folder or file using the viewmodels FileViewModel and FolderViewModel. The FolderViewModel, which is of more interest, contains two strings, one for the full path and one for the name of the file, as well as an ObservableCollection containing the contents of a given folder which is lazy-loaded when the contents are retrieved. Here is the FolderViewModel. I omitted everything that is not relevant + the Files ObservableCollection and methods that are similar to the Move:

public class FolderViewModel : FileFolderBaseViewModel
{
    public TrulyObservableCollection<FileFolderBaseViewModel> _folders;

    private ICommand _paste;
    public ICommand PasteClicked
    {
        get => _paste;
        set
        {
            _paste = value;
            RaisePropertyChanged();
        }
    }
    public TrulyObservableCollection<FileFolderBaseViewModel> Folders
    {
        get
        {
            if (_folderModel.HasDummy)
            {
                _folders.Clear();
                PopulateFoldersOnDemand();
            }
            return _folders;
        }
        set { _folders = value; RaisePropertyChanged(); }
    }
    public override bool IsSelected
    {
        get => _folderModel.IsSelected;
        set
        {
            if (value != _folderModel.IsSelected)
            {
                _folderModel.IsSelected = value;
                RaisePropertyChanged();
                //Remove contents only if not expanded and not selected
                if (!_folderModel.IsSelected && !_folderModel._isExpanded)
                {
                    if (_folders.Count() > 0)
                    {
                        _folderModel.HasDummy = true;
                        Folders.Clear();
                        Files.Clear();
                        Folders.Add(new FileViewModel(new FileModel("dummy", "dummy")));
                    }
                }
            }
        }
    }
    public bool IsExpanded
    {
        get => _folderModel._isExpanded;
        set
        {
            if (value != _folderModel._isExpanded)
            {
                _folderModel._isExpanded = value;
                RaisePropertyChanged();

                //Remove contents only if not expanded and not selected
                if (!_folderModel._isExpanded && !_folderModel.IsSelected)
                {
                    if (_folders.Count() > 0)
                    {
                        _folders.Clear();
                        _files.Clear();
                        _folderModel.HasDummy = true;
                        Folders.Add(new FileViewModel(new FileModel("dummy", "nopath")));
                    }
                }
            }
        }
    }

    public FolderViewModel(FolderModel folderModel, TrulyObservableCollection<FileFolderBaseViewModel> contents) : base(folderModel)
    {
        _files = new TrulyObservableCollection<FileFolderBaseViewModel>(contents.OfType<FileViewModel>());
        _folders = new TrulyObservableCollection<FileFolderBaseViewModel>(contents.OfType<FolderViewModel>());

        if (Directory.GetDirectories(_folderModel.FilePath).Any())
        {
            _folderModel.HasDummy = true;
            _folders.Add(new FileViewModel(new FileModel("dummy", "nopath")));
        }

        _paste = new RelayCommand(new Action<object>(HandlePaste));
    }

    private void Move(FileFolderBaseViewModel file)
    {
        try
        {
            if (file is FileViewModel f)
            {
                File.Move(file.FilePath, FilePath + "\\" + file.FileName, true);
            }
            else
            {
                Directory.Move(file.FilePath, FilePath + "\\" + file.FileName);
            }
        }
        catch (Exception e)
        {
            MessageBox.Show(e.ToString(), "Error", MessageBoxButton.OK);
            return;
        }
    }

    private void HandlePaste(object obj)
    {
        if (ClipBoardItem is not null)
        {
            if (PreviousRightClickAction is RightClickAction.Move)
            {
                if (File.Exists(FilePath + "\\" + ClipBoardItem.FileName))
                {
                    MessageBoxResult retVal = MessageBox.Show("File already exists. Overwrite?", "Warning", MessageBoxButton.YesNoCancel, MessageBoxImage.Warning);

                    if (retVal != MessageBoxResult.Yes)
                    {
                        return;
                    }
                }
                Files.Add(ClipBoardItem);
                Move(ClipBoardItem);
            }
            ...
        }
    }

    private void HandleClickListBoxFolder(object obj) => IsSelected = true;

    private void PopulateFoldersOnDemand()
    {
        string[] dirs = Directory.GetDirectories(_folderModel.FilePath);
        FileFolderBaseModel model;

        foreach (string dir in dirs)
        {
            DirectoryInfo inf = new DirectoryInfo(dir);
            model = new FolderModel(inf.Name, inf.FullName);
            _folders.Add(new FolderViewModel((FolderModel)model, new TrulyObservableCollection<FileFolderBaseViewModel>()));
        }
    }
}

Then I have the FileExplorerViewModel which contains two ObservableCollection, one for the TreeView folders, and one for the ListBox items to be displayed depending on the selected folder in the TreeView:

public class FileExplorerViewModel : BaseViewModel
{
    private ICommand _selectedItemChanged;
    public TrulyObservableCollection<FileFolderBaseViewModel> ListViewItems { get; set; }
    public TrulyObservableCollection<FolderViewModel> Folders { get; set; }

    public ICommand ChangeRoot
    {
        get => _changeRoot;
        set {_changeRoot = value;}
    }

    public ICommand SelectedItemChanged
    {
        get => _selectedItemChanged;
        set { _selectedItemChanged = value;}
    }

    public FileFolderBaseViewModel? SelectedItem 
    {
        get => FileFolderBaseModel.SelectedItem;
        set
        {
            if(FileFolderBaseModel.SelectedItem != value)
            {
                FileFolderBaseModel.SelectedItem = value;
                RaisePropertyChanged();

                if (FileFolderBaseModel.SelectedItem is FolderViewModel selectedFolder)
                {
                    ListViewItems.Clear();
                    foreach (var item in selectedFolder.Folders)
                    {
                        ListViewItems.Add(item);
                    }
                    foreach (var item in selectedFolder.Files)
                    {
                        ListViewItems.Add(item);
                    }
                }
            }
        }
    }

    public FileExplorerViewModel()
    {
        _rootDirectory = "C:";
        ListViewItems = new TrulyObservableCollection<FileFolderBaseViewModel>();
        Folders = new TrulyObservableCollection<FolderViewModel>();

        PopulateFolders();
        _selectedItemChanged = new RelayCommand(new Action<object>(HandleListViewItemChanged));
    }

    private void HandleListViewItemChanged(object obj) => SelectedItem = (FileFolderBaseViewModel)obj;

    private void PopulateFolders()
    {
        string[] directoryPaths = Directory.GetDirectories(RootDirectory);

        foreach (string f in directoryPaths)
        {
            FileAttributes attributes = File.GetAttributes(@f);
            if (attributes.HasFlag(FileAttributes.Hidden) || attributes.HasFlag(FileAttributes.System))
            {
                continue;
            }
            Folders.Add((FolderViewModel)CreateFolderItem(f));
        }
    }

    private FileFolderBaseViewModel CreateFolderItem(string path)
    {
        DirectoryInfo dirInfo = new DirectoryInfo(path);

        return 
            new FolderViewModel(
                new FolderModel(dirInfo.Name, path), new TrulyObservableCollection<FileFolderBaseViewModel>());
    }
}

I tried implementing a TrulyObservableCollection so that a modification of an item in the TrulyObservableCollection will trigger a CollectionChanged event and the app will be redrawn. Then when I paste an item into a folder, either the item is copied/moved into the object also, or the contents list refetches all files and folders using Directory.GetFiles and Directory.GetDirectories.

I also tried using a FileSystemWatcher that fires a CollectionChanged event but that didn't work either.

magmyr01
  • 13
  • 4

1 Answers1

0

By using the FileSystemWatcher in the FileExplorerViewModel and repopulating the ObservableCollection I could manage and redraw the app as soon as I move or copy an item into a folder that is open.

magmyr01
  • 13
  • 4