Imagine a file system manager like Windows Explorer that shows files and folders from a computer in a treeview frontend.
This article is a partnership with Lucas Juliano, friend, workmate, and my blog’s editor.
There is no fun -- just writing methods and functions to interact with the system operational file system, so let’s use some Design Patterns to organize our C# code.
Before creating all frontend in VueJS, let’s create an API to access the file system and then use jQuery to show them.
At least, our File System Manager will earn a new clean frontend in VueJS, more friendly and, with more performance.
File System
API .NET Core C#
We are going to create a new .NET CORE API to interact and read all the files and folders from the file system.
Tag Helpers MVC .NET CORE
We are going to use the Tag Helper concept to create a file system manager component to use more than one time or more than one page.
jQuery frontend
Create a simple jQuery Frontend to interact with the DOM and consume the file system manager API.
VueJS frontend
Create a new frontend in VueJS, consuming the same API above. Lucas Juliano developed that frontend.
Design Patterns
For the API, we are going to use some Design Patterns like Repository, Dependency Injection, Strategy, and Factory.
I have written an eBook about Design Patterns, but I haven’t translated it into English yet.
jQuery and VueJS frontend preview
One of my students gave me a tip about showing in an article what to expect before showing any code.
His concern was the benefit of showing the final project before all explanations and coding to get there.
So Mario, for you, this is the final project.
jQuery frontend
VueJS frontend
The file system manager is not a big deal, so let’s check in more detail how we did it.
ASP.NET Core API Software Architecture
For understanding purposes, I had made a class diagram separating all responsibilities in the solution.
The FileSystemController knows about an IFileSystemService interface that contains all methods to return files and folders.
public interface IFileSystemService
{
IEnumerable<FileSystemObject> GetAllFileSystemObject(
string fullName);
object ToJson(
IEnumerable<FileSystemObject> objs,
IFileSystemFormatter fileSystemFormatter = null);
bool DirectoryExists(
string fullName);
}
The FileSystemService that implements IFileSystemService knows another interface called IFileSystemRepository to interact with files doesn’t matter if those files are in a database or in the file system itself.
public interface IFileSystemRepository :
ISelectRepository<Models.FileSystemObject>,
IDeleteRepository<Models.FileSystemObject>
{
bool Exists(
string fullName);
}
There is just one implementation of that repository interface that reads all stuff from the System.IO namespace.
In the future, if you want to create a logical file system using a database, you will only need to implement the IFileSystemRepository interface.
That is the Inversion of Control concept that we program using interfaces without knowing about their implementations.
The FileSystemService class is responsible for bringing all files and folders with a specific format. That is the goal of the IFileSystemFormatter interface.
We have two IFileSystemFormatter implementations, one for a Default format and others using the Humanizer library to format all dates and sizes with a more readable text.
public interface IFileSystemFormatter
{
object ToJson(
IEnumerable<FileSystemObject> fileSystemObjects);
}
That Formatter solution uses the Strategy Design Pattern.
One of my Blog’s contributors, Kleber Silva, had written an article about the Humanizer library.
ASP.NET CORE Configuration
The .NET CORE dependency injection settings code,
services.AddSingleton<IFileSystemRepository, FileSystemRepository>();
services.AddSingleton<IFileSystemService, DefaultFileSystemService>();
There are other settings in the Startup.cs file and, for performance, let’s remove NULL properties from JSON serialization.
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(opt =>
{
opt.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
opt.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});
API .NET CORE Controller
The file system manager controller,
public sealed class FileSystemController :
Controller
{
private readonly IFileSystemService _fileSystemService;
public FileSystemController(
IFileSystemService fileSystemService)
{
_fileSystemService = fileSystemService;
}
public IActionResult Index(
string fullName = null,
string formatterName = null)
{
var data = _fileSystemService.GetAllFileSystemObject(
fullName);
var json = _fileSystemService.ToJson(
data,
FormatterFactory.CreateInstance(formatterName));
return Json(
json);
}
}
The Index action receives two parameters, a folder path and a type of format, default or humanizer format.
API .NET CORE Formatter classes, Services, and Repository
The DefaultFileSystemFormatter and HumanizerFileSystemFormatter,
public sealed class DefaultFileSystemFormatter :
IFileSystemFormatter
{
public object ToJson(
IEnumerable<FileSystemObject> objs)
{
var dateTimeFormat = "MM/dd/yyyy HH:mm";
return objs?.Select(obj => new
{
obj.Name,
Id = obj.Id.Replace(@"\", @"\\"),
obj.IsFile,
obj.HasChilds,
LastWriteTime = obj.LastWriteTime?.ToString(dateTimeFormat),
CreationTime = obj.CreationTime?.ToString(dateTimeFormat),
obj.Size,
obj.Extension
});
}
}
public sealed class HumanizerFileSystemFormatter :
IFileSystemFormatter
{
public object ToJson(
IEnumerable<FileSystemObject> objs)
{
return objs?.Select(obj => new
{
obj.Name,
Id = obj.Id.Replace(@"\", @"\\"),
obj.IsFile,
obj.HasChilds,
LastWriteTime = obj.LastWriteTime.Humanize(),
CreationTime = obj.CreationTime.Humanize(),
Size = obj.Size.HasValue ?
obj.Size.Value.Bytes().Humanize() :
null,
obj.Extension
});
}
}
public sealed class DefaultFileSystemService :
IFileSystemService
{
private readonly IFileSystemRepository _fileSystemRepository;
public DefaultFileSystemService(
IFileSystemRepository fileSystemRepository)
{
_fileSystemRepository = fileSystemRepository;
}
public IEnumerable<FileSystemObject> GetAllFileSystemObject(
string fullName)
{
var objs = _fileSystemRepository.SelectMany(
fullName)
.ToList();
if (objs.Any())
objs.OrderBy(obj => obj.IsFile.ToString());
return objs;
}
public object ToJson(
IEnumerable<FileSystemObject> objs,
IFileSystemFormatter fileSystemFormatter = null)
{
fileSystemFormatter = fileSystemFormatter ?? FormatterFactory.CreateInstance();
return fileSystemFormatter.ToJson(
objs);
}
public bool DirectoryExists(
string fullName)
{
var result = _fileSystemRepository.Exists(
fullName);
return result;
}
}
The class mentioned above doesn’t know about System.IO; in other words, it doesn’t matter if the data came from the database, filesystem, or other data sources.
The more complex class is the FileSystemRepository that interacts with the system operational file system. You can download all source code from my Github.
public IEnumerable<FileSystemObject> SelectMany(
string id)
{
id = id ?? Environment.CurrentDirectory;
var objs = new List<FileSystemObject>();
if (Directory.Exists(id))
{
foreach (DirectoryInfo directoryInfo in new DirectoryInfo(id).GetDirectories().AsParallel())
{
var obj = SelectOneDirectoryInfo(
directoryInfo);
objs.AddIfNotNull(obj);
}
foreach (FileInfo fileInfo in new DirectoryInfo(id).GetFiles().AsParallel())
{
var obj = SelectOneFileInfo(
fileInfo);
objs.AddIfNotNull(obj);
}
}
return objs;
}
public FileSystemObject SelectOne(
string id)
{
var obj = SelectOneFileInfo(
new FileInfo(id));
if (obj == null)
{
obj = SelectOneDirectoryInfo(
new DirectoryInfo(id));
}
return obj;
}
public bool Delete(
string id)
{
var obj = SelectOneFileInfo(
new FileInfo(id));
if (obj == null)
{
obj = SelectOneDirectoryInfo(
new DirectoryInfo(id));
if (obj != null)
{
Directory.Delete(id);
}
}
else
{
File.Delete(id);
return true;
}
return false;
}
public bool Exists(
string fullName)
{
if (!Directory.Exists(fullName))
return false;
return true;
}
}
API .NET Core Running the API
To run the API just press F5, change the URL and set the format parameter like that,
http://localhost:59615/FileSystem?formatterName=Default.
My Chrome web browser is formatting and indenting the JSON data. That is a Google Chrome Extension that you can download and install in your browser.
jQuery frontend
I used jQuery only for testing the file system manager API and, to show the Tag Helpers features.
<div class="col-md-6"
fsl-filesystem="dir1"
fsl-filesystem-full-name="c:\dev"></div>
<div class="col-md-6"
fsl-filesystem="dir2"
fsl-filesystem-full-name="c:\dev"></div>
As you can see, there are two DIVs with some fsl-filesystem attributes and each one of them is pointing to a different folder.
To create a Tag Helper it is necessary to create a C# Class and put it in the TagHelpers folders in a .NET CORE project. Don’t forget to add this TagHelpers namespace in the _ViewImports.cshtml file.
@using FSL.FileSystem.Core
@using FSL.FileSystem.Core.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, FSL.FileSystem.Core
You can create as many properties as you want. Each property must have an attribute like this,
[HtmlTargetElement("div", Attributes = "fsl-filesystem")]
public class FileSystemTagHelper :
TagHelper
{
[HtmlAttributeName("fsl-filesystem")]
public string Id { get; set; }
[HtmlAttributeName("fsl-filesystem-full-name")]
public string FullName { get; set; }
[HtmlAttributeName("fsl-filesystem-full-height")]
public int? Height { get; set; }
public override void Process(
TagHelperContext context,
TagHelperOutput output)
{
var height = Height ?? 400;
output.Attributes.Add("style", $"overflow-y:auto;overflow-x:hidden;height:{height}px");
var fullName = FullName ?? "";
fullName = fullName.Replace(@"\", @"\\");
var sb = new StringBuilder();
sb.Append($"<div id=\"{Id}0\"></div>");
sb.Append("<script type=\"text/javascript\">");
sb.Append("$(document).ready(function () {");
sb.Append($"fileSystem.build('{fullName}', '{Id}', 0, 0);");
sb.Append("});");
sb.Append("</script>");
output.Content.SetHtmlContent(sb.ToString());
}
}
In the previous code, I had created three properties: Height, for the vertical size of treeview; Id, for the component’s distinct ID to work in the DOM; and FullName, a source path to the file system.
It is in the Process method that we need to write code to render HTML code.
The Tag Helper will render scripts that will call a method called build from the javascript filesystem file.
var fileSystem = function () {
var index = 0,
build = function (dir, id, objIndex, tab) {
$.getJSON(
'filesystem?fullName=' + dir + '&formatterName=Humanizer',
(data, status) => {
// full code in my github
});
}
return {
build: build
};
}();
To create the filesystem.js file, I used the Revealing Module Pattern to organize all javascript code.
All the [treeview] is built by hand using jQuery with the data returned by the API.
Obviously, I reinvented the wheel to write this javascript code because there are a lot of Treeview components out there. But I don’t care. It was such fun playing with jQuery again.
VueJS Frontend
VueJS is a single-page application framework that uses the MVVM Design Pattern to interact with the javascript and DOM.
You can download the File System Manager VueJS source code from Lucas Juliano’s Github.
Thank you for reading it.