Silverlightでユーザがアップロードしたファイルをダウンロードする

なんのこっちゃと思われそうなタイトルだけど、どういうことかというと、以下のようなテキストボックスと「Open」「Save」ボタンがある簡易的なテキストエディタアプリ(Silverlight製)があるとする。

このアプリは「Open」ボタンをクリックすると、

お馴染みの「ファイルを開く」ダイアログが表示される。そして、テキストファイルを選択して「開く」ボタンをクリックすると

そのファイルを読み込んで、テキストボックスに内容を表示するというもの*1

この部分の処理は至極単純で、以下のようになっている。

Page.xaml.cs

using System;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;

namespace SilverlightApplication1 {
    public partial class Page : UserControl {
        /// <summary>
        /// 開いているファイル
        /// </summary>
        private string opendFile;

        public Page() {
            InitializeComponent();
        }

        private void openButton_Click(object sender, RoutedEventArgs e) {
            contentInput.Text = opendFile = string.Empty;

            using(var openFileDlg = new OpenFileDialog()) {
                openFileDlg.Filter = "テキストファイル(*.txt)|*.txt";
                if((openFileDlg.ShowDialog()) == DialogResult.Cancel) return;

                using(var sr = openFileDlg.SelectedFile.OpenText()) {
                    contentInput.Text = sr.ReadToEnd();
                    contentInput.Focus();

                    opendFile = openFileDlg.SelectedFile.Name;
                }
            }
        }
    }
}

ここまでは簡単な話で何の問題も無い。

じゃあ、今度は逆にこの変更した内容をファイルに保存したい場合はどうすればいいだろうか?
普通にファイルに書き込めばいいって?Sandboxで実行されるSilverlightにそんな事ができるわけがない。仮にそんな事ができたとしたらセキュリティもくそも無いじゃろがい!!

Silverlightが書き込める領域はIsolatedStorage(分離ストレージ)と呼ばれる隔離された領域だけで、ここにしたってユーザが容易にアクセスできるような所には存在しないので、要件を満たすことはできない。

では、どうするかというとサーバーサイドの力を借りるしかない。

よくブラウザでPDFとかOffice系のファイルのリンクをクリックするとダウンロードするかどうかを問い合わせる画面が表示されて、ファイルをダウンロードする事ができる。

ようは↓が出るようにすればいいのだ。

↑の画面が出るようにするために、まずサーバーサイド側の処理から実装していく。

「Save.aspx」というWebフォームを追加して、以下のように処理を実装する。

Save.aspx.cs

using System;
using System.Text;
using System.Diagnostics;

namespace SilverlightApplication1_Web {
    public partial class Save : System.Web.UI.Page {
        protected void Page_Load(object sender, EventArgs e) {
            var name = Request.Form["name"];
            var content = Request.Form["content"];

            Response.ContentType = "application/octet-stream";
            Response.AppendHeader("Content-Disposition", "attachment; filename=" + name);
            Response.Output.Write(content);
            Response.End();
        }
    }
}

解説
  • 「name」と「content」というポストデータを取ってくる。
  • ContentTypeに「application/octet-stream」を指定する。
  • レスポンスヘッダに「Content-Disposition」=「attachment; filename=ファイル名」を追加する*2
  • あとはcontentをレスポンスに出力して、終了する。

これで、このURLにPOSTでアクセスすると「content」で指定した内容のファイルを「name」で指定したファイル名でダウンロードすることができる。

これはASP.NETで開発をした事がある人にはお馴染みの方法なので目新しいことは無いと思う。

あとはこのURLに対して、Silverlight側からアクセスするわけだけど一つ問題がある。Silverlight側からWebサイトにアクセスする場合WebClientかHttpWebRequest*3を使うわけだけど、どちらにしてもレスポンス出力をSilverlight側でただのストリームとして受け取るため、そこにはブラウザは一切介在しない。

なので例のダウンロードのダイアログを表示しようがないのだ。

こりゃまいったね。

でも、これはあっさりと解決することができる。

まず、Silverlightをホストするページに以下のHTMLタグを記述する。

Default.aspx
<form id="saveForm" action="Save.aspx" method="POST" style="display:none;">
    <input id="name" name="name" />
    <textarea id="content" name="content" />
</form>

みたまんま、お馴染みのFormタグとそれぞれの属性に対応するinput要素とtextarea要素があるだけ。

そして、「Save」ボタンがクリックされた時のイベントハンドラに以下の処理を記述する。

Page.xaml.cs
private void saveButton_Click(object sender, RoutedEventArgs e) {
    var contentEle = HtmlPage.Document.GetElementById("content");
    contentEle.SetProperty("value", contentInput.Text);

    var nameEle = HtmlPage.Document.GetElementById("name");
    nameEle.SetProperty("value", opendFile);

    HtmlPage.Document.Submit("saveForm");
}

「HtmlPage.Document.GetElementById」メソッドを使って、それぞれのHtml要素を取ってきて値を設定し最後にサブミットしているだけ。

これで無事、例のダウンロードのダイアログが表示されて、変更した内容のテキストファイルをユーザがゲットする事ができる。

Silverlightといえど既存のテクノロジの上に成り立っているだけなので、全てをカバーすることはできない。だから、こういう風に適材適所での使いわけが重要となってくるわけですな。

Silverlightの相互運用性の高さ万歳!!

ソース

*1:アップロードちゃうやんという突っ込みは無しの方向で

*2:Response.Headers.Addは駄目!!

*3:今回はPOSTなのでHttpWebRequest一択