凌峰创科服务平台

WinForm如何实现文件上传到服务器?

  • 后端: ASP.NET Core Web API,它轻量、高性能且跨平台。
  • 前端: WinForms + HttpClient,这是 .NET 中进行 HTTP 通信的标准方式。

第一步:创建服务器端 (ASP.NET Core Web API)

服务器端将提供一个 API 端点,用于接收 HTTP POST 请求,并处理上传的文件。

WinForm如何实现文件上传到服务器?-图1
(图片来源网络,侵删)

创建项目

打开 Visual Studio,创建一个新的项目,选择 "ASP.NET Core Web API" 模板。

配置服务和中间件

Program.cs 文件中,我们需要配置文件上传的大小限制,默认限制很小(约 28MB),我们需要根据需求进行调整。

// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 1. 添加服务到容器。
// 允许将大文件上传到最多 1 GB
builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = 1073741824; // 1 GB
    options.MultipartHeadersLengthLimit = 1024 * 1024; // 1 MB for headers
});
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// 2. 配置 HTTP 请求管道。
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

创建上传控制器

创建一个新的控制器,UploadController.cs,并添加一个用于处理文件上传的 Action。

// UploadController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Hosting; // 需要注入 IWebHostEnvironment
namespace FileUploadServer.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class UploadController : ControllerBase
    {
        private readonly IWebHostEnvironment _hostingEnvironment;
        // 通过构造函数注入 IWebHostEnvironment
        public UploadController(IWebHostEnvironment hostingEnvironment)
        {
            _hostingEnvironment = hostingEnvironment;
        }
        [HttpPost("uploadfile")]
        public async Task<IActionResult> UploadFile(IFormFile file)
        {
            // 检查文件是否为空
            if (file == null || file.Length == 0)
            {
                return BadRequest("没有选择文件或文件为空。");
            }
            // 定义服务器上保存文件的路径(在 wwwroot/uploads 文件夹下)
            var uploadsFolderPath = Path.Combine(_hostingEnvironment.WebRootPath, "uploads");
            // 如果文件夹不存在,则创建它
            if (!Directory.Exists(uploadsFolderPath))
            {
                Directory.CreateDirectory(uploadsFolderPath);
            }
            // 创建一个唯一的文件名,防止文件名冲突
            var uniqueFileName = Guid.NewGuid().ToString() + "_" + file.FileName;
            var filePath = Path.Combine(uploadsFolderPath, uniqueFileName);
            // 将文件保存到服务器
            using (var stream = new FileStream(filePath, FileMode.Create))
            {
                await file.CopyToAsync(stream);
            }
            // 返回成功响应,包含文件的访问路径
            return Ok(new { filePath = $"/uploads/{uniqueFileName}", message = "文件上传成功!" });
        }
        // 如果你需要一次上传多个文件
        [HttpPost("uploadmultiple")]
        public async Task<IActionResult> UploadMultipleFiles(List<IFormFile> files)
        {
            if (files == null || files.Count == 0)
            {
                return BadRequest("没有选择文件。");
            }
            var uploadedFiles = new List<string>();
            var uploadsFolderPath = Path.Combine(_hostingEnvironment.WebRootPath, "uploads");
            if (!Directory.Exists(uploadsFolderPath))
            {
                Directory.CreateDirectory(uploadsFolderPath);
            }
            foreach (var file in files)
            {
                if (file.Length > 0)
                {
                    var uniqueFileName = Guid.NewGuid().ToString() + "_" + file.FileName;
                    var filePath = Path.Combine(uploadsFolderPath, uniqueFileName);
                    using (var stream = new FileStream(filePath, FileMode.Create))
                    {
                        await file.CopyToAsync(stream);
                    }
                    uploadedFiles.Add($"/uploads/{uniqueFileName}");
                }
            }
            return Ok(new { filePaths = uploadedFiles, message = $"成功上传 {uploadedFiles.Count} 个文件。" });
        }
    }
}

说明:

WinForm如何实现文件上传到服务器?-图2
(图片来源网络,侵删)
  • [HttpPost("uploadfile")] 定义了一个 POST 请求的路由。
  • IFormFile file 是 ASP.NET Core 用于接收上传文件的参数。
  • IWebHostEnvironment 用于获取应用程序的根路径,方便我们找到 wwwroot 文件夹。
  • 我们将文件保存在 wwwroot/uploads 目录下,这样文件就可以通过 URL 直接访问(http://localhost:5000/uploads/your_file_name.jpg)。

第二步:创建客户端 (WinForms 应用程序)

现在我们来创建 WinForms 客户端,它将选择文件并发送到我们刚刚创建的 API。

创建项目

打开 Visual Studio,创建一个新的 "Windows Forms App (.NET Framework)" 或 "Windows Forms App" (.NET 6/7/8) 项目。

设计窗体

打开 Form1.cs [Design],从工具箱中拖拽以下控件到窗体上:

  • Button (命名为 btnSelectFile)
  • TextBox (命名为 txtFilePath)
  • Button (命名为 btnUpload)
  • ProgressBar (命名为 progressBarUpload)
  • Label (用于显示状态信息)

布局可以参考下图:

WinForm如何实现文件上传到服务器?-图3
(图片来源网络,侵删)

编写上传逻辑

双击 btnSelectFilebtnUpload 按钮,在 Form1.cs 中编写代码。

// Form1.cs
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsFileUploader
{
    public partial class Form1 : Form
    {
        // 创建一个静态的 HttpClient 实例,避免重复创建,提高性能
        private static readonly HttpClient client = new HttpClient();
        public Form1()
        {
            InitializeComponent();
            // 设置进度条初始状态
            progressBarUpload.Visible = false;
        }
        private void btnSelectFile_Click(object sender, EventArgs e)
        {
            using (OpenFileDialog openFileDialog = new OpenFileDialog())
            {
                openFileDialog.InitialDirectory = "c:\\";
                openFileDialog.Filter = "所有文件 (*.*)|*.*|文本文件 (*.txt)|*.txt|图片文件 (*.jpg;*.png)|*.jpg;*.png";
                openFileDialog.FilterIndex = 1;
                openFileDialog.RestoreDirectory = true;
                if (openFileDialog.ShowDialog() == DialogResult.OK)
                {
                    // 获取选中的文件路径
                    txtFilePath.Text = openFileDialog.FileName;
                }
            }
        }
        private async void btnUpload_Click(object sender, EventArgs e)
        {
            // 检查是否选择了文件
            if (string.IsNullOrEmpty(txtFilePath.Text))
            {
                MessageBox.Show("请先选择一个文件!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }
            // 禁用上传按钮,防止重复点击
            btnUpload.Enabled = false;
            progressBarUpload.Visible = true;
            progressBarUpload.Value = 0;
            lblStatus.Text = "上传中...";
            try
            {
                // 创建一个 MultipartFormDataContent 对象,用于构建包含文件的请求
                using (var form = new MultipartFormDataContent())
                {
                    // 读取文件流
                    byte[] fileBytes = File.ReadAllBytes(txtFilePath.Text);
                    // 将文件流添加到请求中
                    // "file" 必须与服务器端 Action 中的 IFormFile 参数名 "file" 匹配
                    form.Add(new ByteArrayContent(fileBytes), "file", Path.GetFileName(txtFilePath.Text));
                    // 服务器 API 的 URL (请替换为你的实际地址)
                    // 使用 localhost 时,如果你的 WinForms 和 Web API 在同一台机器上运行,
                    // Web API 运行在 https 上,WinForms 也需要使用 https。
                    // 为了方便开发,可以暂时关闭 Web API 的 HTTPS。
                    string apiUrl = "https://localhost:7123/api/upload/uploadfile"; // 注意端口号
                    // 发送 POST 请求并获取响应
                    HttpResponseMessage response = await client.PostAsync(apiUrl, form);
                    // 检查响应是否成功
                    response.EnsureSuccessStatusCode();
                    // 读取响应内容
                    string result = await response.Content.ReadAsStringAsync();
                    // 显示成功消息
                    MessageBox.Show("文件上传成功!\n服务器响应: " + result, "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
                    lblStatus.Text = "上传完成";
                }
            }
            catch (HttpRequestException ex)
            {
                MessageBox.Show($"上传失败: {ex.Message}\n请检查服务器是否正在运行,以及URL是否正确。", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                lblStatus.Text = "上传失败";
            }
            catch (Exception ex)
            {
                MessageBox.Show($"发生未知错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                lblStatus.Text = "上传失败";
            }
            finally
            {
                // 无论成功或失败,都重新启用上传按钮
                btnUpload.Enabled = true;
                progressBarUpload.Visible = false;
            }
        }
    }
}

显示上传进度 (进阶功能)

上面的代码没有显示上传进度。HttpClient 本身不直接提供进度事件,但我们可以通过计算已发送的字节数来实现,这需要一些额外的代码。

我们需要一个自定义的 ByteArrayContent 来跟踪上传进度。

// 在 Form1.cs 文件中添加这个类
public class ProgressableByteArrayContent : ByteArrayContent
{
    private readonly Action<int> _progress;
    public ProgressableByteArrayContent(byte[] bytes, Action<int> progress) : base(bytes)
    {
        _progress = progress;
    }
    protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
    {
        // 计算总字节数
        var length = this.Headers.ContentLength ?? 0;
        // 使用流包装器来跟踪已写入的字节数
        var wrappedStream = new ProgressStream(stream, (bytesWritten) =>
        {
            // 计算进度百分比 (0-100)
            var progressPercentage = (int)((double)bytesWritten / length * 100);
            _progress(progressPercentage);
        });
        await base.SerializeToStreamAsync(wrappedStream, context);
    }
}
// 再添加一个 ProgressStream 类
public class ProgressStream : Stream
{
    private readonly Stream _stream;
    private readonly Action<long> _progress;
    public ProgressStream(Stream stream, Action<long> progress)
    {
        _stream = stream;
        _progress = progress;
    }
    public override bool CanRead => _stream.CanRead;
    public override bool CanSeek => _stream.CanSeek;
    public override bool CanWrite => _stream.CanWrite;
    public override long Length => _stream.Length;
    public override long Position { get => _stream.Position; set => _stream.Position = value; }
    public override void Flush() => _stream.Flush();
    public override int Read(byte[] buffer, int offset, int count) => _stream.Read(buffer, offset, count);
    public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin);
    public override void SetLength(long value) => _stream.SetLength(value);
    public override void Write(byte[] buffer, int offset, int count)
    {
        _stream.Write(buffer, offset, count);
        _progress?.Invoke(_stream.Position);
    }
    protected override void Dispose(bool disposing)
    {
        _stream.Dispose();
        base.Dispose(disposing);
    }
}

然后修改 btnUpload_Click 方法中的 form.Add 部分:

// 替换原来的 form.Add 这一行
// form.Add(new ByteArrayContent(fileBytes), "file", Path.GetFileName(txtFilePath.Text));
// 使用新的 ProgressableByteArrayContent
form.Add(new ProgressableByteArrayContent(fileBytes, (progress) => 
{
    // 在 UI 线程上更新进度条
    this.Invoke((MethodInvoker)delegate 
    {
        progressBarUpload.Value = progress;
    })
}), "file", Path.GetFileName(txtFilePath.Text));

注意: this.Invoke 是必需的,因为 HttpClient 的回调是在后台线程中执行的,而 WinForms 控件(如 ProgressBar)只能在主 UI 线程上访问。


第三步:运行和测试

  1. 运行服务器: 按 F5 运行你的 ASP.NET Core Web API 项目,它会启动并在浏览器中打开 Swagger UI。
  2. 运行客户端: 再按 F5 运行你的 WinForms 项目。
  3. 测试:
    • 在 WinForms 窗体中点击 "选择文件",选择一个本地文件。
    • 点击 "上传文件" 按钮。
    • 你会看到进度条在移动,上传完成后会弹出成功提示。
    • 回到 Swagger UI 页面,你可以直接测试 API,上传一个文件,然后去服务器项目的 wwwroot/uploads 文件夹中查看文件是否已成功保存。

总结与最佳实践

  • 异常处理: 客户端和服务器端都要有完善的异常处理。
  • 文件大小限制: 服务器端一定要设置 FormOptionsMultipartBodyLengthLimit,防止因上传过大文件而导致服务崩溃。
  • 安全性: 在生产环境中,要对上传的文件类型、文件名进行严格的验证和清理,防止恶意文件上传(如病毒、脚本等)。
  • HttpClient 的生命周期: 最好在整个应用程序生命周期内只创建一个 HttpClient 实例(如 static readonly),而不是每次上传都创建一个新的,这样可以避免端口耗尽问题。
  • 取消操作: 可以添加一个 CancellationToken 来允许用户取消正在进行的上传操作。
  • 异步编程: 客户端的上传操作是 I/O 密集型的,使用 async/await 可以避免阻塞 UI 线程,保持应用程序的响应性。
分享:
扫描分享到社交APP
上一篇
下一篇