2
\$\begingroup\$

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:

enter image description here

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.

\$\endgroup\$
1
  • \$\begingroup\$ Your ManagerHelper isn't a helper. It's a file router. \$\endgroup\$
    – RubberDuck
    Apr 10, 2018 at 20:14

1 Answer 1

1
\$\begingroup\$

Duplicate code in FileInformation and MediaFileInformation

MediaFileInformation should inherit from FileInformation to eliminate the duplicate code. In the ctor just call base

public class FileInformation
{
    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)
    {
    }
}
public class MediaFileInformation : FileInformation
{
    public TimeSpan FileLength { get; }
    public MediaFileInformation(string filePath)
        : base(filePath)
    {
        FileLength = FileInfo.GetFileDuration();
    }

    public MediaFileInformation(Uri fileUri)
        : base(fileUri)
    {
    }
}
\$\endgroup\$
8
  • \$\begingroup\$ Please, take another look at the class MediaFileInformation. \$\endgroup\$
    – Denis
    Mar 11, 2018 at 16:28
  • \$\begingroup\$ Please take a look at duplicate code is eliminate. Sorry if this is not helpful. \$\endgroup\$
    – paparazzo
    Mar 11, 2018 at 16:36
  • \$\begingroup\$ The DependencyProperty is missing \$\endgroup\$
    – Denis
    Mar 11, 2018 at 16:44
  • \$\begingroup\$ You cannot add it? \$\endgroup\$
    – paparazzo
    Mar 11, 2018 at 16:45
  • \$\begingroup\$ If you can I'd be more than happy to incorporate your advice in my project \$\endgroup\$
    – Denis
    Mar 11, 2018 at 16:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.