You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 8 MAUI应用层级音频文件分级文件夹结构构建及相关实现咨询

问题描述

我正在构建一个.NET 8 MAUI应用,需要为医生就诊时的患者音频笔记构建如下分级文件夹存储结构:

Root
    +1001_John Dale
    -1002_Anna Simpson
     -2024-05-23
      09-12-45.wav
     -2024-05-21
      14-25-45.wav
      17-05-22.wav
    -1003_Johnatan Doe
     -2024-05-23
      09-32-45.wav
     -2024-05-20
      16-25-45.wav

具体需求:

  1. 在.NET MAUI支持的所有平台上创建上述应用级文件夹与文件存储结构,同时需支持同步至Google Drive、Azure Blob Storage或Amazon S3,要求有统一接口适配本地与云端存储。
  2. 实现可展开的患者层级视图:医生点击+图标展开患者节点显示就诊日期及关联音频笔记;搜索患者ID/姓名片段仅得到一条记录时自动展开该节点。
  3. 加载文件夹结构时不显示根目录,且用户可在应用设置中配置顶层文件夹(如示例中的"Root")。
解决方案

1. 统一存储接口与跨平台实现

定义抽象存储接口

先定义统一的存储操作接口,屏蔽本地与云端的差异:

public interface IStorageService
{
    // 创建文件夹
    Task CreateFolderAsync(string path);
    // 获取指定路径下的子文件夹列表
    Task<List<string>> GetFoldersAsync(string parentPath);
    // 获取指定路径下的文件列表
    Task<List<string>> GetFilesAsync(string folderPath);
    // 上传文件到指定路径
    Task UploadFileAsync(string localFilePath, string remotePath);
    // 下载文件到本地路径
    Task DownloadFileAsync(string remoteFilePath, string localPath);
    // 删除文件/文件夹
    Task DeleteAsync(string path);
}

本地存储实现(跨平台)

利用.NET MAUI的FileSystem API实现本地存储,自动适配各平台的应用私有目录:

public class LocalStorageService : IStorageService
{
    private readonly string _rootPath;

    public LocalStorageService()
    {
        _rootPath = FileSystem.AppDataDirectory;
    }

    public async Task CreateFolderAsync(string path)
    {
        var fullPath = Path.Combine(_rootPath, path);
        if (!Directory.Exists(fullPath))
        {
            Directory.CreateDirectory(fullPath);
            await Task.CompletedTask;
        }
    }

    public async Task<List<string>> GetFoldersAsync(string parentPath)
    {
        var fullPath = Path.Combine(_rootPath, parentPath);
        if (!Directory.Exists(fullPath)) return new List<string>();
        
        var folders = Directory.GetDirectories(fullPath)
                               .Select(Path.GetFileName)
                               .ToList();
        await Task.CompletedTask;
        return folders;
    }

    public async Task<List<string>> GetFilesAsync(string folderPath)
    {
        var fullPath = Path.Combine(_rootPath, folderPath);
        if (!Directory.Exists(fullPath)) return new List<string>();
        
        var files = Directory.GetFiles(fullPath)
                             .Select(Path.GetFileName)
                             .ToList();
        await Task.CompletedTask;
        return files;
    }

    // 本地场景下上传/下载可简化为文件复制,删除直接调用Directory/File.Delete
}

云端存储适配

针对Google Drive、Azure Blob、S3分别实现IStorageService接口,以Azure Blob为例:

public class AzureBlobStorageService : IStorageService
{
    private readonly BlobContainerClient _containerClient;

    public AzureBlobStorageService(string connectionString, string containerName)
    {
        _containerClient = new BlobContainerClient(connectionString, containerName);
    }

    public async Task CreateFolderAsync(string path)
    {
        // Azure Blob无实际文件夹,通过占位Blob模拟
        var blobClient = _containerClient.GetBlobClient($"{path}/.placeholder");
        using var stream = new MemoryStream();
        await blobClient.UploadAsync(stream, overwrite: true);
    }

    public async Task<List<string>> GetFoldersAsync(string parentPath)
    {
        var prefix = string.IsNullOrEmpty(parentPath) ? "" : $"{parentPath}/";
        var folders = new HashSet<string>();
        
        await foreach (var item in _containerClient.GetBlobsAsync(prefix: prefix, traits: BlobTraits.Metadata))
        {
            var blobName = item.Name.Substring(prefix.Length);
            if (blobName.Contains("/"))
            {
                var folderName = blobName.Split('/')[0];
                folders.Add(folderName);
            }
        }
        return folders.ToList();
    }

    // 其他方法调用Azure Blob SDK完成对应操作
}

通过依赖注入切换存储实现(在MauiProgram.cs中注册):

builder.Services.AddSingleton<IStorageService>(sp => 
{
    var useCloud = Preferences.Get("UseCloudStorage", false);
    if (useCloud)
    {
        var connStr = Preferences.Get("AzureBlobConnStr", "");
        return new AzureBlobStorageService(connStr, "audio-notes");
    }
    return new LocalStorageService();
});

2. 可展开层级视图实现

定义层级数据模型

创建支持展开/折叠状态的树形数据模型:

public class TreeNode : ObservableObject
{
    private bool _isExpanded;

    public string Name { get; set; }
    public string Path { get; set; }
    public NodeType Type { get; set; }
    public List<TreeNode> Children { get; set; } = new();
    public bool IsExpanded 
    { 
        get => _isExpanded; 
        set => SetProperty(ref _isExpanded, value); 
    }
}

public enum NodeType
{
    Patient,
    Date,
    AudioFile
}

使用TreeView构建视图

在XAML中使用.NET MAUI的TreeView控件,结合数据模板区分不同节点:

<TreeView ItemsSource="{Binding RootNodes}">
    <TreeView.ItemTemplate>
        <DataTemplate>
            <TreeViewItem IsExpanded="{Binding IsExpanded}">
                <StackLayout Orientation="Horizontal" Spacing="5">
                    <Image Source="expand_icon.png" 
                           IsVisible="{Binding Type, Converter={StaticResource NodeTypeToFolderVisibilityConverter}}"
                           WidthRequest="20" HeightRequest="20"/>
                    <Label Text="{Binding Name}" 
                           FontSize="16" 
                           VerticalOptions="Center"/>
                </StackLayout>
                <TreeViewItem.ItemsSource>
                    <Binding Path="Children"/>
                </TreeViewItem.ItemsSource>
            </TreeViewItem>
        </DataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

添加值转换器控制文件夹图标显示:

public class NodeTypeToFolderVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value is NodeType nodeType && nodeType != NodeType.AudioFile;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

搜索与自动展开逻辑

在ViewModel中实现搜索功能,单结果时自动展开节点:

public class NotesViewModel : ObservableObject
{
    private readonly IStorageService _storageService;
    public ObservableCollection<TreeNode> RootNodes { get; set; } = new();
    private string _searchText;

    public string SearchText
    {
        get => _searchText;
        set 
        { 
            SetProperty(ref _searchText, value);
            SearchPatients();
        }
    }

    public NotesViewModel(IStorageService storageService)
    {
        _storageService = storageService;
        LoadRootNodes();
        MessagingCenter.Subscribe<SettingsViewModel>(this, "SettingsUpdated", (sender) => LoadRootNodes());
    }

    private async void LoadRootNodes()
    {
        RootNodes.Clear();
        var topFolder = Preferences.Get("TopLevelFolder", "Root");
        var patientFolders = await _storageService.GetFoldersAsync(topFolder);
        foreach (var folder in patientFolders)
        {
            RootNodes.Add(new TreeNode 
            { 
                Name = folder, 
                Path = Path.Combine(topFolder, folder), 
                Type = NodeType.Patient 
            });
        }
    }

    private async void SearchPatients()
    {
        RootNodes.Clear();
        var topFolder = Preferences.Get("TopLevelFolder", "Root");
        var patientFolders = await _storageService.GetFoldersAsync(topFolder);
        var filtered = patientFolders.Where(f => f.Contains(SearchText, StringComparison.OrdinalIgnoreCase)).ToList();
        
        foreach (var folder in filtered)
        {
            RootNodes.Add(new TreeNode 
            { 
                Name = folder, 
                Path = Path.Combine(topFolder, folder), 
                Type = NodeType.Patient 
            });
        }

        if (RootNodes.Count == 1)
        {
            var patientNode = RootNodes[0];
            patientNode.IsExpanded = true;
            await LoadDateNodes(patientNode);
        }
    }

    private async Task LoadDateNodes(TreeNode patientNode)
    {
        var dateFolders = await _storageService.GetFoldersAsync(patientNode.Path);
        foreach (var folder in dateFolders)
        {
            var dateNode = new TreeNode 
            { 
                Name = folder, 
                Path = Path.Combine(patientNode.Path, folder), 
                Type = NodeType.Date 
            };
            patientNode.Children.Add(dateNode);
            dateNode.IsExpanded = true;
            await LoadAudioFiles(dateNode);
        }
    }

    private async Task LoadAudioFiles(TreeNode dateNode)
    {
        var audioFiles = await _storageService.GetFilesAsync(dateNode.Path);
        foreach (var file in audioFiles)
        {
            dateNode.Children.Add(new TreeNode 
            { 
                Name = file, 
                Path = Path.Combine(dateNode.Path, file), 
                Type = NodeType.AudioFile 
            });
        }
    }
}

3. 根目录配置与结构加载

应用设置页面实现

添加设置页面让用户配置顶层文件夹:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="YourApp.Views.SettingsPage">
    <StackLayout Padding="20">
        <Label Text="顶层文件夹路径"/>
        <Entry Text="{Binding TopLevelFolder}" Placeholder="Root"/>
        <Button Text="保存" Command="{Binding SaveSettingsCommand}"/>
    </StackLayout>
</ContentPage>

对应的ViewModel:

public class SettingsViewModel : ObservableObject
{
    private string _topLevelFolder;

    public string TopLevelFolder
    {
        get => Preferences.Get("TopLevelFolder", "Root");
        set => SetProperty(ref _topLevelFolder, value);
    }

    public ICommand SaveSettingsCommand => new Command(() =>
    {
        Preferences.Set("TopLevelFolder", TopLevelFolder);
        MessagingCenter.Send(this, "SettingsUpdated");
    });
}

跳过根目录加载

在主ViewModel的LoadRootNodes方法中,直接加载顶层文件夹下的患者文件夹,不显示根目录:

private async void LoadRootNodes()
{
    RootNodes.Clear();
    var topFolder = Preferences.Get("TopLevelFolder", "Root");
    // 直接获取顶层文件夹下的患者文件夹,跳过根目录显示
    var patientFolders = await _storageService.GetFoldersAsync(topFolder);
    foreach (var folder in patientFolders)
    {
        RootNodes.Add(new TreeNode 
        { 
            Name = folder, 
            Path = Path.Combine(topFolder, folder), 
            Type = NodeType.Patient 
        });
    }
}

内容的提问来源于stack exchange,提问作者Rad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 21:09:52