WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

HttpClient でファイルアップロード

by WebSurfer 11. August 2019 14:00

HttpClient クラス を使った Windows Forms アプリで multipart/form-data 形式にてファイルをアップロードする方法を書きます。

HttpClient でファイルをアップロード

multipart/form-data 形式で送信するには MultipartFormDataContent クラスを利用します。

MultipartFormDataContent クラスのインスタンスを生成し、それに HttpContent クラスの派生クラスを multipart の各パートとして Add します。

この記事では文字列とファイルを別々のパートとして送信するサンプルを書きます。

その場合、試用する HttpContent クラスの派生クラスは、文字列を送信する場合は StringContent クラスを、ファイルを送信する場合は StreamContent クラスを使うのがよさそうです。

下のサンプルコードを見てください、StringContent クラスとStringContent クラスを初期化してヘッダ情報を設定し、それぞれを MultipartFormDataContent オブジェクトに Add し、さらにそれを HttpRequestMessage オブジェクトの Content プロパティに設定しています。

using System;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;

namespace WindowsFormsApplication2
{
  public partial class Form3 : Form
  {
    // socket 浪費問題対応
    private static HttpClient httpClient;

    public Form3()
    {
      InitializeComponent();
      this.textBox1.Text = "";
      this.label1.Text = "";
    }

    // ファイル選択に OpenFileDialog 利用
    private void button1_Click(object sender, EventArgs e)
    {
      OpenFileDialog openFileDialog1 = new OpenFileDialog();

      openFileDialog1.InitialDirectory = 
                               @"C:\Users\surfe\Pictures";
      openFileDialog1.Filter = 
           "jpeg files (*.jpg)|*.jpg|All files (*.*)|*.*";
      openFileDialog1.FilterIndex = 1;
      openFileDialog1.RestoreDirectory = true;

      if (openFileDialog1.ShowDialog() == DialogResult.OK)
      {
        this.textBox1.Text = openFileDialog1.FileName;
      }
    }

    // HttpWebRequest を利用
    private void button2_Click(object sender, EventArgs e)
    {
      // ・・・コードは省略・・・
    }

    // HttpClient を利用
    private async void button3_Click(object s, EventArgs e)
    {
      if (string.IsNullOrEmpty(this.textBox1.Text)) return;

      this.label1.Text = "";

      if (httpClient == null)
      {
        httpClient = new HttpClient();
      }

      //送信するファイルへのパス
      string filePath = this.textBox1.Text;
      string fileName = Path.GetFileName(filePath);

      string url = @"送信先 Web サーバーの URL";

      MultipartFormDataContent content = 
                              new MultipartFormDataContent();
            
      // ファイルのみでなく文字列も送信してみる
      string strData = "これは、テストです。";
      StringContent stringContent = new StringContent(strData);
      stringContent.Headers.ContentDisposition = 
               new ContentDispositionHeaderValue("form-data")
      {
        Name = "comment"
      };
      content.Add(stringContent);

      // アップロードするファイル
      using (FileStream fs = new FileStream(filePath, 
                                            FileMode.Open, 
                                            FileAccess.Read))
      {
        StreamContent streamContent = new StreamContent(fs);
        streamContent.Headers.ContentDisposition = 
               new ContentDispositionHeaderValue("form-data")
        {
          Name = "upfile",
          FileName = fileName
        };                
        streamContent.Headers.ContentType = 
                    new MediaTypeHeaderValue("image/jpeg");
        content.Add(streamContent);

        // メソッド (POST) と送信先の URL 指定
        HttpRequestMessage request = 
                 new HttpRequestMessage(HttpMethod.Post, url);
        request.Content = content;

        // ここでファイルを HTTP ストリームに書き込むので、
        // 以下は using の { } 内にないとファイルが読めな
        // いというエラーになる
        HttpResponseMessage response = 
                           await httpClient.SendAsync(request);

        // 応答のコンテンツを Stream として取得
        using (Stream responseStream = 
                   await response.Content.ReadAsStreamAsync())
        {
          using (StreamReader sr = 
              new StreamReader(responseStream, Encoding.UTF8))
          {
            this.label1.Text = sr.ReadToEnd();
          }
        }
      }            
    }
  }
}

上記のコードを実行して、HttpClient を使ってファイルを送信し、返ってきた応答を表示したのが上に表示した画像です。

下の画像は Fiddler でのキャプチャ結果で、送信ヘッダとコンテンツを表示しています。送信ヘッダに指定されている boundary で StringContent とStringContent の部分が分けられ、各パートに Content-Disposition などのヘッダ情報が付与されているのが分かるでしょうか?

送信ヘッダとコンテンツ

最後になりましたが HttpClient を使う際の注意点として重要なことを書いておきます。それは、using 句を使うなどしてHttpClient の初期化と Dispose を繰り返すと socket が浪費されるという問題があるということです。詳しくは以下の記事を見てください。static にして使い回すのが良いとのことです。

YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE

Tags: ,

Upload Download

MVC でファイルのアップロード

by WebSurfer 3. August 2019 15:58

ASP.NET MVC でファイルをアップロードする方法について書きます。(ダウンロードする方法は先の記事「MVC でファイルのダウンロード」に書きましたのでそちらを見てください)

MVC でファイルのアップロード

普通に form を submit して POST 送信する場合と、jQuery Ajax を利用して非同期で送信する場合の両方の例を紹介します。ちなみに、上の画像は jQuery Ajax を使ってアップロードした結果です。

気をつけるべき点は以下の通りです。

  1. View では form 要素の enctype 属性に "multipart/form-data" が設定されるようにする。
  2. Controller のアクションメソッドでは、アップロードされたファイルがバインドされるパラメータまたはクラスのプロパティは HttpPostedFileBase 型であること。  
  3. 上で述べたバインドされるパラメータまたはクラスのプロパティの名前は、html ソースの <input type="file" ... /> の name 属性と一致させる。
  4. Internet Explorer (IE) でファイルをアップロードすると、クライアント PC でのフルパスがファイル名として送信されることがある(先の記事「IE でアップロードする際のファイル名」を参照)。その場合、HttpPostedFileBase.FieName でファイル名を取得するとクライアント PC でのフルパスになるので、必ず Path.GetFileName を使うこと。  
  5. ワーカープロセスがアップロードするホルダに対する「書き込み」権限を持っていること。
  6. ASP.NET では、デフォルト設定では 4MB を超えるリクエストは送信できないので注意。4MB を超える場合は、web.config の <httpRuntime> セクションの maxLengthRequest の設定で調整できる。

jQuery Ajax を使ってファイルをアップロードする場合は、上記に加えて以下の点に要注目です。

  1. XMLHttpRequest を使用して送信するためのキーと値のペアのセットを取得するために FormData オブジェクトを利用する。詳しくは MDN の記事「FormData オブジェクトの利用」にありますのでそちらを参照してください。
  2. ASP.NET MVC 組み込みの CSRF 防止機能は Ajax でもそのまま使えますので、View での @Html.AntiForgeryToken() と Controller のアクションメソッドへの [ValidateAntiForgeryToken] を忘れずに設定する。

上の画像を表示するのに使ったコードを以下に書いておきます。

Model

public class UploadModels
{
    public string CustomField { get; set; }
    public HttpPostedFileBase PostedFile { get; set; }
}

View

@model Mvc5App.Controllers.UploadModels

@{
    ViewBag.Title = "Upload";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Upload</h2>

@using (Html.BeginForm("Upload", "Home", FormMethod.Post,
                new { enctype = "multipart/form-data" }))
{
    // form 内の隠しフィールドは Ajax でも送信される。
    // なので以下に設定したトークンは送信される。もちろん
    // クッキーのトークンも送信されるので、アクションメソ
    // ッドに [ValidateAntiForgeryToken] を付与すれば
    // CSRF の検証はできる
    @Html.AntiForgeryToken()

    // name 属性はモデルのクラスのプロパティ名と同じにしない
    // とサーバー側でモデルバインディングされないので注意。
    // 大文字小文字は区別しない。
    <input type="file" name="postedfile" />
    <button type="submit">Upload by Submit</button>
    <br />
    @ViewBag.Result
}
<br />
<input type="button" id="ajaxUpload" value="Ajax Upload" />
<br />

<div id="result"></div>


@section Scripts {
  <script type="text/javascript">
    //<![CDATA[
    $(function () {
      $('#ajaxUpload').on('click', function (e) {
        // FormData オブジェクトの利用
        var fd = new FormData(document.querySelector("form"));

        // 追加データを以下のようにして送信できる。フォーム
        // データの一番最後に追加されて送信される
        fd.append("CustomField", "This is some extra data");

        $.ajax({
          url: '/home/upload',
          method: 'post',
          data: fd,
          processData: false, // jQuery にデータを処理させない
          contentType: false  // contentType を設定させない
          }).done(function(response) {
            $("#result").empty;
            $("#result").text(response);
          }).fail(function( jqXHR, textStatus, errorThrown ) {
            $("#result").empty;
            $("#result").text('textStatus: ' + textStatus +
                            ', errorThrown: ' + errorThrown);
          });
      });
    });
    //]]>
  </script>
}

Controler / Action Method

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
using Mvc5App.Models;
using System.IO;

namespace Mvc5App.Controllers
{
  public class HomeController : Controller
  {
    public ActionResult Upload()
    {
      return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Upload(UploadModels model)
    {
      string result = "";
      HttpPostedFileBase postedFile = model.PostedFile;
      if (postedFile != null && 
          postedFile.ContentLength > 0)
      {
        // アップロードされたファイル名を取得。ブラウザが IE 
        // の場合 postedFile.FileName はクライアント側でのフ
        // ルパスになることがあるので Path.GetFileName を使う
        string filename = 
                  Path.GetFileName(postedFile.FileName);

        // 保存ホルダの物理パス\ファイル名
        string path = Server.MapPath("~/UploadedFiles") + 
                      "\\" + filename;

        // アップロードされたファイルを保存
        postedFile.SaveAs(path);

        result = filename + 
                 " (" + postedFile.ContentType + ") - " +
                 postedFile.ContentLength.ToString() + 
                 " bytes アップロード完了";
      }
      else
      {
        result = "ファイルアップロードに失敗しました";
      }

      if (Request.IsAjaxRequest())
      {
        return Content(result);
      }
      else
      {
        ViewBag.Result = result;
        return View();
      }
    }
  }
}

上の例は単一ファイルをアップロードする場合のものです。複数のファイルをアップロードする場合は、例えば html ソースで以下のように name 属性の値に連番で index を付与できれば、

<input type="file" name="postedfiles[0]" />
<input type="file" name="postedfiles[1]" />
 ・・・中略・・・
<input type="file" name="postedfiles[n]" />

Model のクラスを以下のようにして PostedFiles にモデルバインドできます。

public class UploadModels
{
    public string CustomField { get; set; }
    public IList<HttpPostedFileBase> PostedFiles 
    { get; set; }
}

Tags: , , ,

Upload Download

不正なクロススレッドコールの捕捉

by WebSurfer 11. July 2019 16:43

Windows Forms のマルチスレッドアプリで不正なクロススレッドコールがある場合、Visual Studio から[デバッグの開始(S)]でなら以下のような例外が捕捉されるが、[デバッグなしで開始(H)]では捕捉されないという話を書きます。

不正なクロススレッドコールの捕捉

元の話は Teratail の「非同期処理でデバッグ時・ビルド実行時で処理結果が異なる?」というスレッドから来ています。

例えば、Windows Forms アプリの Button.Click のイベントハンドラで、以下のように AsParallel 拡張メソッドを使ってマルチスレッドで処理するケースを考えます。

private void button4_Click(object sender, EventArgs e)
{
    Enumerable.Range(1, 10).AsParallel().ForAll(z =>
    {
        this.textBox2.Text += z.ToString();
    });
}

this.textBox2.Text += z.ToString(); の行で、textBox2 が作成された UI スレッドとは別のスレッドから textBox2 の呼び出しを行っています。これが不正なクロススレッドコールになります。

このコードを Visual Studio から[デバッグの開始(S)]で実行した場合は、デバッガがこの行で InvalidOperationException 例外をスローし、"コントロールが作成されたスレッド以外のスレッドからコントロール 'textBox2' がアクセスされました" というエラーメッセージを表示します。それが上の画像です。

ところが[デバッグなしで開始(H)]で実行した場合は例外はスローされず、あたかもこのコードは正常終了したかのように見えます。集約的例外ハンドラで捕捉してアプリケーションを終了させるということもできません(そもそも例外がスローされないので)。

という訳で、特にマルチスレッドアプリを開発している場合は必ず[デバッグの開始(S)]で実行して動作確認をするようにした方がよさそうです。

なお、[デバッグなしで開始(H)]でも CheckForIllegalCrossThreadCalls プロパティを true(デフォルトは false)に設定すれば不正なクロススレッドコールで例外をスローさせることはできます。

でも、たぶんオーバーヘッドが増えてパフォーマンスに影響があるでしょうからリリース前には元に戻さなければならず、そんな面倒なことをするより[デバッグの開始(S)]で実行して確認するのが正解だと思います。

Tags:

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  October 2019  >>
MoTuWeThFrSaSu
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

View posts in large calendar