自定义文件夹选取功能

用于设置类型

csharp 复制代码
/// <summary>
/// 文件浏览器选择模式枚举
/// <para>控制用户在U盘文件浏览器中可以选择文件还是文件夹</para>
/// </summary>
public enum FileBrowserSelectionMode
{
    /// <summary>
    /// 仅选择文件(双击文件夹进入,单击文件选中)
    /// </summary>
    SelectFile = 0,

    /// <summary>
    /// 仅选择文件夹(双击进入文件夹,单击选中当前文件夹)
    /// </summary>
    SelectFolder = 1,

    /// <summary>
    /// 文件和文件夹均可选择
    /// </summary>
    SelectFileOrFolder = 2
}

文件夹及文件的内容信息

csharp 复制代码
 /// <summary>
 /// 文件系统项类型枚举,区分文件夹和文件
 /// </summary>
 public enum FileSystemItemType
 {
     /// <summary>文件夹</summary>
     Directory,

     /// <summary>文件</summary>
     File
 }

 /// <summary>
 /// 文件系统项信息模型类
 /// <para>封装文件/文件夹的名称、完整路径、类型、大小、修改时间等元数据</para>
 /// <para>用于U盘文件浏览器列表项的数据绑定</para>
 /// </summary>
 public class FileSystemItemInfo : IEquatable<FileSystemItemInfo>
 {
     /// <summary>
     /// 文件/文件夹名称(仅文件名,不含路径)
     /// </summary>
     public string Name { get; }

     /// <summary>
     /// 完整路径(绝对路径)
     /// </summary>
     public string FullPath { get; }

     /// <summary>
     /// 文件系统项类型(文件夹或文件)
     /// </summary>
     public FileSystemItemType ItemType { get; }

     /// <summary>
     /// 文件大小(字节),文件夹时为0
     /// </summary>
     public long SizeInBytes { get; }

     /// <summary>
     /// 最后修改时间
     /// </summary>
     public DateTime LastModified { get; }

     /// <summary>
     /// 文件扩展名(含点号,如".txt"),文件夹时为空字符串
     /// </summary>
     public string Extension { get; }

     /// <summary>
     /// 构造函数,根据文件系统项类型初始化元数据
     /// </summary>
     /// <param name="name">文件/文件夹名称</param>
     /// <param name="fullPath">完整路径</param>
     /// <param name="itemType">项类型</param>
     /// <param name="sizeInBytes">文件大小(字节)</param>
     /// <param name="lastModified">最后修改时间</param>
     /// <param name="extension">文件扩展名</param>
     public FileSystemItemInfo(
         string name,
         string fullPath,
         FileSystemItemType itemType,
         long sizeInBytes = 0,
         DateTime? lastModified = null,
         string extension = "")
     {
         Name = name ?? throw new ArgumentNullException(nameof(name));
         FullPath = fullPath ?? throw new ArgumentNullException(nameof(fullPath));
         ItemType = itemType;
         SizeInBytes = sizeInBytes;
         LastModified = lastModified ?? DateTime.MinValue;
         Extension = extension ?? string.Empty;
     }

     /// <summary>
     /// 从DirectoryInfo创建文件夹类型的FileSystemItemInfo实例
     /// </summary>
     /// <param name="dirInfo">目录信息对象</param>
     /// <returns>文件夹类型的FileSystemItemInfo实例</returns>
     public static FileSystemItemInfo FromDirectory(DirectoryInfo dirInfo)
     {
         if (dirInfo == null) throw new ArgumentNullException(nameof(dirInfo));

         return new FileSystemItemInfo(
             name: dirInfo.Name,
             fullPath: dirInfo.FullName,
             itemType: FileSystemItemType.Directory,
             lastModified: dirInfo.LastWriteTime);
     }

     /// <summary>
     /// 从FileInfo创建文件类型的FileSystemItemInfo实例
     /// </summary>
     /// <param name="fileInfo">文件信息对象</param>
     /// <returns>文件类型的FileSystemItemInfo实例</returns>
     public static FileSystemItemInfo FromFile(FileInfo fileInfo)
     {
         if (fileInfo == null) throw new ArgumentNullException(nameof(fileInfo));

         return new FileSystemItemInfo(
             name: fileInfo.Name,
             fullPath: fileInfo.FullName,
             itemType: FileSystemItemType.File,
             sizeInBytes: fileInfo.Length,
             lastModified: fileInfo.LastWriteTime,
             extension: fileInfo.Extension);
     }

     /// <summary>
     /// 判断是否与另一个FileSystemItemInfo相等(基于FullPath比较)
     /// </summary>
     public bool Equals(FileSystemItemInfo other)
     {
         if (other is null) return false;
         return string.Equals(FullPath, other.FullPath, StringComparison.OrdinalIgnoreCase);
     }

     /// <summary>
     /// 重写Equals方法,支持与任意对象比较
     /// </summary>
     public override bool Equals(object obj)
     {
         return Equals(obj as FileSystemItemInfo);
     }

     /// <summary>
     /// 重写GetHashCode,基于FullPath生成哈希值
     /// </summary>
     public override int GetHashCode()
     {
         return FullPath?.ToLowerInvariant().GetHashCode() ?? 0;
     }

     /// <summary>
     /// 重写ToString,返回名称
     /// </summary>
     public override string ToString()
     {
         return Name;
     }
 }

xaml内容显示模板

csharp 复制代码
    /// <summary>
    /// 文件系统项图标模板选择器
    /// <para>根据FileSystemItemInfo.ItemType选择对应的DataTemplate</para>
    /// <para>用于在XAML中根据项类型动态切换文件夹/文件图标</para>
    /// </summary>
    public class FileSystemItemTemplateSelector : DataTemplateSelector
    {
        /// <summary>
        /// 文件夹图标模板
        /// </summary>
        public DataTemplate FolderTemplate { get; set; }

        /// <summary>
        /// 文件图标模板
        /// </summary>
        public DataTemplate FileTemplate { get; set; }

        /// <summary>
        /// 根据数据项的ItemType选择对应的DataTemplate
        /// </summary>
        /// <param name="item">数据项,预期为FileSystemItemInfo类型</param>
        /// <param name="container">容器元素</param>
        /// <returns>匹配的DataTemplate,未匹配时返回null</returns>
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            if (item is FileSystemItemInfo fsItem)
            {
                return fsItem.ItemType switch
                {
                    FileSystemItemType.Directory => FolderTemplate,
                    FileSystemItemType.File => FileTemplate,
                    _ => base.SelectTemplate(item, container)
                };
            }

            return base.SelectTemplate(item, container);
        }
    }

u盘检测功能

csharp 复制代码
 /// <summary>
 /// U盘驱动器检测辅助类
 /// <para>通过系统DriveInfo API检测可移动驱动器(U盘),提供根路径获取和验证功能</para>
 /// <para>此类为静态工具类,无需实例化</para>
 /// </summary>
 public static class UsbDriveHelper
 {
     /// <summary>
     /// 检测系统中所有可移动驱动器(U盘)的根路径
     /// <para>仅返回已就绪(Ready)的可移动驱动器</para>
     /// </summary>
     /// <returns>可移动驱动器根路径列表(如 ["E:\", "F:\"]),无U盘时返回空列表</returns>
     public static List<string> GetUsbDriveRootPaths()
     {
         var usbRootPaths = new List<string>();

         try
         {
             /* 遍历系统所有逻辑驱动器,筛选可移动类型且已就绪的驱动器 */
             usbRootPaths = DriveInfo.GetDrives()
                 .Where(drive => drive.DriveType == DriveType.Removable && drive.IsReady)
                 .Select(drive => drive.RootDirectory.FullName)
                 .ToList();
         }
         catch (UnauthorizedAccessException)
         {
             /* 部分驱动器可能因权限不足无法访问,忽略此类异常 */
         }
         catch (Exception)
         {
             /* 其他异常同样忽略,确保方法不会抛出异常 */
         }

         return usbRootPaths;
     }

     /// <summary>
     /// 获取第一个检测到的U盘根路径
     /// <para>适用于系统中只插入一个U盘的常见场景</para>
     /// </summary>
     /// <returns>第一个U盘根路径(如 "E:\"),无U盘时返回null</returns>
     public static string GetFirstUsbDriveRootPath()
     {
         var paths = GetUsbDriveRootPaths();
         return paths.Count > 0 ? paths[0] : null;
     }

     /// <summary>
     /// 验证指定路径是否位于U盘(可移动驱动器)上
     /// <para>通过比较路径的根目录与系统中可移动驱动器根路径进行判断</para>
     /// </summary>
     /// <param name="path">待验证的路径(绝对路径或相对路径均可)</param>
     /// <returns>如果路径位于U盘上返回true,否则返回false</returns>
     public static bool IsPathOnUsbDrive(string path)
     {
         if (string.IsNullOrWhiteSpace(path)) return false;

         try
         {
             /* 获取路径的根目录部分,如 "E:\" */
             var pathRoot = Path.GetPathRoot(Path.GetFullPath(path));
             if (string.IsNullOrEmpty(pathRoot)) return false;

             var usbRoots = GetUsbDriveRootPaths();
             return usbRoots.Any(usbRoot =>
                 string.Equals(pathRoot, usbRoot, StringComparison.OrdinalIgnoreCase));
         }
         catch
         {
             return false;
         }
     }

     /// <summary>
     /// 验证指定路径是否为有效的U盘根路径
     /// <para>直接比较路径与系统中可移动驱动器根路径</para>
     /// </summary>
     /// <param name="rootPath">待验证的根路径(如 "E:\")</param>
     /// <returns>如果是有效的U盘根路径返回true,否则返回false</returns>
     public static string IsValidUsbRootPath(string rootPath)
     {
         // 空值直接返回 false
         if (string.IsNullOrWhiteSpace(rootPath))
             return null;

         // 获取所有 U 盘根目录
         var usbRoots = GetUsbDriveRootPaths();

         // 遍历每个 U 盘,检查:U盘根目录\rootPath 是否存在
         foreach (var usbRoot in usbRoots)
         {
             // 拼接完整路径:U 盘根目录 + 目标文件夹名
             string targetFolder = Path.Combine(usbRoot, rootPath);

             // 如果这个文件夹存在,直接返回 true
             if (Directory.Exists(targetFolder))
                 return targetFolder;
         }

         // 所有 U 盘都没有,返回 false
         return null;
     }
 }

界面代码

csharp 复制代码
    <UserControl.Resources>
        <!-- 文件夹图标模板 -->
        <DataTemplate x:Key="FolderIconTemplate">
            <materialDesign:PackIcon Kind="Folder" Width="28" Height="28"
                                      Foreground="#F0B94D" VerticalAlignment="Center"/>
        </DataTemplate>

        <!-- 文件图标模板 -->
        <DataTemplate x:Key="FileIconTemplate">
            <materialDesign:PackIcon Kind="FileDocumentOutline" Width="28" Height="28"
                                      Foreground="#607D8B" VerticalAlignment="Center"/>
        </DataTemplate>

        <!-- 根据ItemType选择图标的DataTemplate选择器 -->
        <dialogs:FileSystemItemTemplateSelector x:Key="IconTemplateSelector"
                                                 FolderTemplate="{StaticResource FolderIconTemplate}"
                                                 FileTemplate="{StaticResource FileIconTemplate}"/>

        <!-- 文件系统项的列表项模板 -->
        <DataTemplate DataType="{x:Type dialogs:FileSystemItemInfo}">
            <Grid Height="50" Margin="4,2">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="40"/>
                    <ColumnDefinition Width="500"/>
                    <ColumnDefinition Width="200"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>

                <!-- 图标区域 -->
                <ContentControl Grid.Column="0"
                                Content="{Binding}"
                                ContentTemplateSelector="{StaticResource IconTemplateSelector}"
                                VerticalAlignment="Center" HorizontalAlignment="Center"/>

                <!-- 文件/文件夹名称 -->
                <TextBlock Grid.Column="1"
                           Text="{Binding Name}"
                           VerticalAlignment="Center"
                           FontSize="16"
                           TextTrimming="CharacterEllipsis"
                           ToolTip="{Binding Name}"/>

                <!-- 文件大小(仅文件显示) -->
                <TextBlock Grid.Column="2"
                           VerticalAlignment="Center" 
                           FontSize="16" Foreground="#888"
                           Margin="0,0,10,0">
                    <TextBlock.Style>
                        <Style TargetType="TextBlock">
                            <Setter Property="Visibility" Value="Collapsed"/>
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding ItemType}" Value="{x:Static dialogs:FileSystemItemType.File}">
                                    <Setter Property="Visibility" Value="Visible"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </TextBlock.Style>
                    <TextBlock.Text>
                        <MultiBinding StringFormat="{}{0:F1} KB">
                            <Binding Path="SizeInBytes"/>
                        </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>

                <!-- 修改时间 -->
                <TextBlock Grid.Column="3"
                           Text="{Binding LastModified, StringFormat='{}{0:yyyy-MM-dd HH:mm}'}"
                           VerticalAlignment="Center"
                           FontSize="16" Foreground="#888"
                           Margin="0,0,0,0"/>
            </Grid>
        </DataTemplate>
    </UserControl.Resources>

    <!-- 主布局 -->
    <Grid Margin="16">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>   <!-- 标题栏 -->
            <RowDefinition Height="Auto"/>   <!-- 工具栏:路径 + 导航按钮 -->
            <RowDefinition Height="*"/>      <!-- 文件列表 -->
            <RowDefinition Height="Auto"/>   <!-- 状态栏 -->
            <RowDefinition Height="Auto"/>   <!-- 底部按钮 -->
        </Grid.RowDefinitions>

        <!-- Row 0: 标题栏 -->
        <Border Grid.Row="0" Background="White" CornerRadius="8,8,0,0" BorderBrush="#E0E0E0" BorderThickness="0,0,0,1" DevicePart:OverlayService.DragHandle="True">
            <Grid Margin="20,15">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>

                <!-- 标题 -->
                <TextBlock Grid.Column="0" x:Name="TitleText" 
                   Text="{Binding Title}"
                   FontSize="25" FontWeight="SemiBold"
                   VerticalAlignment="Center"
                   Foreground="#333333"/>

                <!-- 关闭按钮 -->
                <Button Grid.Column="2" x:Name="CloseButton"
                Width="50" Height="50"
                Background="Transparent" BorderThickness="0"
                Cursor="Hand" Command="{Binding CancelCommand}">
                    <Path Data="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" 
                  Fill="#666666" Width="30" Height="30" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </Button>
            </Grid>
        </Border>

        <!-- Row 1: 工具栏 - 当前路径 + 返回上一级 + 刷新 -->
        <Grid Grid.Row="1" Margin="0,0,0,8">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>

            <!-- 当前路径显示 -->
            <Border Grid.Column="0"
                    BorderBrush="#DDD" BorderThickness="1" CornerRadius="4"
                    Background="#FAFAFA" Padding="8,6">
                <TextBlock Text="{Binding CurrentPath}"
                           FontSize="18" Foreground="#555"
                           VerticalAlignment="Center"
                           TextTrimming="CharacterEllipsis"/>
            </Border>

            <!-- 返回上一级按钮 -->
            <Button Grid.Column="1"
                    Command="{Binding NavigateUpCommand}"
                    ToolTip="返回上一级"
                    Width="50" Height="50" Margin="20,0,0,0"
                    Style="{StaticResource MaterialDesignIconButton}">
                <materialDesign:PackIcon Kind="ArrowUp" Width="30" Height="40"/>
            </Button>

            <!-- 刷新按钮 -->
            <Button Grid.Column="2"
                    Command="{Binding RefreshCommand}"
                    ToolTip="刷新"
                    Width="50" Height="50" Margin="20,0,0,0"
                    Style="{StaticResource MaterialDesignIconButton}">
                <materialDesign:PackIcon Kind="Refresh" Width="30" Height="40"/>
            </Button>
        </Grid>

        <!-- Row 2: 文件列表 -->
        <Grid Grid.Row="2">
            <!-- U盘未检测到的提示 -->
            <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"
                        Visibility="{Binding IsUsbNotFound, Converter={StaticResource BooleanToVisibilityConverter}}">
                <materialDesign:PackIcon Kind="UsbFlashDriveOutline" Width="64" Height="64"
                                          Foreground="#BBB" HorizontalAlignment="Center" Margin="0,0,0,12"/>
                <TextBlock Text="未检测到U盘" FontSize="18" Foreground="#999"
                           HorizontalAlignment="Center" Margin="0,0,0,8"/>
                <TextBlock Text="{Binding StatusMessage}" FontSize="13" Foreground="#BBB"
                           HorizontalAlignment="Center" TextWrapping="Wrap" MaxWidth="400"/>
            </StackPanel>

            <!-- 文件列表(正常状态) -->
            <ListBox ItemsSource="{Binding Items}"
                     SelectedItem="{Binding SelectedItem}"
                     BorderBrush="#DDD" BorderThickness="1"
                     ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                     FontSize="18" d:ItemsSource="{d:SampleData ItemCount=45}">
                <ListBox.Style>
                    <Style TargetType="ListBox">
                        <Setter Property="Visibility" Value="Visible"/>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding IsUsbNotFound}" Value="True">
                                <Setter Property="Visibility" Value="Collapsed"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ListBox.Style>
                <ListBox.ItemContainerStyle>
                    <Style TargetType="ListBoxItem" BasedOn="{StaticResource MaterialDesignListBoxItem}">
                        <Setter Property="Padding" Value="0"/>
                        <EventSetter Event="MouseDoubleClick" Handler="Item_MouseDoubleClick"/>
                    </Style>
                </ListBox.ItemContainerStyle>
            </ListBox>

            <!-- 空目录提示 -->
            <TextBlock Text="此文件夹为空"
                       FontSize="18" Foreground="#999"
                       HorizontalAlignment="Center" VerticalAlignment="Center">
                <TextBlock.Style>
                    <Style TargetType="TextBlock">
                        <Setter Property="Visibility" Value="Collapsed"/>
                        <Style.Triggers>
                            <MultiDataTrigger>
                                <MultiDataTrigger.Conditions>
                                    <Condition Binding="{Binding IsUsbNotFound}" Value="False"/>
                                    <Condition Binding="{Binding Items.Count}" Value="0"/>
                                </MultiDataTrigger.Conditions>
                                <Setter Property="Visibility" Value="Visible"/>
                            </MultiDataTrigger>
                        </Style.Triggers>
                    </Style>
                </TextBlock.Style>
            </TextBlock>
        </Grid>

        <!-- Row 3: 状态栏 -->
        <TextBlock Grid.Row="3"
                   Text="{Binding StatusMessage}"
                   FontSize="12" Foreground="#E53935"
                   Margin="0,4,0,4"
                   TextWrapping="Wrap" MaxHeight="40">
            <TextBlock.Style>
                <Style TargetType="TextBlock">
                    <Setter Property="Visibility" Value="Collapsed"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding StatusMessage}" Value="{x:Null}">
                            <Setter Property="Visibility" Value="Collapsed"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBlock.Style>
        </TextBlock>

        <!-- Row 4: 底部按钮 -->
        <StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">

            <!-- 确定按钮 -->
            <Button Content="确定"
                    Command="{Binding ConfirmCommand}"
                    Style="{StaticResource ActionButtonStyle}"
                    Width="100" Height="50" Margin="0,0,8,0"/>

            <!-- 取消按钮 -->
            <Button Content="取消"
                    Command="{Binding CancelCommand}"
                    Style="{StaticResource ActionButtonStyle}"
                    Background="#C9CACA"
                    Foreground="#898989"
                    Width="100" Height="50"/>
        </StackPanel>
    </Grid>
</UserControl>

xaml.cs

csharp 复制代码
    /// <summary>
    /// UsbFileBrowserDialog 的代码隐藏类
    /// <para>主要负责处理双击事件的转发,将ListBoxItem的双击事件映射到ViewModel的命令</para>
    /// <para>由于WPF ListBox的MouseDoubleClick是直接路由事件,无法直接绑定Command</para>
    /// <para>因此在code-behind中通过EventSetter转发到ViewModel的ItemDoubleClickCommand</para>
    /// </summary>
    public partial class UsbFileBrowserDialog : UserControl
    {
        /// <summary>
        /// 构造函数,初始化XAML组件
        /// </summary>
        public UsbFileBrowserDialog()
        {
            InitializeComponent();
        }

        /// <summary>
        /// ListBox项的双击事件处理
        /// <para>从事件源向上查找ListBoxItem,获取其绑定的FileSystemItemInfo</para>
        /// <para>然后转发到ViewModel的ItemDoubleClickCommand执行</para>
        /// </summary>
        /// <param name="sender">事件源</param>
        /// <param name="e">鼠标按钮事件参数</param>
        private void Item_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            /* 从事件源向上查找ListBoxItem */
            if (sender is not ListBoxItem listBoxItem)
                return;

            /* 获取绑定的数据项 */
            if (listBoxItem.Content is not FileSystemItemInfo item)
                return;

            /* 转发到ViewModel的ItemDoubleClickCommand */
            if (DataContext is UsbFileBrowserViewModel viewModel
                && viewModel.ItemDoubleClickCommand.CanExecute(item))
            {
                viewModel.ItemDoubleClickCommand.Execute(item);
            }
        }
    }

xaml.cs.model

csharp 复制代码
/// <summary>
/// U盘文件浏览器对话框ViewModel
/// <para>负责管理U盘内文件/文件夹的浏览、导航和选择逻辑</para>
/// <para>根目录限定为U盘路径,不允许用户导航至其他驱动器</para>
/// <para>支持选择文件、文件夹或两者均可,由FileBrowserSelectionMode控制</para>
/// </summary>
public class UsbFileBrowserViewModel : DialogBase
{
    #region 私有字段

    /// <summary>U盘根路径(浏览的起始和上限边界)</summary>
    private string _usbRootPath;

    /// <summary>当前浏览路径</summary>
    private string _currentPath;

    /// <summary>当前选中的文件系统项</summary>
    private FileSystemItemInfo _selectedItem;

    /// <summary>文件浏览器选择模式</summary>
    private FileBrowserSelectionMode _selectionMode;

    /// <summary>文件扩展名过滤器(如".txt;.csv"),为空则显示所有文件</summary>
    private string _filter;

    /// <summary>是否可以返回上一级(当前路径不在U盘根目录时为true)</summary>
    private bool _canNavigateUp;

    /// <summary>状态提示信息</summary>
    private string _statusMessage;

    /// <summary>是否没有检测到U盘</summary>
    private bool _isUsbNotFound;

    /// <summary>
    /// 遮罩层ID,由调用方在 OverlayService.Show 后赋值
    /// 用于关闭遮罩层时标识具体的遮罩层实例
    /// </summary>
    private string _overlayId;

    #endregion

    #region 事件

    /// <summary>
    /// 对话框结果请求事件
    /// <para>当通过 OverlayService 模式显示时,ViewModel 通过此事件通知调用方对话框的结果</para>
    /// <para>调用方订阅此事件以获取用户的选择结果(确定/取消)</para>
    /// </summary>
    public event Action<IDialogResult> DialogResultRequested;

    #endregion

    #region 属性

    /// <summary>
    /// 当前浏览路径(显示在地址栏中)
    /// </summary>
    public string CurrentPath
    {
        get => _currentPath;
        set
        {
            if (SetProperty(ref _currentPath, value))
            {
                /* 路径变化时重新计算是否可返回上一级 */
                CanNavigateUp = !string.IsNullOrEmpty(value)
                    && !string.Equals(value, _usbRootPath, StringComparison.OrdinalIgnoreCase);
            }
        }
    }

    /// <summary>
    /// 当前选中的文件系统项
    /// </summary>
    public FileSystemItemInfo SelectedItem
    {
        get => _selectedItem;
        set
        {
            if (SetProperty(ref _selectedItem, value))
            {
                /* 选中项变化时刷新确定按钮的可用状态 */
                ConfirmCommand.RaiseCanExecuteChanged();
            }
        }
    }

    /// <summary>
    /// 文件浏览器选择模式
    /// <para>SelectFile: 仅选择文件</para>
    /// <para>SelectFolder: 仅选择文件夹</para>
    /// <para>SelectFileOrFolder: 文件和文件夹均可选择</para>
    /// </summary>
    public FileBrowserSelectionMode SelectionMode
    {
        get => _selectionMode;
        private set => SetProperty(ref _selectionMode, value);
    }

    /// <summary>
    /// 文件扩展名过滤器(如".txt;.csv"),为空则显示所有文件
    /// </summary>
    public string Filter
    {
        get => _filter;
        private set => SetProperty(ref _filter, value);
    }

    /// <summary>
    /// 是否可以返回上一级目录
    /// <para>当前路径等于U盘根路径时不可返回</para>
    /// </summary>
    public bool CanNavigateUp
    {
        get => _canNavigateUp;
        private set
        {
            if (SetProperty(ref _canNavigateUp, value))
            {
                NavigateUpCommand.RaiseCanExecuteChanged();
            }
        }
    }

    /// <summary>
    /// 状态提示信息(如错误提示、U盘未检测到等)
    /// </summary>
    public string StatusMessage
    {
        get => _statusMessage;
        private set => SetProperty(ref _statusMessage, value);
    }

    /// <summary>
    /// 是否没有检测到U盘
    /// </summary>
    public bool IsUsbNotFound
    {
        get => _isUsbNotFound;
        private set => SetProperty(ref _isUsbNotFound, value);
    }

    /// <summary>
    /// 遮罩层ID,由调用方在 OverlayService.Show 后赋值
    /// <para>用于在对话框关闭时通过 OverlayService.Close(overlayId) 关闭对应的遮罩层</para>
    /// <para>如果此值为空,说明是通过 Prism IDialogService 显示的,走 RequestClose 逻辑</para>
    /// </summary>
    public string OverlayId
    {
        get => _overlayId;
        set => _overlayId = value;
    }

    /// <summary>
    /// 当前目录下的文件系统项集合(文件夹在前,文件在后)
    /// </summary>
    public ObservableCollection<FileSystemItemInfo> Items { get; }

    #endregion

    #region 命令

    /// <summary>
    /// 返回上一级目录命令
    /// </summary>
    public DelegateCommand NavigateUpCommand { get; }

    /// <summary>
    /// 刷新当前目录命令
    /// </summary>
    public DelegateCommand RefreshCommand { get; }

    /// <summary>
    /// 确定选择命令(返回选中的文件/文件夹路径)
    /// </summary>
    public DelegateCommand ConfirmCommand { get; }

    /// <summary>
    /// 取消选择命令(关闭对话框,不返回任何结果)
    /// </summary>
    public DelegateCommand CancelCommand { get; }

    /// <summary>
    /// 双击项命令(进入文件夹或选择文件)
    /// </summary>
    public DelegateCommand<FileSystemItemInfo> ItemDoubleClickCommand { get; }

    #endregion

    #region 构造函数

    /// <summary>
    /// 构造函数,初始化命令和集合
    /// </summary>
    public UsbFileBrowserViewModel()
    {
        Items = new ObservableCollection<FileSystemItemInfo>();

        /* 初始化各命令,绑定执行方法和可用性判断 */
        NavigateUpCommand = new DelegateCommand(ExecuteNavigateUp, CanExecuteNavigateUp);
        RefreshCommand = new DelegateCommand(ExecuteRefresh);
        ConfirmCommand = new DelegateCommand(ExecuteConfirm, CanExecuteConfirm);
        CancelCommand = new DelegateCommand(ExecuteCancel);
        ItemDoubleClickCommand = new DelegateCommand<FileSystemItemInfo>(ExecuteItemDoubleClick);
    }

    #endregion

    #region IDialogAware 接口实现

    /// <summary>
    /// 对话框打开时的初始化操作
    /// <para>从参数中读取选择模式、过滤器和指定U盘路径</para>
    /// <para>自动检测U盘并加载根目录内容</para>
    /// </summary>
    /// <param name="parameters">对话框传入参数,支持以下Key:</param>
    /// <para>  "SelectionMode" - FileBrowserSelectionMode枚举值,默认SelectFile</para>
    /// <para>  "Filter" - 文件扩展名过滤器字符串,如".txt;.csv"</para>
    /// <para>  "UsbRootPath" - 指定U盘根路径(可选,不传则自动检测)</para>
    public override void OnDialogOpened(IDialogParameters parameters)
    {
        base.OnDialogOpened(parameters);

        /* 从参数中提取选择模式,默认为选择文件 */
        SelectionMode = parameters?.ContainsKey("SelectionMode") == true
            ? parameters.GetValue<FileBrowserSelectionMode>("SelectionMode")
            : FileBrowserSelectionMode.SelectFile;

        /* 从参数中提取文件扩展名过滤器 */
        Filter = parameters?.ContainsKey("Filter") == true
            ? parameters.GetValue<string>("Filter")
            : string.Empty;

        /* 从参数中提取指定的U盘根路径(可选) */
        var specifiedRoot = parameters?.ContainsKey("UsbRootPath") == true
            ? parameters.GetValue<string>("UsbRootPath")
            : null;

        /* 确定U盘根路径:优先使用指定路径,否则自动检测 */
        if (!string.IsNullOrWhiteSpace(specifiedRoot))
        {
            string path = UsbDriveHelper.IsValidUsbRootPath(specifiedRoot);
            if (path == null)
            {
                _usbRootPath = UsbDriveHelper.GetFirstUsbDriveRootPath();
            }
            else
            {
                _usbRootPath = path;
            }    
        }

        /* 检查是否成功获取U盘根路径 */
        if (string.IsNullOrEmpty(_usbRootPath))
        {
            IsUsbNotFound = true;
            StatusMessage = "未检测到U盘,请插入U盘后重试";
            return;
        }

        // 选择模式提示
        if (SelectionMode == FileBrowserSelectionMode.SelectFile)
        {
            Title = "请选择文件";
        }
        else if (SelectionMode == FileBrowserSelectionMode.SelectFolder)
        {
            Title = "请选择文件夹";
        }
        else if (SelectionMode == FileBrowserSelectionMode.SelectFileOrFolder)
        {
            Title = "请选择文件或文件夹";
        }
        else
        {
            Title = "";
        }

            IsUsbNotFound = false;

        /* 加载U盘根目录内容 */
        LoadDirectoryContents(_usbRootPath);
    }

    /// <summary>
    /// 对话框关闭时的清理操作
    /// </summary>
    public override void OnDialogClosed()
    {
        /* 清空文件系统项集合,释放资源引用 */
        Items.Clear();
        SelectedItem = null;
        base.OnDialogClosed();
    }

    #endregion

    #region 命令实现

    /// <summary>
    /// 执行返回上一级目录
    /// <para>导航到当前目录的父目录,但不能超出U盘根目录</para>
    /// </summary>
    private void ExecuteNavigateUp()
    {
        if (string.IsNullOrEmpty(CurrentPath) || string.IsNullOrEmpty(_usbRootPath))
            return;

        try
        {
            var parentPath = Path.GetDirectoryName(CurrentPath);

            /* 确保父目录不超出U盘根目录边界 */
            if (parentPath != null
                && parentPath.StartsWith(_usbRootPath, StringComparison.OrdinalIgnoreCase))
            {
                LoadDirectoryContents(parentPath);
            }
            else if (parentPath != null
                && string.Equals(parentPath.TrimEnd(Path.DirectorySeparatorChar),
                    _usbRootPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase))
            {
                /* 父目录恰好是U盘根目录的情况 */
                LoadDirectoryContents(_usbRootPath);
            }
        }
        catch (Exception ex)
        {
            StatusMessage = $"返回上一级失败:{ex.Message}";
        }
    }

    /// <summary>
    /// 判断是否可以执行返回上一级
    /// </summary>
    private bool CanExecuteNavigateUp()
    {
        return CanNavigateUp;
    }

    /// <summary>
    /// 执行刷新当前目录
    /// </summary>
    private void ExecuteRefresh()
    {
        if (!string.IsNullOrEmpty(CurrentPath))
        {
            LoadDirectoryContents(CurrentPath);
        }
    }

    /// <summary>
    /// 执行确定选择
    /// <para>根据选择模式验证选中项,通过DialogResult返回选中的路径</para>
    /// </summary>
    /// <summary>
    /// 执行确定选择,返回选中项的路径
    /// <para>如果通过 OverlayService 模式显示(OverlayId不为空),则通过 DialogResultRequested 事件通知调用方</para>
    /// <para>否则通过 Prism IDialogService 的 RequestClose 机制关闭对话框</para>
    /// </summary>
    private void ExecuteConfirm()
    {
        if (SelectedItem == null)
            return;

        /* 构建返回参数,包含选中项的完整路径 */
        var resultParameters = new DialogParameters
        {
            { "SelectedPath", SelectedItem.FullPath },
            { "SelectedItem", SelectedItem }
        };

        var dialogResult = new DialogResult(ButtonResult.OK, resultParameters);

        /* 如果通过 OverlayService 显示,则通过事件通知调用方并关闭遮罩层 */
        if (!string.IsNullOrEmpty(_overlayId))
        {
            DialogResultRequested?.Invoke(dialogResult);
        }
        else
        {
            /* 通过 Prism IDialogService 显示时,走标准 RequestClose 机制 */
            CloseDialog(dialogResult);
        }
    }

    /// <summary>
    /// 判断是否可以执行确定选择
    /// <para>根据选择模式验证选中项的类型是否匹配</para>
    /// </summary>
    private bool CanExecuteConfirm()
    {
        if (SelectedItem == null)
            return false;

        return SelectionMode switch
        {
            FileBrowserSelectionMode.SelectFile => SelectedItem.ItemType == FileSystemItemType.File,
            FileBrowserSelectionMode.SelectFolder => SelectedItem.ItemType == FileSystemItemType.Directory,
            FileBrowserSelectionMode.SelectFileOrFolder => true,
            _ => false
        };
    }

    /// <summary>
    /// 执行取消选择,关闭对话框
    /// <para>如果通过 OverlayService 模式显示(OverlayId不为空),则通过 DialogResultRequested 事件通知调用方</para>
    /// <para>否则通过 Prism IDialogService 的 RequestClose 机制关闭对话框</para>
    /// </summary>
    private void ExecuteCancel()
    {
        var dialogResult = new DialogResult(ButtonResult.Cancel);

        /* 如果通过 OverlayService 显示,则通过事件通知调用方并关闭遮罩层 */
        if (!string.IsNullOrEmpty(_overlayId))
        {
            DialogResultRequested?.Invoke(dialogResult);
        }
        else
        {
            /* 通过 Prism IDialogService 显示时,走标准 RequestClose 机制 */
            CloseDialog(dialogResult);
        }
    }

    /// <summary>
    /// 执行双击项操作
    /// <para>双击文件夹:进入该文件夹</para>
    /// <para>双击文件:直接选中该文件并确定(仅在SelectFile或SelectFileOrFolder模式下)</para>
    /// </summary>
    /// <param name="item">双击的文件系统项</param>
    private void ExecuteItemDoubleClick(FileSystemItemInfo item)
    {
        if (item == null)
            return;

        if (item.ItemType == FileSystemItemType.Directory)
        {
            /* 双击文件夹:导航进入 */
            LoadDirectoryContents(item.FullPath);
        }
        else if (item.ItemType == FileSystemItemType.File
            && (SelectionMode == FileBrowserSelectionMode.SelectFile
                || SelectionMode == FileBrowserSelectionMode.SelectFileOrFolder))
        {
            /* 双击文件:直接选中并确认 */
            SelectedItem = item;
            ExecuteConfirm();
        }
    }

    #endregion

    #region 私有方法

    /// <summary>
    /// 加载指定目录的内容到Items集合
    /// <para>文件夹排列在前,文件排列在后</para>
    /// <para>根据Filter过滤文件扩展名</para>
    /// <para>发生异常时显示状态信息而不抛出</para>
    /// </summary>
    /// <param name="path">要加载的目录路径</param>
    private void LoadDirectoryContents(string path)
    {
        try
        {
            /* 安全检查:确保路径在U盘范围内 */
            if (!path.StartsWith(_usbRootPath, StringComparison.OrdinalIgnoreCase)
                && !string.Equals(path.TrimEnd(Path.DirectorySeparatorChar),
                    _usbRootPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase))
            {
                StatusMessage = "不允许访问U盘范围外的路径";
                return;
            }

            if (!Directory.Exists(path))
            {
                StatusMessage = $"目录不存在:{path}";
                return;
            }

            var dirInfo = new DirectoryInfo(path);
            Items.Clear();
            StatusMessage = string.Empty;

            /* 加载子文件夹(优先显示) */
            foreach (var dir in dirInfo.EnumerateDirectories()
                .OrderBy(d => d.Name, StringComparer.OrdinalIgnoreCase))
            {
                try
                {
                    /* 跳过无权限访问的目录(如系统隐藏目录) */
                    Items.Add(FileSystemItemInfo.FromDirectory(dir));
                }
                catch (UnauthorizedAccessException)
                {
                    /* 忽略无权限的目录 */
                }
            }

            /* 加载文件(根据过滤器筛选) */
            var files = dirInfo.EnumerateFiles();
            if (!string.IsNullOrWhiteSpace(Filter))
            {
                /* 解析过滤器字符串,支持分号分隔的多个扩展名 */
                var extensions = Filter.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
                    .Select(ext => ext.Trim().ToLowerInvariant())
                    .ToHashSet();

                files = files.Where(f => extensions.Contains(f.Extension.ToLowerInvariant()));
            }

            foreach (var file in files.OrderBy(f => f.Name, StringComparer.OrdinalIgnoreCase))
            {
                Items.Add(FileSystemItemInfo.FromFile(file));
            }

            /* 更新当前路径 */
            CurrentPath = path;

            /* 清除选中项 */
            SelectedItem = null;
        }
        catch (UnauthorizedAccessException)
        {
            StatusMessage = "没有权限访问该目录";
        }
        catch (IOException ioEx)
        {
            StatusMessage = $"读取目录失败:{ioEx.Message}";
        }
        catch (Exception ex)
        {
            StatusMessage = $"加载目录内容失败:{ex.Message}";
        }
    }

    #endregion
}

样例图

相关推荐
思麟呀5 小时前
C++工业级日志项目(六)异步日志器
linux·c++·windows
PAK向日葵5 小时前
从零实现 Python 虚拟机(二):S.A.A.U.S.O 的总体架构设计
c++·python
无限进步_5 小时前
【C++】weak_ptr、循环引用与线程安全
开发语言·数据结构·c++·算法·安全
咩咦6 小时前
C++学习笔记30:友元类、内部类和封装
c++·学习笔记·类和对象·封装·内部类·友元类·friend
黄小白的进阶之路6 小时前
C++提高编程---3.6 STL-常用容器-queue 容器【P213~P214】
c++
ID_180079054736 小时前
小红书评论 API 接口详解与实战开发
java·jvm·c++
khalil10207 小时前
代码随想录算法训练营Day-58 图论08 | 拓扑排序精讲、dijkstra(朴素版)精讲
c++·算法·图论·dijkstra·拓扑排序·prim·最短距离
丘山望岳8 小时前
藤萝垂序——二叉搜索树
开发语言·数据结构·c++
光电笑映8 小时前
深入理解 ELF:从目标文件到程序加载的全过程
linux·运维·服务器·c++