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 ObservableCollection
s 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.