用于设置类型
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
}
样例图
