by WebSurfer
2010年8月30日 16:21
クライアントがサーバーからファイルをダウンロードし、自分の HDD などに保存した場合、そのファイルの更新日時は HDD に保存した日時になります。
これをサーバー側で設定した日付にすることはできるでしょうか? ActiveX などのプラグインは使わないという条件でです。
結論から言えば、世間一般に使われている解凍ツールと互換性のある zip アーカイブを使えば可能です。
サーバーからは zip アーカイブに更新日時情報を含めて送信し、クライアント側で解凍して保存する時に zip アーカイブに含まれている更新日時に設定してもらうということです。
Web アプリケーションとしては、以下ような手順になります。
-
データベースまたはファイルシステムからバイト列データを取り出す。
-
既存のライブラリを利用して zip アーカイブをバイト列として作成。更新日時やファイル名はその時設定。
-
作成したバイト列を Response.BinaryWrite メソッドで HTTP 出力ストリームに書き込み
参考に、フリーのライブラリ SharpZipLib を使った例をアップしておきます。実行するには Bin フォルダに ICSharpCode.SharpZipLib.dll を入れておく必要があります。下記のサイトから入手できます。
The Zip, GZip, BZip2 and Tar Implementation For .NET
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Configuration" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="ICSharpCode.SharpZipLib.Zip" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
// 例として、SQL Server DB からデータを取得する
// ことで考えています。
ConnectionStringSettings cs =
ConfigurationManager.ConnectionStrings["MyDB"];
string connString = cs.ConnectionString;
SqlConnection connection = new SqlConnection(connString);
string query =
"SELECT filename, data FROM files WHERE id=@id";
SqlCommand command = new SqlCommand(query, connection);
command.Parameters.AddWithValue("@id",
Convert.ToInt32(Request.QueryString["id"]));
string filename = null;
byte[] data = null;
try
{
connection.Open();
SqlDataReader reader = command.ExecuteReader();
if (reader.Read())
{
filename = (string)reader[0];
data = (byte[])reader[1];
}
}
finally
{
connection.Close();
}
if (filename == null || data == null)
{
// DB から取得できなかった場合の処置。
// ここでは単に return
return;
}
Response.AppendHeader("Content-Disposition",
"attachment; filename=" +
HttpUtility.UrlEncode(filename) + ".zip");
Response.ContentType = "application/x-zip-compressed";
// 何故かバイト列のサイズを指定した方が圧縮率
// が高くなる。圧縮前よりは小さくなるはずなの
// で、とりあえず data.Length とする。
byte[] zippedData = new byte[data.Length];
int zippedDataLength = 0;
using (MemoryStream memoryStream =
new MemoryStream(zippedData))
{
using (ZipOutputStream zipOutStream =
new ZipOutputStream(memoryStream))
{
// 圧縮度の設定。9 が最高
zipOutStream.SetLevel(9);
ZipEntry entry = new ZipEntry(filename);
// ここで更新日時を設定
entry.DateTime = new DateTime(2000, 1, 1);
zipOutStream.PutNextEntry(entry);
zipOutStream.Write(data, 0, data.Length);
zipOutStream.Finish();
// 圧縮後のサイズを取得。Finish() の前に置く
// のは NG。zipOutStream.Length は NG。
zippedDataLength = (int)zipOutStream.Position;
zipOutStream.Close();
}
}
Array.Resize(ref zippedData, zippedDataLength);
Response.BinaryWrite(zippedData);
Response.End();
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
</div>
</form>
</body>
</html>
そもそも、ファイル名、更新日時その他のメタデータはファイルシステムが保持しているもので、ファイル本体には含まれていません。
ファイル名のみは、ブラウザが自動的に attachment; filename= に指定された名前で保存してくれますが(日本語の文字化けの問題はありますが)、その他の情報はダメなようです。
Content-Disposition ヘッダーフィールドにファイル名、作成日時、更新日時等の情報を追加してクライアントに送ることは可能です。でもそれは送る手段が用意されているというだけで、ブラウザが自動的にそれに書き換えてくれることはありません。
by WebSurfer
2010年8月28日 13:50
世の中は Visual Studio 2010 への移行が進んでいるようですので、話題に乗り遅れないように、自分も開発環境に Visual Studio 2010 Professional を導入しました。
Web アプリしか作らないし、自分の使っているレンタルサーバーが .NET 3.5 にしか対応していないし、2008 でも何ら問題なく十分使えているし、ちょっと(かなり?)もったいないような気もしますけど。(笑)
まず既存の .NET 2.0, 3.5 ベースの Web アプリを開いて実行してみると、二重定義だとか宣言されてないとかのエラーが出てビルドできません。2008 よりチェックが厳しくなっているようです。でも、それはほんのわずかな部分で、そこを直したら、2008 と同様に使えるようになりました。
使い勝手は 2008 とほとんど同じようです。ただ、ヘルプはブラウザ (IE8) で表示されるようになっており、自分的には非常に使いづらいです。特に困るのが、2008 で使われている専用の Document Explorer にあるキーワードタブがないんです。これは慣れの問題でなんとかなることではなさそうです。
.NET 4.0 ベースで新しい Web サイトを作ると、どうなるか試してみました。デフォルトで、以下のようなフォルダ/ファイルが自動生成されます。
うわさに聞いていた通り、jQuery が標準で実装されています。ASP.NET AJAX には見切りをつけて、jQuery に移行しようとしているという話を聞きましたが、本当でしょうか?
その他、Forms 認証に使う Login.aspx などのページ、マスターページ、CSS などが自動生成されます。CSS は空ではなく、Default.aspx を実行すると以下のようになります。至れり尽くせりというよりは、大きなお世話というべきか・・・(笑)
中でも一番気になっていた web.config ですが、以下のように .NET 3.5 の場合と比べて、ずいぶんと簡略化され、���通しがよくなっているようです。
<?xml version="1.0"?>
<!--
ASP.NET アプリケーションを構成する方法の詳細については、
http://go.microsoft.com/fwlink/?LinkId=169433 を参照してください
-->
<configuration>
<connectionStrings>
<add name="ApplicationServices"
connectionString=
"data source=.\SQLEXPRESS;
Integrated Security=SSPI;
AttachDBFilename=|DataDirectory|\aspnetdb.mdf;
User Instance=true"
providerName="System.Data.SqlClient" />
</connectionStrings>
<system.web>
<compilation debug="false" targetFramework="4.0" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login.aspx"
timeout="2880" />
</authentication>
<membership>
<providers>
<clear/>
<add name="AspNetSqlMembershipProvider"
type="System.Web.Security.SqlMembershipProvider"
connectionStringName="ApplicationServices"
enablePasswordRetrieval="false"
enablePasswordReset="true"
requiresQuestionAndAnswer="false"
requiresUniqueEmail="false"
maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0"
passwordAttemptWindow="10"
applicationName="/" />
</providers>
</membership>
<profile>
<providers>
<clear/>
<add name="AspNetSqlProfileProvider"
type="System.Web.Profile.SqlProfileProvider"
connectionStringName="ApplicationServices"
applicationName="/"/>
</providers>
</profile>
<roleManager enabled="false">
<providers>
<clear/>
<add name="AspNetSqlRoleProvider"
type="System.Web.Security.SqlRoleProvider"
connectionStringName="ApplicationServices"
applicationName="/" />
<add name="AspNetWindowsTokenRoleProvider"
type="System.Web.Security.WindowsTokenRoleProvider"
applicationName="/" />
</providers>
</roleManager>
</system.web>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
--------------- 2010/9/10 追記 ---------------
ヘルプが使いづらいと書きましたが、Viewer がありました。以下のサイトからダウンロードできます。
H3Viewer - Help Viewer for VS 2010
ちょっと使ってみた限りでは、Visual Studio 2008 のヘルプの使い勝手に近いものです。ちょっと動きが遅いような感じがしますが、Visual Studio 2010 のブラウザで表示するヘルプに比べれば、はるかに使いやすそうで、お勧めです。
by WebSurfer
2010年8月27日 17:36
マスターページに Button を配置して Click イベントで MenuPage にリダイレクトするようにしているとします。
あるページ(SourcePage とします)では、Button クリックで MenuPage ではなく別のページ(TargetPage とします)に遷移させたいので、SourcePage の Page_Load で Button.PostBackUrl に TargetPage を設定しました。
それで期待通りに、SourcePage で Button をクリックした場合は TargetPage に遷移していました。
ところが、マスターページの Page_Load に Page.PreviousPage を取得するコードを追加したところ、SourcePage で Button をクリックしても、TargetPage に遷移せず、MenuPage にリダイレクトされるようになってしまいました。
PostBackUrl の設定が無視されて Click イベントが発生しているようです。何故、Page.PreviousPage を取得すると、そうなってしまうのでしょうか?
サンプルのソースコードをアップしておきます。
MasterPage
<%@ Master Language="C#" ClassName="MasterExample" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
// Page.PreviousPage を取得すると Button1.Click イ
// ベントが発生する。
if (Page.PreviousPage != null)
{
Button btn2 =
(Button)Page.PreviousPage.Master.FindControl("Button1");
}
}
// Button1 の PostBackUrl, Text をコンテンツから設定
// するためのプロパティ。
public string SetPostBackUrl
{
set { Button1.PostBackUrl = value; }
}
public string SetButtonText
{
set { Button1.Text = value; }
}
// 通常は Button1 クリックで MenuPage へリダイレクト
protected void Button1_Click(object sender, EventArgs e)
{
Response.Redirect("MenuPage.aspx");
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<asp:ContentPlaceHolder id="head" runat="server">
</asp:ContentPlaceHolder>
</head>
<body>
<form id="form1" runat="server">
<asp:Button ID="Button1"
runat="server"
Text="Go To MenuPage"
OnClick="Button1_Click" />
<hr />
<div>
<asp:ContentPlaceHolder id="ContentPlaceHolder1"
runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
SourcePage
<%@ Page Language="C#" MasterPageFile="~/MasterPage.master" %>
<script runat="server">
// このページの時は、Button1 クリックで、Menu ページでは
// なく TargetPage にクロスページポストバックで遷移する。
// そのため、以下のように Button1 の設定を変更する。
// SetPostBackUrl, SetButtonText プロパティはマスターペー
// ジで定義している。
protected void Page_Load(object sender, EventArgs e)
{
MasterExample m = (MasterExample)Page.Master;
m.SetPostBackUrl = "TargetPage.aspx";
m.SetButtonText = "Go To TargetPage";
}
</script>
<asp:Content ID="Content1"
ContentPlaceHolderID="head"
Runat="Server">
</asp:Content>
<asp:Content ID="Content2"
ContentPlaceHolderID="ContentPlaceHolder1"
Runat="Server">
<h1>This is the Source Page.</h1>
</asp:Content>
ASP.NET 内部でどのような処理がされているか見えないので多分に推測が入っていますが、以下のようなことではないかと思います。
-
SourcePage を要求すると、Page_Load で Button1.PostBackUrl に TargetPage が設定される。ブラウザ上で Button1 をクリックすると、クライアント側のスクリプトによって TargetPage が要求される。
-
その際、サーバーには、SourcePage の ViewState と共に、Button1 がクリックされたという情報も送られる。
-
サーバー側に制御が戻り、TargetPage がロードされ実行される。
-
そこで Page.PreviousPage が参照されていると、SourcePage の状態を取得するため、SourcePage がロードされ実行される。
-
その時、通常発生する Page.Init, Page.Load 等のイベントのみならず Button1.Click イベントも発生し(すべてのイベントを発生させて処理しないと正しく状態が取得できないはず)、そのハンドラで遷移先に設定してある MenuPage にリダイレクトされてしまう。
上記を裏付ける Microsoft のライブラリなどを探したのですが、残念ながら見つかるませんでした。見つけた中で一番信頼の置けそうなのが以下のサイトです。
Solve Postback Hassles with Cross-Page Postbacks in ASP.NET 2.0
というわけで、このような問題を避けるため、ASP.NET でクロスページポストバックを使うのはできるだけ避けた方がよさそうです。
今回のケースでは、対症療法的に以下のようにして対応できますが、予期しない副作用があるかもしれませんし。
protected void Button1_Click(object sender, EventArgs e)
{
if (!Page.IsCrossPagePostBack)
{
Response.Redirect("MenuPage.aspx");
}
}