当前博文未提及该问题,以下是基于C知道的知识内容为您回答。
针对您在 .NET Framework 4.5.2 环境下,开发 WinForm 应用程序时遇到的需求:**实现本地文件到公共盘(网络共享路径)的批量上传、无需按钮触发、支持任务取消、单文件超时重试、任务执行互斥以及完善的错误处理**,以下提供一个基于 `Task` 和 `CancellationToken` 的高效解决方案。该方案设计为后台服务模式,可直接集成到现有的 WinForm 程序中。
### 1. 方案核心设计思路
为了满足“简洁高效”且“互斥”的要求,我们将上传逻辑封装在一个独立的服务类中,利用 `lock` 关键字确保任务互斥,利用 `CancellationTokenSource` 实现全局取消和单文件超时控制。
下表展示了核心需求与技术实现的映射关系:
| 需求描述 | 技术实现方案 | 关键类/方法 |
| :--- | :--- | :--- |
| **WinForm 集成** | 事件驱动模式,后台服务通过事件通知 UI 更新进度和日志。 | `EventHandler`, `Control.Invoke` [ref_2] |
| **无按钮触发** | 在窗体加载(`Load` 事件)或定时器回调中直接调用服务启动方法。 | `async/await` [ref_4] |
| **任务互斥** | 使用私有锁对象和布尔标志位,防止并发启动多个上传实例。 | `lock`, `bool` |
| **上传到公共盘** | 使用 .NET IO 类进行文件复制,支持 UNC 路径(如 `\\Server\Share`)。 | `System.IO.File.Copy` |
| **可取消任务** | 通过 `CancellationTokenSource` 向正在运行的 Task 发送取消信号。 | `CancellationTokenSource.Cancel()` [ref_5] |
| **单文件超时** | 为每个文件创建独立的 `CancellationTokenSource` 并设置超时时间。 | `CancellationTokenSource.CancelAfter()` |
| **超时重试机制** | 捕获超时异常,在 `for` 循环中控制重试次数,超过最大次数则放弃。 | `try-catch`, `Task.Delay` |
| **错误处理** | 区分用户取消、超时、IO错误和权限错误,分别处理并记录日志。 | `catch (SpecificException)` |
### 2. 核心上传服务类实现
创建一个名为 `FileUploadService` 的类,该类负责所有的后台逻辑、重试策略和状态管理。
```csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace WinFormApp.Services
{
public class FileUploadService
{
private readonly object _syncRoot = new object();
private bool _isRunning = false;
private CancellationTokenSource _globalCts;
// 定义进度更新事件参数
public class UploadProgressEventArgs : EventArgs
{
public string Message { get; set; }
public int TotalFiles { get; set; }
public int ProcessedFiles { get; set; }
public int SuccessCount { get; set; }
public int FailedCount { get; set; }
}
// 定义日志事件参数
public class LogEventArgs : EventArgs
{
public string LogText { get; set; }
public bool IsError { get; set; }
}
// 声明事件,用于通知UI层 [ref_2]
public event EventHandler<UploadProgressEventArgs> ProgressChanged;
public event EventHandler<LogEventArgs> Log;
/// <summary>
/// 启动上传任务
/// </summary>
public async Task StartUploadAsync(string sourcePath, string targetPath, int maxRetries = 3, int timeoutMs = 5000)
{
// 1. 任务互斥检查:确保同一时间只有一个任务在运行 [ref_5]
lock (_syncRoot)
{
if (_isRunning)
{
OnLog("任务正在执行中,无法重复启动。", true);
return; // 或者 throw new InvalidOperationException("任务正在运行");
}
_isRunning = true;
_globalCts = new CancellationTokenSource();
}
try
{
OnLog("开始初始化上传任务...", false);
// 检查目录有效性
if (!Directory.Exists(sourcePath))
{
OnLog($"源路径不存在: {sourcePath}", true);
return;
}
// 确保目标目录存在
if (!Directory.Exists(targetPath))
{
try { Directory.CreateDirectory(targetPath); }
catch (Exception ex) { OnLog($"创建目标目录失败: {ex.Message}", true); return; }
}
// 获取文件列表 (使用 EnumerateFiles 提升大量文件时的性能) [ref_1]
var files = new DirectoryInfo(sourcePath).EnumerateFiles("*.*", SearchOption.TopDirectoryOnly).ToList();
int total = files.Count;
int processed = 0;
int success = 0;
int failed = 0;
OnLog($"扫描完成,共发现 {total} 个文件。", false);
OnProgress("准备上传...", total, processed, success, failed);
// 2. 遍历文件进行上传
foreach (var file in files)
{
// 检查全局取消请求
_globalCts.Token.ThrowIfCancellationRequested();
processed++;
bool isFileSuccess = false;
// 3. 单文件重试循环逻辑
for (int retry = 0; retry <= maxRetries; retry++)
{
// 如果在重试等待期间用户取消了任务
if (_globalCts.Token.IsCancellationRequested) break;
try
{
OnLog($"正在上传 [{retry + 1}/{maxRetries + 1}]: {file.Name}", false);
// 执行带超时的上传操作
await UploadFileAsync(file, targetPath, timeoutMs, _globalCts.Token).ConfigureAwait(false);
isFileSuccess = true;
success++;
OnLog($"上传成功: {file.Name}", false);
break; // 成功则跳出重试循环
}
catch (OperationCanceledException) when (!_globalCts.Token.IsCancellationRequested)
{
// 捕获超时导致的取消异常
OnLog($"文件 {file.Name} 上传超时。", true);
}
catch (OperationCanceledException)
{
// 捕获用户主动取消的异常,直接向上抛出以结束整个任务
throw;
}
catch (Exception ex)
{
// 捕获其他IO错误(如网络断开、文件占用)
OnLog($"上传出错: {ex.Message}", true);
}
// 如果失败且未达到最大重试次数,延迟一段时间后重试
if (!isFileSuccess && retry < maxRetries)
{
await Task.Delay(2000, _globalCts.Token).ConfigureAwait(false); // 延迟2秒
}
}
if (!isFileSuccess) failed++;
// 更新进度
OnProgress($"正在处理: {file.Name}", total, processed, success, failed);
}
OnLog("所有文件处理完成。", false);
OnProgress("任务完成", total, processed, success, failed);
}
catch (OperationCanceledException)
{
OnLog("任务已被用户取消。", false);
}
catch (Exception ex)
{
OnLog($"任务发生严重错误: {ex.Message}", true);
}
finally
{
// 4. 清理状态,释放锁
lock (_syncRoot)
{
_isRunning = false;
_globalCts?.Dispose();
_globalCts = null;
}
}
}
/// <summary>
/// 取消任务
/// </summary>
public void Cancel()
{
lock (_syncRoot)
{
if (_isRunning && _globalCts != null)
{
_globalCts.Cancel();
OnLog("正在取消任务...", false);
}
}
}
/// <summary>
/// 单个文件上传(带超时控制)
/// </summary>
private async Task UploadFileAsync(FileInfo file, string targetDir, int timeoutMs, CancellationToken globalToken)
{
// 创建链接的 TokenSource,同时响应全局取消和单文件超时
using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(globalToken))
{
linkedCts.CancelAfter(timeoutMs); // 设置单文件超时时间
string destFilePath = Path.Combine(targetDir, file.Name);
// 使用 Task.Run 将同步的 IO 操作转为异步,避免阻塞
await Task.Run(() =>
{
linkedCts.Token.ThrowIfCancellationRequested(); // 检查取消
file.CopyTo(destFilePath, true); // 执行覆盖复制
}, linkedCts.Token).ConfigureAwait(false);
}
}
// 触发事件辅助方法
private void OnProgress(string msg, int total, int processed, int success, int failed)
{
ProgressChanged?.Invoke(this, new UploadProgressEventArgs
{
Message = msg,
TotalFiles = total,
ProcessedFiles = processed,
SuccessCount = success,
FailedCount = failed
});
}
private void OnLog(string text, bool isError)
{
Log?.Invoke(this, new LogEventArgs { LogText = text, IsError = isError });
}
}
}
```
### 3. WinForm 现有程序集成代码
在您的主窗体(`MainForm.cs` 或其他窗体)中,按照以下方式集成上述服务。此处演示如何在窗体加载时自动触发上传,以及如何处理 UI 更新。
```csharp
using System;
using System.Windows.Forms;
using WinFormApp.Services; // 引用服务命名空间
namespace WinFormApp
{
public partial class MainForm : Form
{
private readonly FileUploadService _uploadService;
public MainForm()
{
InitializeComponent();
_uploadService = new FileUploadService();
// 订阅服务事件
_uploadService.ProgressChanged += UploadService_ProgressChanged;
_uploadService.Log += UploadService_Log;
// 示例:窗体加载时自动开始(不用按钮触发) [ref_3]
this.Load += MainForm_Load;
}
private async void MainForm_Load(object sender, EventArgs e)
{
// 配置路径
string sourceDir = @"C:\LocalData"; // 本地路径
string targetDir = @"\\192.168.1.100\PublicShare"; // 公共盘UNC路径
// 开始异步上传
await _uploadService.StartUploadAsync(sourceDir, targetDir, maxRetries: 3, timeoutMs: 5000);
}
// 线程安全的 UI 更新:进度条
private void UploadService_ProgressChanged(object sender, FileUploadService.UploadProgressEventArgs e)
{
if (this.InvokeRequired)
{
this.Invoke(new Action(() => UploadService_ProgressChanged(sender, e)));
return;
}
// 假设窗体上有这些控件
if (progressBar1 != null)
{
progressBar1.Maximum = e.TotalFiles > 0 ? e.TotalFiles : 1;
progressBar1.Value = e.ProcessedFiles;
}
if (lblStatus != null)
{
lblStatus.Text = $"{e.Message} | 总数: {e.TotalFiles}, 成功: {e.SuccessCount}, 失败: {e.FailedCount}";
}
}
// 线程安全的 UI 更新:日志
private void UploadService_Log(object sender, FileUploadService.LogEventArgs e)
{
if (this.InvokeRequired)
{
this.Invoke(new Action(() => UploadService_Log(sender, e)));
return;
}
if (txtLog != null)
{
string color = e.IsError ? "red" : "green";
txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {e.LogText}\r\n");
// 如果是富文本框,还可以根据 IsError 设置颜色
}
}
// 如果需要手动取消(例如窗体关闭时)
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
_uploadService.Cancel();
}
}
}
```
### 4. 关键技术点解析
1. **任务互斥性**:
在 `StartUploadAsync` 方法开头,使用 `lock (_syncRoot)` 检查 `_isRunning` 标志。这确保了即使代码被多次调用(例如用户快速双击或多个事件触发),同一时刻也只有一个上传任务在后台运行,防止了文件冲突和资源竞争 [ref_5]。
2. **超时与重试机制**:
`UploadFileAsync` 方法中使用了 `CancellationTokenSource.CreateLinkedTokenSource`。这是实现“单文件超时”的关键。它将“全局取消令牌”和“单文件超时令牌”绑定在一起。如果文件复制超过 `timeoutMs` 毫秒,`linkedCts` 会触发取消,抛出 `OperationCanceledException`。外层的 `for` 循环捕获此异常后,会判断是否继续重试,直到达到 `maxRetries` 次数。
3. **无按钮触发与后台运行**:
通过在 `MainForm_Load` 事件中使用 `await` 调用服务,实现了程序启动即自动执行的需求。由于使用了 `Task.Run` (在服务内部) 和 `ConfigureAwait(false)`,繁重的 IO 操作不会阻塞 UI 线程,保证了 WinForm 界面的流畅响应 [ref_4]。
4. **错误处理**:
代码中细致地区分了 `OperationCanceledException`(超时或用户取消)、`IOException`(网络问题)等。对于权限错误等无法通过重试解决的问题,代码会直接标记失败,避免无限死循环。
5. **高效文件遍历**:
使用 `DirectoryInfo.EnumerateFiles` 而非 `GetFiles`。前者在 .NET 中是延迟执行的,意味着在开始遍历时不需要一次性将所有文件信息加载到内存,这在处理“很多文件”时能显著降低内存消耗 [ref_1]。