- PR -

DetailsView内でファイルをアップロードをする場合のパラメータの渡し方について

投稿者投稿内容
BT
ベテラン
会議室デビュー日: 2006/09/24
投稿数: 81
お住まい・勤務地: Tokyo
投稿日時: 2006-11-06 10:12
GridViewで一覧表示させた商品ページを選択すると、DetailsViewに詳細が表示され
修正や追加ができるという、よくありがちなページを作っています。
サンプルはあちこちに出回っているので、簡単なものは作れるものの、
基本を分かっていないのでちょっと凝ったことをしようとすると
すぐ行き詰まってしまうというありさまです。

今回は、商品データはXMLファイルで管理しており、ユーザ入力値を
ASPXページに配置したObjectDataSourceを介してビジネスオブジェクト
に渡してXMLを読み書きしています。
簡単なテキスト値の操作は出来るのですが、FileUploadコントロールを使っての
商品の写真など画像のアップロードで壁にぶつかってしまいました。
TextBoxの場合だと、BindingされているDataFieldを見て、UpdateMethodなどの
どのパラメータ名として渡すのかを決めてくれているのだろうと推測しているのですが
(実はここも理解できていません。ObjectDataSourceのSelectMethodで取得した
DataFieldとUpdateMethodで渡す引数名は同じだとは限らないと思うので
何故明示的に指定しなくても良いのか不思議です)
FileUploadコントロールの場合だと、Bindingするわけにもいけないので
ObjectDataSourceのUpdateParametersの設定でForm.Request("FileUpload1")
のような感じで設定してみましたが、これではダメでした。
どう設定すればFileUploadのデータをObjectDataSourceのパラメータとして
渡せるのでしょうか?

基本的なことだと思いますが、アドバイスいただければありがたいです。
よろしくお願いします。
Access
ぬし
会議室デビュー日: 2002/04/08
投稿数: 829
投稿日時: 2006-11-07 05:29
以下の手順で実装できます。

1. GridViewにTemplateFieldを追加してEditItemTemplateにFileUploadを配置する
2. SqlDataSourceのUpdateParametersにImageのパラメータを追加する
3. GridViewのRowUpdatingでSqlDataSourceのパラメータ値にイメージファイルの
  パスを代入する

コード:
<asp:TemplateField HeaderText="Image">
  <ItemTemplate>
    <asp:Image ImageUrl="<%# Eval("Image") %>" runat="server" ID="image" />
  </ItemTemplate>
  <EditItemTemplate>
    <asp:FileUpload ID="FileUpload1" runat="server" />
  </EditItemTemplate>
</asp:TemplateField>

<asp:SqlDataSource ConnectionString="<%$ ConnectionStrings:CustomerConnectionString %>"
  ID="SqlDataSource1" runat="server"
  SelectCommand="SELECT [CustomerID], [Name], 
                   [Image] FROM [Customers]"
  UpdateCommand="UPDATE [Customers] 
                 SET [Name] = @Name, [Image] = @Image 
                 WHERE [CustomerID] = @original_CustomerID">
  <UpdateParameters>
    <asp:Parameter Name="Image" DefaultValue="default.gif" />
  </UpdateParameters>
</asp:SqlDataSource>

protected void GridView1_RowUpdating(object sender, 
  GridViewUpdateEventArgs e)
{
  FileUpload fileUpload = 
    GridView1.Rows[e.RowIndex].FindControl("FileUpload1") as FileUpload; 
  fileUpload.SaveAs(System.IO.Path.Combine(Server.MapPath("Images"), 
    fileUpload.FileName));
  SqlDataSource1.UpdateParameters["Image"].DefaultValue = 
    "~/Images/" + fileUpload.FileName;
}



_________________
ASP.NET+Ajaxサンプル集 | JavaScript+Ajaxサンプル集
BT
ベテラン
会議室デビュー日: 2006/09/24
投稿数: 81
お住まい・勤務地: Tokyo
投稿日時: 2006-11-07 14:39
お世話になります。

これはDetailsViewではなくてGridViewで、DataSourceもSqlDataSourceですが、同じようなものだということですよね。
DetailsViewの中のコントロールにアクセスする方法がイマイチわかっていないのですが、(GridViewはDataGridに似ているのである程度分かるのですけど)
GridViewでのアップロードもやりたかったのでGridViewで話を進めていきたいと思います。

教えていただいた方法だと、(1)コードビハインドファイルの中でFileUploadコントロールを見つけ、(2)画像ファイルを保存し、(3)保存したパスをUpdateParametersのImegeにセットしてSQLを発行、ということになるかと思います。
RowUpdatedでなくRowUpdatingでやっているのはSQL発行前に値をセットする必要があるためでしょうか。
(Row***ingの使い道がわからなかったのですがなるほどこういう風に使うのですね)

確かにこれでできると思うのですが、コードビハインドの中でコントロールを見つけるのではなく、ObjectDataSourceのパラメータとして直接FileUploadコントロールを渡してやり、渡した先でファイルやパスの保存をするということができないか?できるとしたらパラメータの渡し方で良い方法はないのかなと思って質問させてもらいました。ノンコーディングでできればそれに越したことはないですから。

例えば、TemplateFieldとDataSourceオブジェクトが以下のとおりとして、

<asp:TemplateField HeaderText="画像">
<EditItemTemplate>
<asp:FileUpload ID="FileUpload1" runat="server" />
</EditItemTemplate>
<ItemTemplate>
<asp:Image ID="Image2" runat="server" ImageUrl='<%# Eval("Image") %>' />
</ItemTemplate>
</asp:TemplateField>

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" DeleteMethod="DeleteData"
SelectMethod="GetData" TypeName="Product" UpdateMethod="UpdateData">
<DeleteParameters>
<asp:Parameter Name="id" Type="String" />
</DeleteParameters>
<UpdateParameters>
<asp:Parameter Name="id" Type="String" />
<asp:Parameter Name="visible" Type="String" />
<asp:Parameter Name="image" Type="Object" />
<asp:Parameter Name="title" Type="String" />
<asp:Parameter Name="description" Type="String" />
</UpdateParameters>
</asp:ObjectDataSource>

渡した先(例えば~/App_Code/Product.csに書かれたProductクラスのUpdateDataメソッド)
が以下のようになる場合です。

public void UpdateData(string id, string visible, object image, string title, string description)
{
//ここでimageを保存&idから該当データを探して保存パス等を更新する
}

このメソッドにブレークポイントを設定したところでは、imageには何も入っていなかったのでパラメータとして渡されていないようです。ただ、そもそもFileUploadコントロールがimageにBindingされていないし出来ないので当然という気もしますが。(という理解でよいのでしょうか?ここが理解できていないので見当違いなことをしているのではという疑念が払拭できません)

とすると、Bindingしていなくても何らかの方法でFileUploadコントロールを<asp:Parameter Name="image" Type="Object" />にマッピングすればよいのでは?と思いましたが、そもそもそんなことができるのかどうか?できないならばRowUpdatingの中で、全てのコントロールの値を取り出して、直接UpdateDataメソッドを呼び出せばよいのでしょうが、それだとVS2003の時とあまり変わらなくなってしまいます。かといって、Type="Object"というのがあるのを見ると画像ファイルのようなオブジェクトも渡せるのではないかと思ってしまいますし、そうでなければ"Object"を指定できる意味がよくわかりません。

そこで、デザイン画面のObjectDataSourceのプロパティのUpdateParametersでパラメータコレクションエディタを開いて触ってみましたが、GridView1.SelectedValueはBindingしてないとダメそうで、Request.Form("FileUpload1")でもダメでした。
できなければ仕方がないので、従来どおりの方法でやるしかないのですが、ここまでノンコーディングでできるようになっていながら、FileUploadが入るだけでいきなり元の方法に戻ってしまうものだろうか?何か良い方法があるのではないのか?というのが正直なところです。

と、ここまで書いて思いついたのですが、教えていただいた方法を使ってRowUpdatingの中で、FileUploadコントロールだけをFindControlで取り出した上で、

ObjectDataSource1.UpdateParameters["image"].DefaultValue=fileUpload;

のように指定すれば、コントロール自体を渡すことも可能かもしれませんね。全てのコントロールから値を取り出すよりもコード量が減りますし、後でやってみたいと思います。
ただ、この場合でもノンコーディングとはいかないようですし、パラメータの一部だけを入れ替えるというのは後で見てコードが分かりにくくなりそうな気もしますね。

やってみて出来ましたらまた報告したいと思います。
どうもありがとうございました。
Access
ぬし
会議室デビュー日: 2002/04/08
投稿数: 829
投稿日時: 2006-11-08 10:08
DAL層に直接UI層のFileUploadコントロールを渡すと多階層に分離した意図と
反するのではないでしょうか。

3階層の例

コード:
UI層
<asp:TemplateField HeaderText="PhotoPath" SortExpression="PhotoPath">
  <EditItemTemplate>
    <asp:FileUpload ID="FileUpload1" runat="server" />
    <asp:Label ID="lblFileUpload" runat="server" EnableViewState="false" ForeColor="red"/>            
  </EditItemTemplate>
  <ItemTemplate>
    <asp:Image ID="Image1" runat="server" ImageUrl='<%# Eval("PhotoPath") %>' />            
  </ItemTemplate>
</asp:TemplateField>

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" 
  SelectMethod="GetEmployees"     
  DeleteMethod="DeleteEmployee"
  UpdateMethod="UpdateEmployee"  
  TypeName="EmployeeManager">
  <DeleteParameters>
    <asp:Parameter Name="EmployeeID" Type="Int32" />
  </DeleteParameters>
  <UpdateParameters>
    <asp:Parameter Name="EmployeeID" Type="Int32" />
    <asp:Parameter Name="Name" Type="String" />
    <asp:Parameter Name="Title" Type="String" />
    <asp:Parameter Name="PhotoPath" Type="String" />
  </UpdateParameters>
</asp:ObjectDataSource>

BLL層
Protected Sub GridView1_RowUpdating(ByVal sender As Object, _
  ByVal e As System.Web.UI.WebControls.GridViewUpdateEventArgs)
  Dim fileUpload As FileUpload = CType(GridView1.Rows(e.RowIndex).FindControl("FileUpload1"), FileUpload)
  If fileUpload.HasFile Then
    fileUpload.SaveAs(Path.Combine(Server.MapPath("~/Images/temp/"), fileUpload.FileName))
    e.NewValues("PhotoPath") = "~/Images/temp/" & fileUpload.FileName
  Else
    e.Cancel = True
    Dim lbl As Label = CType(GridView1.Rows(e.RowIndex).FindControl("lblFileUpload"), Label)
    lbl.Text = "*"
  End If
End Sub

DAL層
<ComponentModel.DataObjectMethodAttribute(ComponentModel.DataObjectMethodType.Update, True)> _
<PrincipalPermission(SecurityAction.Demand, Role:="Admin")> _
Public Shared Sub UpdateEmployee(ByVal EmployeeID As Integer, _
    ByVal Name As String, ByVal Title As String, ByVal PhotoPath As String)
    Dim updateString As String = "UPDATE Employees " & _
        "SET [Name]=@Name, [Title]=@Title, [PhotoPath]=@PhotoPath " & _
        "WHERE [EmployeeID]=@EmployeeID"
    Dim con As New SqlConnection(connectionString)
    Dim cmd As New SqlCommand(updateString, con)
    cmd.Parameters.AddWithValue("@Name", Name)
    cmd.Parameters.AddWithValue("@Title", Title)
    cmd.Parameters.AddWithValue("@PhotoPath", PhotoPath)
    cmd.Parameters.AddWithValue("@EmployeeID", EmployeeID)
    con.Open()
    cmd.ExecuteNonQuery()
    con.Close()
End Sub



_________________
ASP.NET+Ajaxサンプル集 | JavaScript+Ajaxサンプル集
BT
ベテラン
会議室デビュー日: 2006/09/24
投稿数: 81
お住まい・勤務地: Tokyo
投稿日時: 2006-11-09 16:47
返信ありがとうございます。

いろいろ試したりしていて遅くなりました。結論として、

ObjectDataSource1.UpdateParameters["image"].DefaultValue=fileUpload;

はダメでした。(当然といわれそうですが)DefaultValueってstringなんですね。
他にも、自動生成されるコントロールのnameの値をどうにかして取得してRequest.Form()で取り出そうかなど調べたりしていました(←これ、出来そうですがかえってコードが増えそうなのでやっぱり止めました)
で、今回教えていただいたのを参考に、

protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
  FileUpload fileUpload = (FileUpload)GridView1.Rows[e.RowIndex].FindControl("FileUpload1");
  e.NewValues["image"] = fileUpload;
}

とやると、fileUploadコントロール自身が渡されていることが確認出来ました。
素晴らしい!どうもありがとうございました。

ところで、もう一点よろしければ教えてください。
こういう処理の場合ですが、今回教えていただいた処理のようにコードビハインドの中で画像を保存してDB操作は別に行うというのが定石なのでしょうか?
というのも、画像ファイルもデータソースの一部では?という頭があるのと(実際にDBに直接画像を保存したりしますよね)、画像保存用コードがページ毎に分散してしまうというので保守性に難があるのではと思ったりしてしまいます。

確かに、DBに格納するのでなければデータアクセス層(DAL)で画像保存はどうかと思いますが、私が書いたコードでのProductクラスは自分としてはDALではなくて、ビジネスロジック層(BLL)のつもりで作りました。で、本来は分離すべきBLL,DALが自分のスキルの制約などもあって仕方なく一緒になっていてBLLの中でデータアクセスもやっているという感じです。中途半端な階層構造であることは自分でも認識していて、将来的には分離したいと考えていますが・・。

ですので、いろんなページにあるFileUploadコントロールをBLLで集中処理するつもりであのようなコードにしたというのが理由です。でも、今回示して頂いたコードではコードビハインドがBLLということになっていたので、あれ?そうなのか?と思いました。自分の理解としてはaspxとaspx.cs(aspx.vb)はそれぞれUI層のサブレイヤで、BLLは~/App_Code/の中に入れるcsファイルなどに書くものと思っていたのですが、この理解は間違っているのでしょうか?

とりあえず、画像がアップロードできるようになってかなり前進しました。
結構長い間、悩んでいたので大変たすかりました。どうもありがとうございました。
あとはDetailsViewとFormViewでもやってみます。(またはまりそうですけど・・)
Access
ぬし
会議室デビュー日: 2002/04/08
投稿数: 829
投稿日時: 2006-11-10 05:48
引用:

ですので、いろんなページにあるFileUploadコントロールをBLLで集中処理するつもりであのようなコードにしたというのが理由です。でも、今回示して頂いたコードではコードビハインドがBLLということになっていたので、あれ?そうなのか?と思いました。自分の理解としてはaspxとaspx.cs(aspx.vb)はそれぞれUI層のサブレイヤで、BLLは~/App_Code/の中に入れるcsファイルなどに書くものと思っていたのですが、この理解は間違っているのでしょうか?


今回はBLLを省略しましたが本来はご指摘のようにApp_CodeフォルダにBLLのような
サブフォルダを作成することになると思います。

私はイベントハンドラはUI層の一部と認識しています。したがって、イベントハンドラではUIから必要なデータを取得してBLLに引数として渡すのが一般的かなと思います。

イベントハンドラをUI、BLLどちらに入れるかは意見が分かれるかもしれませんね。
_________________
ASP.NET+Ajaxサンプル集 | JavaScript+Ajaxサンプル集
BT
ベテラン
会議室デビュー日: 2006/09/24
投稿数: 81
お住まい・勤務地: Tokyo
投稿日時: 2006-11-12 21:58
どうもありがとうございます。

レイヤの分け方は何が正解というものではないということですね。もっと勉強して理解が進んできたらちゃんと3層に分けることにチャレンジしてみたいと思います。

ところでスレッドタイトルはDetailsViewですので、タイトルにつられて来られた(私と同じような初学者の)方のために、「DetailsView内でファイルをアップロードをする場合のパラメータの渡し方について」についてもやってみましたので、ご報告したいと思います。(大して参考にはならないと思いますが)

------------------------------------
aspxファイルの抜粋
------------------------------------
<asp:TemplateField HeaderText="画像">
 <InsertItemTemplate>
  <asp:FileUpload ID="FileUpload1" runat="server" />
 </InsertItemTemplate>
 <ItemTemplate>
  <asp:Image ID="Image1" runat="server" AlternateText='<%# Eval("Title") %>' ImageUrl='<%# Eval("Image") %>' Visible='<%# Eval("Image")!=string.Empty ? true : false %>' />
 </ItemTemplate>
 <EditItemTemplate>
  <asp:FileUpload ID="FileUpload2" runat="server" />
 </EditItemTemplate>
</asp:TemplateField>

//ObjectDataSourceは省略

------------------------------------
aspx.csファイルのUpdatingメソッド
------------------------------------
protected void DetailsView1_ItemUpdating(object sender, DetailsViewUpdateEventArgs e)
{
 e.NewValues["image"] = (FileUpload)DetailsView1.FindControl("FileUpload2");
}
protected void DetailsView1_ItemInserting(object sender, DetailsViewInsertEventArgs e)
{
 e.Values["image"] = (FileUpload)DetailsView1.FindControl("FileUpload1");
}

このようにGridViewの場合とほぼ一緒です。違うのはGridViewは行単位でFindControlするのに対してDetailsViewは直接コントロール名を指定できるというところでしょうか。ページングができるということでGridViewのように当該レコードを探してからその中のコントロールにアクセスするのかなと思ってたのですが、すごく簡単なんですね。これは便利。
あと、何故かわかりませんがInsertの場合はe.NewValues[""]ではなく、e.Values[""]のようです。
また、FormViewについてもやってみようかと思ったのですが、こちらはまた別の問題で止まってます(GridView,DetailsViewにも関係しますが)。内容が異なるので解決できない場合には別途スレッドを立てて質問させてもらうかもしれません。

また、GridViewに関してもInsertができないと方手落ちだろうと思い、拡張して追加時にファイルのアップロードができないかを試みてみました。
サンプルなどによくあるようにFooterに追加行を作ってみましたが、よく見るとGridViewってRowInsert***関係のイベントってないんですね。(といいながら、GridViewのTemplateFieldの中でInsertItemTemplateのインテリセンスが働くのはよくわかりませんが)
となると、同じ手は使えないのでRowCommandメソッドの中でやるしかないということで、

http://dotnetfan.org/blogs/dotnetfanblog/articles/632.aspx

あたりを参考に以下のコードを書いてみました。

------------------------------------
aspxファイルの抜粋
------------------------------------
<asp:TemplateField HeaderText="画像">
 <EditItemTemplate>
  <asp:FileUpload ID="FileUpload1" runat="server" />
 </EditItemTemplate>
 <ItemTemplate>
  <asp:Image ID="Image2" runat="server" ImageUrl='<%# Eval("Image") %>' Visible='<%# Eval("Image")!=string.Empty ? true : false %>' />
 </ItemTemplate>
 <FooterTemplate>
  <asp:FileUpload ID="NewFileUpload" runat="server" />
 </FooterTemplate>
</asp:TemplateField>

//ObjectDataSourceのInsertはEditと同じような感じ

------------------------------------
aspx.csファイルのRowCommandメソッドの抜粋
------------------------------------
protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
・・・
 switch (e.CommandName)
 {
  ・・・・
  case "Insert":
   ObjectDataSource1.InsertParameters.Clear();
   foreach (string key in Request.Form.AllKeys)
   {
    if (key.Contains("NewFileUpload"))
     ObjectDataSource1.InsertParameters.Add(new ControlParameter("image", key));
    if (key.Contains("NewTitleBox"))
     ObjectDataSource1.InsertParameters.Add(new ControlParameter("title", TypeCode.String, key, "Text"));
   }
   ObjectDataSource1.Insert();
   break;
   ・・・
 }

 GridView1.DataBind();
}

これって、自分が調べていたRequest.Formからコントロールを拾ってくる方式ですが、TextBoxやCheckBoxはOKなものの肝心のFileUploadは拾えません。何故??HTMLソースではちゃんと検索で引っかかるのですけども。

ちなみにDataGirdだと、***_Commandメソッドの中で同じようにe.CommandNameで分岐して
TextBox newtitle = (TextBox)GridView1.FindControl("NewTitleBox");
みたいにすれば容易に拾えたと思うのですが(FileUploadは未実施なので不明)GridViewだとそういうのは出来ないのでしょうか?

という状況でInsertについても停滞中です(やっぱり参考にはなりませんね^_^;)。いずれにしろ編集に比べるとコードを書かないといけないようですし、必須機能でもないので追加行を付けるのは保留として、後日取り組んでみたいと思います。
どっとねっとふぁん
ぬし
会議室デビュー日: 2005/02/23
投稿数: 935
投稿日時: 2006-11-13 00:41
GridViewの中のコントロールをFindControlで見つけるには、GridViewRowを指定して
その中で探す必要があります。

スキルアップ/キャリアアップ(JOB@IT)