SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

現役エンジニア直伝! 「現場」で使えるコンポーネント活用術(ActiveReports)

業務要件を満たすWebアプリの作り方
SPREAD + InputMan + ActiveReports = Web業務アプリケーション

  • X ポスト
  • このエントリーをはてなブックマークに追加

ダウンロード サンプルソース (2.0 MB)

 今回は、SPREAD、InputMan、ActiveReportsを使って、業務用Webアプリを作成するときの注意点を押さえながら基本的な技法を説明していきます。

  • X ポスト
  • このエントリーをはてなブックマークに追加

コンポーネントを活用した業務用Webアプリの作成

 一時期、業務アプリをWebアプリとして構築する流行がありましたが、今では、当時のように何でも「Webで」ということはなくなりました。このように状況が変わってきたのは、Webアプリは「手軽」「変更も気軽」という誤ったイメージが払拭され、Windowsアプリと変わらないきちんとした設計が必要だということが再認識されたこと、日本企業の原動力の1つである、様々な配慮がなされたアプリを使ったきめ細かな業務にはWebアプリの操作性では実現できない部分があったことなどが大きな理由ではないかと思います。

 そのため今後の業務用Webアプリには、業務ロジックを正しく実装したバックボーンの柔軟性、AJAXなどによる優れた操作性と表現力などが、さらに求められることが予想されます。以前のようなデザイン主体の構築方法から、従来の業務アプリの作り方へと回帰してきた今だからこそ、業務アプリ構築のノウハウをもった技術者がInputMan for ASP.NET 3.0JやSPREAD for .NET 3.0J Web Forms Editionなどを活用して、使いやすいUIを提供する開発スタイルが主流の一つになっていくのではないでしょうか。

 そこで今回は、SPREAD、InputMan、ActiveReportsを使って、注意点を押さえながら業務用Webアプリを作成するときの基本的な技法を説明していきます。サンプルアプリには、ログイン機能やPDF出力機能など、一般的な業務アプリに必要とされる基本機能を備えてみました。ぜひ参考にしてみてください。

 なお、今回のサンプルでは動作環境構築を簡単にするためにデータの保存場所としてmdbファイルを使用しています。しかし、Webアプリのように多数のアクセスがある環境にmdbファイルを使った場合の安定動作を、マイクロソフトは保障していません(参考リンク:ネットワーク環境におけるその他の推奨事項)。実運用を伴う業務アプリを作成する場合は、SQL ServerやOracle DatabaseなどのRDBMS製品をお使いください。本サンプルはADO.NETで作成されているので、Providerや接続文字列の変更によりmdbファイルからSQL ServerやOracle Databaseに切り替えても、基本となるロジックは変更する必要がありません。

業務用Webアプリに必要なインフラ構成

 業務アプリなどのように重要なデータを取り扱うWebアプリの場合、ブラウザとWebアプリの間はSSLを使ってhttpsを採用するだけではなく、図1にあるようにファイアウォールによってDMZなどを設置するよう、ハードウェアなどのインフラについても考慮する必要があります。図1のような構成はインターネットを使った場合だけでなく社内イントラネットなどでも有用です。

図1 業務Webアプリに必要なインフラ構成
図1 業務Webアプリに必要なインフラ構成

ログイン機能を作成する

 業務アプリならばログイン画面やメニューが必要になる場合が多いと思います。ログイン画面でIDとパスワードを入力してユーザー認証を行う場合、ユーザーテーブルのようなテーブルにパスワードを保存せず、画面での入力値でデータベースの認証を行えれば、次のようなセキュリティ面での有利さを得ることができます。

  • Webアプリがデータベースに接続するためのIDとパスワードをconfigファイルなどでシステム内に保存不要
  • 利用者ごとにデータベースに接続するIDが異なるため、データベースのアクセス権限を使って不要なデータへの操作を禁止
  • トリガーなどの機能を使い、ユーザーIDと操作などを記録することで操作ログをRDBMS側で出力

 今回のサンプルではmdbファイルを使用しているので、データベースの個別ユーザー認証がないため、IDとパスワードが一致すればログインできるという簡単な認証方式としています。

今回のソリューション構造

図2 今回のサンプルのソリューションについて
図2 今回のサンプルのソリューションについて

 今回のサンプルのソリューションは、「CZ1001Service」と「CZ1001Web」の2つのプロジェクトから構成されています。

 CZ1001ServiceはXML Webサービスプロジェクトで、App_Dataフォルダには今回のサンプルのデータストアであるBill.mdbファイルを配置しています。

 CZ1001WebはWebアプリプロジェクトで、スタートアッププロジェクトとしておき、IDEでソリューションを実行するとWebアプリ側の実行結果がブラウザに表示されます。

フォーム認証とログイン画面の実現

 ASP.NETでWebアプリを作成した場合、フォーム認証やログイン画面の指定はweb.configファイルの中で定義します。

リスト1 web.configより抜粋
<configuration>
    :
    :
    :
  <system.web>
    <authentication mode="Forms">
      <forms loginUrl="~/CZ1001Login.aspx" protection="All" timeout="60" path="/"
             defaultUrl="~/CZ1001Menu.aspx" />
    </authentication>
    <authorization>
      <deny users="?" />
    </authorization>
    :
    :
    :

 [System.Web]要素の中にある[authentication]要素と[Authorization]要素がフォーム認証で重要な定義です。[Authorization]-[deny]要素でusers=”?”とし、認証が通らなかったらアクセスが許可されないWebアプリとして定義しています。[authentication]要素でmode=”Forms”と定義することで認証方式はフォーム認証にし、[authentication]-[forms]要素にフォーム認証用のログイン画面をloginUrl="~/CZ1001Login.aspx"と定義しています。

 これらの定義により、このWebアプリが配置されたURLへ認証前にアクセスすると、自動的にCZ1001Login.aspxにリダイレクトされてログイン画面が表示されることになります。

図3 ログイン画面
図3 ログイン画面

 今回のサンプルでは、積極的にCSSを活用しています。そのため、ログイン画面のように認証前の画面でCSSを使う場合、aspxファイルに直接CSSを記述するときはいいのですが、外部定義ファイルにCSSを記述してリスト2のように使用する場合、認証前なのでCSS定義ファイルにアクセスできずにCSSが効きません。

リスト2 login.aspxより抜粋
<link href="css/stylesheet.css" rel="stylesheet" type="text/css" />

 それを回避するために、web.configに[location]要素を定義します。

リスト3 web.configより抜粋
<configuration>
    :
    :
    :
  <location path="css">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>
    :
    :
    :

 リスト3の定義では、[location]要素で指定したcssフォルダについては、[allow]要素にusers="*"を指定し、アクセス許可がなくてもアクセス可能になるようにしています。

ログイン画面での認証処理

 ログイン画面で[OK]をクリックすると、CZ1001Login.aspx.vbのOK_Button.Clickイベントが発生します。

リスト3 web.configより抜粋
Protected Sub OK_Button_Click(ByVal sender As Object, ByVal e As System.EventArgs) _
                              Handles OK_Button.Click
    Dim userID As String = String.Empty
    Dim password As String = String.Empty
    Dim isOK As Boolean = False
    Dim errorMessage As String = String.Empty

    'チェック
    isOK = True
    userID = StrConv(Me.UserId_TextBox.Text, VbStrConv.Narrow Or VbStrConv.Lowercase)
    password = StrConv(Me.Password_TextBox.Text, VbStrConv.Narrow Or VbStrConv.Lowercase)
    'サーバー側チェック
    If isOK Then
        'ここにDBサーバーの認証チェックロジックなどを記述する
        isOK = (userID = password)
        If Not isOK Then
            errorMessage = "利用者IDまたはパスワードに誤りがあります。"
        End If
    End If
    If isOK Then
        Me.Message_Label.Text = String.Empty
        FormsAuthentication.SetAuthCookie(userID, False)
        '共通情報をセッション情報に設定(userIDはMy.User.Name)
        Me.Session("password") = password
        Me.Response.Redirect("~/CZ1001Menu.aspx", True)
    Else
        FormsAuthentication.SignOut()
        Me.Message_Label.Text = errorMessage.Replace("\n", "<br />")
        Me.UserId_TextBox.Focus()
    End If
End Sub
  1. isOK = (userID = password)ならば認証OKとします
  2. 認証OKの場合、FormsAuthentication.SetAuthCookieメソッドにより、指定したユーザーに対して認証チケットを作成してCookieに格納します
  3. 認証に成功したらCZ1001Menu.aspxにリダイレクトして、必ずトップ画面を表示します

 自分が使いたいURLをブックマークしておき、ブックマークからURLを指定するとログイン画面が表示され、ログインしたら自動的に指定したURLにリダイレクトされた方が使い勝手がよい気がするのですが、業務アプリでは「ログインしたら必ずトップ画面が表示されること」という要件がよくあるので、サンプルではリスト3のように認証成功した場合に特定の画面に飛ぶ動きにしています。

 なお、FormsAuthentication.RedirectFromLoginPageメソッドにより、ログイン画面が表示される要因となったURLにリダイレクトすることもできます。

トップ画面を作成する

 サンプルアプリでは、トップ画面で請求書番号を選んで[更新]をクリックすると更新画面、[印刷]をクリックするとPDFを表示します。

図4 トップ画面で請求書番号を選択
図4 トップ画面で請求書番号を選択

 図4のようにサンプルアプリでは、意図的に2つのコンボボックスを配置しています。上が標準のDropDownListコントロール、下がInputManのComboコントロールです。この2つのコントロールにドロップダウンリストの一覧としてデータセットを割り当てます。

リスト5 CZ1001Menu.aspx.vbより抜粋
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
                        Handles Me.Load
    If Not Me.IsPostBack Then
        '最初のロードのときにのみ実行
        Call GetBill()
    End If
End Sub

Private Sub GetBill()
    Dim userID As String = My.User.Name
    Dim password As String = CType(Session("password"), String)

    Using _webs As New CZ1001BoundService.BillBound
        Dim ds As System.Data.DataSet = _webs.GetBillList(userID, password)
        '標準コントロール
        Me.BillNo_DropDownList.DataMember = "BillCondition"
        Me.BillNo_DropDownList.DataSource = ds
        Me.BillNo_DropDownList.DataBind()
        'InputManコントロール
        Me.BillNo_Combo.ListBox.HeaderPane.Visible = True
        Me.BillNo_Combo.ListBox.AutoGenerateColumns = True
        Me.BillNo_Combo.ListBox.AutoWidth = True
        Me.BillNo_Combo.ListBox.TextSubItemIndex = 0
        Me.BillNo_Combo.DataMember = "BillCondition"
        Me.BillNo_Combo.DataSource = ds
        Me.BillNo_Combo.DataBind()
        Me.BillNo_Combo.ListBox.Columns(0).AutoWidth = True
    End Using
End Sub

 標準コントロールとInputManコントロールの大きな違いは、InputManコントロールであれば複数列のドロップダウンリストが作成できる点です。例えば、表示値とコード値のように複数の値を持つようなことも可能です。また、コード値列のVisibleプロパティをFalseにすると、コード値をドロップダウンリストでは見せないようにもできます。

更新画面を作成する

 トップ画面で[更新]をクリックされたときに動作するイベントプロシージャには次のように記述します。

リスト6 更新画面を起動する(CZ1001Menu.aspx.vbより抜粋)
Protected Sub OK_Button_Click(ByVal sender As Object, ByVal e As System.EventArgs) _
                              Handles OK_Button.Click
    If Me.BillNo_Combo.SelectedIndex >= 0 Then
        Me.Session("BillNo") = Me.BillNo_Combo.SelectedItem.SubItems(0).Value
        Me.Response.Redirect("~/entry/CZ1001Entry.aspx")
    Else
        Me.Message_Label.Text = "請求書番号を選択してください。"
    End If
End Sub
  1. [BillNo]セッション変数に選択された請求書番号を設定
  2. RedirectメソッドによりCZ1001Entry.aspxにリダイレクト

更新画面にデータを表示する

図5 更新画面
図5 更新画面
リスト7 更新画面でデータを表示(CZ1001Entry.aspx.vbより抜粋)
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
                        Handles Me.Load
    '複数ウィンドウ対策
    If Me.ViewState("billNo") Is Nothing Then
        Me.ViewState("billNo") = Me.Session("billNo")
    Else
        Me.Session("billNo") = Me.ViewState("billNo")
    End If
    '
    If Not (Me.Session("BillNo") Is Nothing) AndAlso _
       Me.Session("BillNo").ToString.Length > 0 Then
        If Not Me.IsPostBack Then
            '最初のロードのときにのみ実行
            Call Me_Load()
            If Not (Me.Session("billNo") Is Nothing) Then
                Call GetRecords(Me.Session("billNo").ToString)
            Else
                Call GetRecords("")
            End If
        End If
    Else
        My.Response.Redirect("~/CZ1001Menu.aspx")
    End If
End Sub

'''  ''' 指定した請求書の編集を行う ''' 
''' 
''' 
Private Sub GetRecords(ByVal billNo As String)
    Dim userID As String = My.User.Name
    Dim password As String = CType(Session("password"), String)

    Using _webs As New CZ1001BoundService.BillBound
        Dim ds As System.Data.DataSet = _webs.GetRecords(userID, _
                                                         password, _
                                                         billNo)
        'Spread以外の設定
        Me.PreInvoice_Number.Format.Digit = "#########0"
        Me.PreInvoice_Number.DisplayFormat.Digit = "#,###,###,##0"
        Me.PreInvoice_Number.DisplayFormat.PositiveSuffix = "円"
        Me.PreIncome_Number.Format.Digit = "#########0"
        Me.PreIncome_Number.DisplayFormat.Digit = "#,###,###,##0"
        Me.PreIncome_Number.DisplayFormat.PositiveSuffix = "円"
        If ds.Tables("BillCondition").Rows.Count > 0 Then
            With ds.Tables("Customers").Rows(0)
                Me.Name_Label.Text = .Item("CustomerName").ToString
            End With
            With ds.Tables("BillCondition").Rows(0)
                Me.BillNO_Label.Text = .Item("BillNo").ToString
                Me.EndDate_Calendar.SelectedDate = CType(.Item("EndDate").ToString, Date)
                Me.EndDate_Calendar.FocusDate = Me.EndDate_Calendar.SelectedDate
                Me.PreInvoice_Number.Value = CType(.Item("PreInvoice"), Decimal)
                Me.PreIncome_Number.Value = CType(.Item("PreIncome"), Decimal)
            End With
        End If
        Me.FpSpread1.ActiveSheetView.DataMember = "Bill"
        Me.FpSpread1.ActiveSheetView.DataSource = ds
        Me.FpSpread1.DataBind()
        Me.FpSpread1.ActiveSheetView.RecalculateAll()
    End Using
End Sub
  1. Page_Loadでは、セッション変数がウィンドウ間で共通のため、複数ウィンドウで使われたときにも正常に動作するようにViewState変数を併用
  2. Page_Loadでは、PostBack以外のときはGetRecords関数を呼んで値を設定
  3. GetRecordsでは、XML WebサービスのGetRecordsメソッドによりデータを取得
  4. 画面左側の項目は、データテーブルより該当値を取得して設定
  5. 右側の一覧は、DataSourceプロパティにデータセットを指定してDataBindメソッドにより反映
  6. RecalculateAllメソッドで「金額」欄を計算

更新画面の内容でデータを更新する

 更新画面で[更新]をクリックすると、画面での変更値をbill.mdbファイルに書きだすXML Webサービスを呼び出します。XML Webサービスを呼び出すときに画面の値をDatasetに格納する必要があるので、次のリスト8のようなコードを実行します。

リスト8 更新画面でデータを表示する (CZ1001Entry.aspx.vbより抜粋)
Private Sub SetRecords(ByVal billNo As String)
    Dim userID As String = My.User.Name
    Dim password As String = CType(Session("password"), String)

    Me.FpSpread1.SaveChanges()
    Using _webs As New CZ1001BoundService.BillBound
        Dim ds As System.Data.DataSet = CType(Me.FpSpread1.ActiveSheetView.DataSource, _
                                             System.Data.DataSet)

        With ds.Tables("BillCondition").Rows(0)
            .BeginEdit()
            .Item("EndDate") = Me.EndDate_Calendar.SelectedDate.BaseDate
            .Item("PreInvoice") = Me.PreInvoice_Number.Value
            .Item("PreIncome") = Me.PreIncome_Number.Value
            .EndEdit()
        End With
        Try
            Call _webs.SetRecords(ds, userID, password, billNo)
            Me.Message_Label.Text = ""
        Catch ex As Exception
            '例外をログなどに記述
            Me.Message_Label.Text = "エラーが発生しました。"
        End Try
    End Using
End Sub
  1. SaveChangesメソッドにより画面上の変更をデータセットに反映
  2. Spreadにバインドしているデータセットをds変数に設定
  3. 画面の左側の値をds.Tables("BillCondition").Rows(0)に反映
  4. データを更新するXML Webメソッド(SetRecordメソッド)をコール

 私がこのサンプルアプリを作成しているとき、はじめはSaveChangesメソッドの存在に気が付かず、画面上で変更しているのにデータセットが更新されないという事象に悩みましたが、FAQの[全般]-[クライアント側で変更した値をサーバー側で取得したい]の内容を読んで解決しました。非常に重要なメソッドなので、記述忘れのないようにしましょう。

PDFを作成する

 今回のサンプル帳票の定義は、前回の帳票定義を流用する予定でした。しかし、WindowsアプリプロジェクトでActiveReportsの帳票定義を追加すると、コードビハインドにより定義部分とコード部分が分離されたファイルになります。しかし、Webアプリプロジェクトではすべてが1ファイルになります。

図6 ActiveReports定義ファイルのプロジェクトへの追加
図6 ActiveReports定義ファイルのプロジェクトへの追加

 そこで、コードビハインドされていた内容を1ファイルの中の適切な部分にコピーすることで流用しました。

図7 帳票定義のコード流用部分
図7 帳票定義のコード流用部分

PDFを生成するXML Webサービスを作成する

リスト9 PDFを生成するXML Webサービス(CZ1001Pdf.vbより抜粋)
<WebMethod(EnableSession:=False, Description:="請求書のPDFデータを取得する")> _
Public Function GetRecords(ByVal userID As String, _
                           ByVal password As String, _
                           ByVal billNo As String) As Byte()
    Dim memStream As System.IO.MemoryStream
    Dim pdf As PdfExport
    Dim ds As DataSet = Nothing
    Dim rpt As New Seikyu_Report()

    Using _prco As New BillBound
        rpt.DataSource = _prco.GetPrintRecords(userID, password, billNo).Tables(0)
        ' 仮想プリンタを設定します。
        rpt.Document.Printer.PrinterName = ""
        rpt.PageSettings.PaperKind = Drawing.Printing.PaperKind.A4
        rpt.PageSettings.Orientation = Document.PageOrientation.Portrait
        rpt.PageSettings.Margins.Top = ActiveReport.CmToInch(0.5F)
        rpt.PageSettings.Margins.Bottom = ActiveReport.CmToInch(0.5F)
        ' レポートを作成します。
        rpt.Run(False)
        ' PDFエクスポートオブジェクトを生成します。
        pdf = New PdfExport
        ' PDFの出力用のメモリストリームを作成します。
        memStream = New System.IO.MemoryStream
        ' メモリストリームにPDFエクスポートを行います。
        pdf.Security.Use128Bit = True
        pdf.Security.OwnerPassword = "123"
        pdf.Security.Permissions = PdfPermissions.AllowPrint
        pdf.Security.Encrypt = True
        pdf.Export(rpt.Document, memStream)
    End Using
    Return memStream.ToArray()
End Function
  1. XML Webサービスの戻り値はバイト配列として定義
  2. PDFの生成先をMemoryStreamとして定義
  3. XML Webサービスが稼働するサーバーに想定したプリンタが定義されていない可能性があるので、仮想プリンタを定義
  4. Runメソッドで帳票をメモリ上で作成
  5. PDFの属性を指定し、ExportメソッドでMemoryStreamに出力
  6. MemoryStreamの内容をToArrayメソッドでバイト配列に変換して返却

 XML Webサービス側でPDFを作成しているので、ActiveReportsの配布モジュールもXML Webサービスが稼働している内部セグメントにあるサーバーにだけに配置します。PDFに設定するOwnerPasswordなどの設定値や、今回は使っていませんがUserPasswordの設定値などを、DMZよりもさらに安全な内部セグメントにおけるのは安心感も増す配置だと思います。

XML Webサービスで作成したPDFを表示する

 リスト9で生成したPDFを表示するWebページは、図8にあるようにブラウザがPDFと分かるようにContext-Typeを指定しなければなりません。

図8 PDFをブラウザで表示するときの流れ
図8 PDFをブラウザで表示するときの流れ

 そこで、ResponseにContentTypeとして"application/pdf"を指定し、AddHeaderを使ってPDFのバイナリデータである旨を指定します。

リスト10 PDFを表示するWebページ (CZ1001Pdf.aspx.vbより抜粋)
Private Sub GetRecords(ByVal billNo As String)
    Dim userID As String = My.User.Name
    Dim password As String = CType(Session("password"), String)
    Dim pdfStream() As Byte
    Dim memStream As System.IO.MemoryStream = New System.IO.MemoryStream
    Dim pdfFilename As String = IIf(billNo.Trim.Length = 0, "CZ1001", _
                                                            billNo.Trim).ToString

    Using _webs As New CZ1001PdfService.CZ1001Pdf
        'Webサービスからデータを受信
        pdfStream = _webs.GetRecords(userID, password, billNo)
        ' ブラウザに対してPDFドキュメントの適切なビューワを使用するように指定
        Response.ContentType = "application/pdf"
        Response.AddHeader("content-disposition", _
                           "inline; filename=" & pdfFilename & ".PDF")
        ' 出力ストリームにPDFのストリームを出力します。
        Response.BinaryWrite(pdfStream)
        ' バッファリングされているすべての内容をクライアントへ送信します。
        Response.End()
    End Using
End Sub
図9 PDF表示
図9 PDF表示

まとめ

 業務アプリをWebアプリとして作成するときの注意点を踏まえて、SPREAD、InputMan、ActiveReportsを使ってサンプルを作成して基本的な技法を説明してきました。

 Windowsアプリの使い勝手と若干違いはありますが、これら市販コンポーネントの使い方の基本には大きな違いはありませんでした。また、できあがったサンプルアプリの操作性もコードの記述量からは考えられないくらい良いと思います。これは、クライアント側のスクリプトがコンポーネントにより自動生成されているという事が大きいでしょう。

 もし、SPREADとInputManを使わなかったならば、クライアント側スクリプトのコーディングやデバッグなど多くの手間が必要です。Webアプリ開発にも市販コンポーネントを導入するのが費用対効果という面でも有利でしょう。

製品情報

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/4892 2010/02/05 18:30

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング