In order to bind something to a control in UWP, you need something. Essentially, what that means is that we need a model.
A model, in .NET terms, is simply a class that holds data.
We're going to create a new class, and we'll call it Item:
public class Item
{
public string Name { get; set; }
public ObservableCollection<Item> Children { get; set; } = new ObservableCollection<Item>();
public ItemType ItemType { get; set; }
public string FullName { get; set; }
public override string ToString()
{
return Name;
}
}
I would always recommend that models are held in their own file and sit in a folder called Models, but there's no technical reason why you couldn't add this class to the end of ImportBooks.cs.
Most of this class should be self-explanatory; we're holding the Name and FullName (that is, the name and path) of the file. The ObservableCollection is a special type of Collection that allows the UI framework to be notified when it changes.
For the code that we're writing here, we could get away with this simply being a List; however, ObservableCollection is good practice when dealing with desktop XAML frameworks such as UWP, and this will make extensibility easier.
Finally, we're holding the type of the item, which is a new enumerated type:
public enum ItemType
{
Docx,
Docxx,
Pdfx,
Epubx,
Folder
}
Back in ImportBooks.cs, we're going to set up our ItemsSource. The first step is to add a class-level variable called DataSource:
public ObservableCollection<Models.Item> DataSource { get; set; }
Our next change is in the btnSelectSourceFolder_Click event handler:
private void btnSelectSourceFolder_Click(object sender, EventArgs e)
{
try
{
FolderBrowserDialog fbd = new FolderBrowserDialog();
fbd.Description = "Select the location of your eBooks and documents";
DialogResult dlgResult = fbd.ShowDialog();
if (dlgResult == DialogResult.OK)
{
UpdateBookList(fbd.SelectedPath);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
As you can see, the new method is hugely simplified compared to the previous version; we've extracted all the real logic into a new method, so let's see that next:
private void UpdateBookList(string path)
{
DirectoryInfo di = new DirectoryInfo(path);
var bookList = new List<Models.Item>();
var rootItem = new Models.Item()
{
Name = di.Name
};
rootItem.ItemType = Models.ItemType.Folder;
PopulateBookList(di.FullName, rootItem);
bookList.Add(rootItem);
DataSource = new ObservableCollection<Models.Item>(bookList);
_tvFoundBooks.ItemsSource = DataSource.OrderBy(a => a.Name);
}
Here, we're setting up the root item of our TreeView; however, you'll notice that the only reference that we actually have to the TreeView is at the end, where we refresh ItemsSource. PopulateBookList is our next port of call. As before, this method is essentially in two parts; let's see the first part:
public void PopulateBookList(string paramDir, Models.Item rootItem)
{
if (rootItem == null) throw new ArgumentNullException();
rootItem.FullName = paramDir;
rootItem.ItemType = Models.ItemType.Folder;
DirectoryInfo dir = new DirectoryInfo(paramDir);
foreach (DirectoryInfo dirInfo in dir.GetDirectories())
{
var item = new Models.Item();
item.Name = dirInfo.Name;
rootItem.Children.Add(item);
PopulateBookList(dirInfo.FullName, item);
}
Here, we're recursively traversing the directory structure and populating our new model. Notice that we're setting the item type and the FullName (the directory path) at the start, and then we iterate through all the sub-directories, re-calling our method.
Recursion is the practice of calling a method from itself. Is can be very useful in scenarios such as this, where you wish to perform exactly the same operation on nested objects. It is faster than using a loop; however, it does have the potential to fill up the stack very quickly if used incorrectly.
For the second part of the function, we'll process any files that are in the current directory (that is, whichever directory is at the top of the recursion stack at the time):
foreach (FileInfo fleInfo in dir.GetFiles().Where(x => AllowedExtensions.Contains(x.Extension)).ToList())
{
var item = new Models.Item();
item.Name = fleInfo.Name;
item.FullName = fleInfo.FullName;
item.ItemType = (Models.ItemType)Enum.Parse(typeof(Extention), fleInfo.Extension.TrimStart('.'), true);
rootItem.Children.Add(item);
}
}
Our next change is to the ItemInvoked method; the new method should look as follows:
private void _tvFoundBooks_ItemInvoked(Windows.UI.Xaml.Controls.TreeView sender, Windows.UI.Xaml.Controls.TreeViewItemInvokedEventArgs args)
{
var selectedItem = (Models.Item)args.InvokedItem;
DocumentEngine engine = new DocumentEngine();
string path = selectedItem.FullName.ToString();
if (File.Exists(path))
{
var (dateCreated, dateLastAccessed, fileName, fileExtention, fileLength, hasError) = engine.GetFileProperties(selectedItem.FullName.ToString());
if (!hasError)
{
txtFileName.Text = fileName;
txtExtension.Text = fileExtention;
dtCreated.Value = dateCreated;
dtLastAccessed.Value = dateLastAccessed;
txtFilePath.Text = selectedItem.FullName.ToString();
txtFileSize.Text = $"{Round(fileLength.ToMegabytes(), 2).ToString()} MB";
}
}
}
Again, this is very marginally changed; instead of storing the full filename (with the path) in the node tag property, we're now just referencing the underlying model, so it's much clearer. Our next step is to remove the existing WinForms TreeView control.