0

I'm trying to make a table that lists 2 types of files linked together. One type are .mp3, and the other are .txt files. I want these files to be linked together, such that the files that share the same name share one row, when the foreach loop passes through them. This is so that the mp3 files can be played, and the corresponding text file can be opened.

App.razor page has a table that displays all files in a folder, but it doesn't take into account if the files of the 2 types share the same name. Can anybody help with how to make a class that has the files linked together so that they can be called in the table?

Here is the code.

<table class="table table-striped mb-0">
    <thead>
    <tr>
        <th scope="col">Name</th>
        <th scope="col">Actions</th>
    </tr>
    </thead>
    <tbody>
        @foreach (var file in textList)
        {
            <tr>
                <td>
                    @file.Name
                </td>
                <td>
                    <span @onclick="() => PlayAudio(file.Url)"
                          class="text-primary oi oi-play-circle me-2" aria-hidden="true" role="button">
                    </span>
                    <span @onclick="() => DeleteAudio(file)"
                          class="text-danger oi oi-trash" aria-hidden="true" role="button">
                    </span>
                    <span @onclick="() => openTextFile(file)"
                          ><button>Open</button>
                    </span>
                </td>
            </tr>
        }
    }
    </tbody>
</table>
@code{
    readonly List<TextFile> textList = new();
    readonly string FolderName = "textSoundFiles";

    protected override void OnInitialized()
    {
        var path = $"{env.WebRootPath}\\{FolderName}\\";
        var files = new DirectoryInfo(path).GetFiles();

        foreach (var file in files)
        {
            textList.Add(new TextFile
            {
                Name = file.Name,
                Url = $"/textFiles/{file.Name}",
                Path = file.FullName
            });
        }
    }

    public class TextFile
    {
        public string Name { get; set; }
        public string Url { get; set; }
        public string Path { get; set; }
    }
}

I'm trying to make a table that lists files, that allows me to play audio from the audio files that are listed. I'm trying to 2 type of files within a directory, such that the files that have the same names(excluding their MIME type) are linked to each other

Addlon
  • 45
  • 1
  • 7
  • Iterate `files.GroupBy(file => file.Name)` – Brian Parker Nov 21 '22 at 14:14
  • @BrianParker how would that make the files link together? – Addlon Nov 21 '22 at 14:32
  • They will be grouped by Name and you will be iterating over a groups. The groups themselves will have a sub list of, in your case, one or two items. If your name includes the extension `.txt` or `.mp3` you may have to write a derived property to remove it and group by that. – Brian Parker Nov 21 '22 at 14:38
  • @BrianParker I came across `System.IO.Path.ChangeExtension(path, null);` Which would allow me to cut off the extensions while keeping the path, but it didn't work. can you check the edit in my post and see what i did wrong? – Addlon Nov 21 '22 at 15:07

2 Answers2

1

Use Linq's GroupBy

@foreach(var fileGroup in myFilesGroupedAndSorted)
{
    <h3>@fileGroup.Key</h3>
    @foreach(var file in fileGroup.OrderBy( file => file.Path))
    {
        <div>@file.Path</div>
    }
}

@code {
    // without access to your folder I generated random data.
    List<TextFile> myList = new List<TextFile>
    {
        new TextFile { Name = "", Path = "some-path\\SomeFile1.txt", Url = "" },
        new TextFile { Name = "", Path = "some-path\\SomeFile2.txt", Url = "" },
        new TextFile { Name = "", Path = "some-path\\SomeFile3.txt", Url = "" },
        new TextFile { Name = "", Path = "some-path\\SomeFile4.txt", Url = "" },
        new TextFile { Name = "", Path = "some-path\\SomeFile5.txt", Url = "" },
        new TextFile { Name = "", Path = "some-path\\SomeFile6.txt", Url = "" },
        new TextFile { Name = "", Path = "some-path\\SomeFile1.mp3", Url = "" },
        new TextFile { Name = "", Path = "some-path\\SomeFile2.mp3", Url = "" },
        new TextFile { Name = "", Path = "some-path\\SomeFile3.mp3", Url = "" },
        new TextFile { Name = "", Path = "some-path\\SomeFile4.mp3", Url = "" },
        new TextFile { Name = "", Path = "some-path\\SomeFile5.mp3", Url = "" },
        new TextFile { Name = "", Path = "some-path\\SomeFile6.mp3", Url = "" },
    };


    IEnumerable<IGrouping<string, TextFile>> myFilesGroupedAndSorted
        => myList.GroupBy(file => GetPathWithoutExtension(file.Path))
                 .OrderBy(group => group.Key);

    private string GetPathWithoutExtension(string path)
    {
        return System.IO.Path.ChangeExtension(path, null);
    }
}

Processing each group then should be trivial by testing for each file extension. I would make a sub-component to pass the IGrouping<string, TextFile> as a parameter to simplify the logic...

Mp3.Razor

<tr>
    <td>
        @FileGroup.Key
    </td>
    <td>
        @if (Mp3 is not null)
        {
            <span @onclick="() => PlayAudio(Mp3.Url)"
              class="text-primary oi oi-play-circle me-2" aria-hidden="true" role="button">
            </span>
            <span @onclick="() => DeleteAudio(FileGroup.Key)"
              class="text-danger oi oi-trash" aria-hidden="true" role="button">
            </span>
        }
        @if (Text is not null)
        {
            <span @onclick="() => openTextFile(Text)">
                <button>Open</button>
            </span>
        }
    </td>
</tr>
@code {
    [Parameter]
    public IGrouping<string, TextFile> FileGroup { get; set; } = default!;

    TextFile? Text => FileGroup.FirstOrDefault(file => Path.GetExtension(file.Path).ToLower() == "txt");
    TextFile? Mp3 => FileGroup.FirstOrDefault(file => Path.GetExtension(file.Path).ToLower() == "mp3");
}
...
  <tbody>
    @foreach(var fileGroup in myFilesGroupedAndSorted)
    {
       <Mp3 FileGroup=fileGroup /> 
    }
  </tbody>
...
Brian Parker
  • 11,946
  • 2
  • 31
  • 41
  • I tried some things but a list never came up. Can I ask you to check the edit to the code in y original post and see what is wrong? – Addlon Nov 21 '22 at 19:39
  • @Addlon First is the list being populated as expected? Can you place a breakpoint and check its contents? – Brian Parker Nov 21 '22 at 20:00
  • I get the exception `System.ArgumentNullException: 'Value cannot be null. (Parameter 'source')'` in `Text => FileGroup...` and `Mp3 =>...`. – Addlon Nov 22 '22 at 10:32
  • sorry to disturb you, but the table now lists the `@fileGroup.Key` but only when the `TextFile? Text => fileGroup...` and `TextFile? Mp3 => fileGroup...` are deleted, as well as the 2 if statements that mentions them. When they are there, they get the exception `ArgumentNullException`. Do you know why it happens? – Addlon Nov 22 '22 at 15:25
  • @Addlon The error is the filter for the extension. Path.GetExtension return includes the `.` so change the two `== "mp3"` to `==".mp3"`. It's was a typo on my behalf. – Brian Parker Nov 22 '22 at 16:28
  • I can regretfully say that the error still persists with the '.' added – Addlon Nov 22 '22 at 16:32
  • 1
    @Addlon https://github.com/BrianLParker/TwoFilesLinkedQuestionOnStack – Brian Parker Nov 22 '22 at 16:49
  • I have an audio element in `Mp3Component` that is outside of the `...`, but it is called into the `foreach` loop. Is there a way to separate the audio element in `Mp3Component` from what is going in the loop, such that it can be referenced outside of the `foreach`? – Addlon Nov 23 '22 at 15:33
0

You need to "pivot" the table, i.e. convert a list to a table. LINQ can be used for this. Check out this answer. Instead of "Jan" and "Feb" you would have properties bool HasMp3 and bool HasTxt, with appropriate Where clauses. Something like this:

var files = new DirectoryInfo(path).GetFiles();

textList.AddRange(files
    .GroupBy(c => Path.ChangeExtension(c.Name, null))
    .Select(g => new TextFile() {
        Name = g.Key,
        Url = $"/textFiles/{g.Key}",
        HasMp3 = g.Any(c => Path.GetExtension(c) == ".mp3"),
        HasTxt = g.Any(c => Path.GetExtension(c) == ".txt"),
    }));

With these two bool properties you can hide buttons if corresponding file does not exist. Url property does not have an extension. You would have to append every appropriate extension in every appropriate function.

Dialecticus
  • 16,400
  • 7
  • 43
  • 103