I'm currently making a system that can accept different sorts of files and some classes are interested only in specific types of files, while some don't discriminate at all.
Note
- Type of file isn't necessarily a difference in the extension of a file, but rather how they function, e.g music files have duration, but not only 1 extension, so every file that has duration only is considered a Music File regardless of their extension.
I decided to go for an event based approach because there might be no consumers and there might be many and I don't really want to introduce any dependencies in the notifier classes or clutter them with unnecessary logic.
To simplify the job of the notifier I wrote a helper class - ManagerHelper
, this avoids the need of filtering the input in every class and also provides extendability in case a new file type is added in the system which would otherwise require a change in every notifier class.
This is the current diagram of how the classes operate:
Simple enough structure and it does the job.
IFileInformation
All file types inherit a common interface with the most basic information a file can have:
public interface IFileInformation
{
string FileName { get; }
FileInfo FileInfo { get; }
Uri Uri { get; }
}
FileInformation
The basic implementation of the interface is FileInformation
, it's used primarily for files such as text documents:
public class FileInformation : IFileInformation
{
public string FileName { get; }
public FileInfo FileInfo { get; }
public Uri Uri { get; }
public FileInformation(string filePath)
{
if (string.IsNullOrEmpty(filePath))
{
throw new ArgumentNullException(nameof(filePath));
}
FileInfo = new FileInfo(filePath);
FileName = Path.GetFileNameWithoutExtension(FileInfo.Name);
Uri = new Uri(FileInfo.FullName);
}
public FileInformation(Uri fileUri)
: this(fileUri.OriginalString)
{
}
}
MediaFileInformation
For media files we have MediaFileInformation
, it has a TimeSpan FileLength
property, and a property + DependencyProperty
for the state of the file - playing/not playing:
public class MediaFileInformation : DependencyObject, IFileInformation, INotifyPropertyChanged
{
public TimeSpan FileLength { get; }
public string FileName { get; }
public FileInfo FileInfo { get; }
public Uri Uri { get; }
public static readonly DependencyProperty IsPlayingProperty =
DependencyProperty.Register(nameof(IsPlaying), typeof(bool), typeof(MediaFileInformation),
new PropertyMetadata(null));
public bool IsPlaying
{
get => (bool)GetValue(IsPlayingProperty);
set
{
SetValue(IsPlayingProperty, value);
OnPropertyChanged();
}
}
public MediaFileInformation(string filePath)
{
if (string.IsNullOrEmpty(filePath))
{
throw new ArgumentNullException(nameof(filePath));
}
FileInfo = new FileInfo(filePath);
FileName = Path.GetFileNameWithoutExtension(FileInfo.Name);
FileLength = FileInfo.GetFileDuration();
Uri = new Uri(FileInfo.FullName);
}
public MediaFileInformation(Uri fileUri)
: this(fileUri.OriginalString)
{
}
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
Manager
There is actually a single generic static class - Manager<TFileInformation>
. It contains a single event, along with few overloads of the same method to request a new file update:
public static class Manager<TFileInformation>
where TFileInformation : IFileInformation
{
public static event EventHandler<ManagerEventArgs<TFileInformation>> NewRequest;
public static void Request(IEnumerable<TFileInformation> selectedFiles)
{
Request(new ManagerEventArgs<TFileInformation>(selectedFiles));
}
public static void Request(ManagerEventArgs<TFileInformation> args)
{
OnNewRequest(args);
}
private static void Request(IEnumerable<object> selectedFiles)
{
Request(selectedFiles.Cast<TFileInformation>());
}
private static void OnNewRequest(ManagerEventArgs<TFileInformation> args)
{
NewRequest?.Invoke(typeof(Manager<TFileInformation>), args);
}
}
ManagerHelper
As illustrated in the diagram, this class servers as a mediator, which notifies each Manager
, along with the appropriate information, that they must invoke their respective event. If you've already noticed, there is an odd private
method inside the Manager
class, it's used here, because of problems I encountered while trying to cast the generic type argument of IEnumerable<object>
to a specific implementation of the IFileInformation
.
The only solution I came up with, which keeps the type safety of the Manager
class, was to make a private method and access that only through reflection and allow the Manager
class to cast the IEnumerable<object>
to the appropriate IEnumerable<TFileInformation>
.
public static class ManagerHelper
{
private static readonly Dictionary<Type, Action<IEnumerable<object>>> _newRequests;
static ManagerHelper()
{
var fileInformations = typeof(IFileInformation).GetDerivedTypesFor(Assembly.GetExecutingAssembly());
_newRequests = new Dictionary<Type, Action<IEnumerable<object>>>();
foreach (var information in fileInformations)
{
var instance = typeof(Manager<>).MakeGenericType(information);
var methodInfo = instance.GetMethod("Request", BindingFlags.NonPublic | BindingFlags.Static);
_newRequests.Add(information, enumerable => methodInfo.Invoke(instance, new object[] { enumerable }));
}
}
public static void Request(IEnumerable<IFileInformation> selectedFiles)
{
Request(new ManagerEventArgs<IFileInformation>(selectedFiles));
}
public static void Request(ManagerEventArgs<IFileInformation> args)
{
var typeGroups = args.SelectedFiles.GroupBy(information => information.GetType());
foreach (var typeGroup in typeGroups)
{
_newRequests[typeGroup.Key].Invoke(typeGroup);
}
}
}
ManagerEventArgs
public class ManagerEventArgs<TFileInformation>
where TFileInformation : IFileInformation
{
public IEnumerable<TFileInformation> SelectedFiles { get; }
public ManagerEventArgs(IEnumerable<TFileInformation> selectedFiles)
{
SelectedFiles = selectedFiles ?? throw new ArgumentNullException(nameof(selectedFiles));
}
}
Usage
Now if you want to receive notification when a new file of type X is added you just do:
Manager<X>.NewRequest+=...
It's also really simple to send a collection of different types of files for the notifier:
ManagerHelper.Request(files)
In case you only want to send a filtered bunch of files you can also do:
Manager<X>.Request(files.OfType<X>());
What are you thoughts? Are there any flaws? Anything I can improve on? Maybe you have a better alternative solution?
I personally feel that the naming could be improved and maybe there is a better way of accessing the public method of the Manager
instead of making a private one just for this use.
If you need any extra information feel free to ask me in the comments.
ManagerHelper
isn't a helper. It's a file router. \$\endgroup\$