コンポーネントを活用した業務用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のような構成はインターネットを使った場合だけでなく社内イントラネットなどでも有用です。
ログイン機能を作成する
業務アプリならばログイン画面やメニューが必要になる場合が多いと思います。ログイン画面でIDとパスワードを入力してユーザー認証を行う場合、ユーザーテーブルのようなテーブルにパスワードを保存せず、画面での入力値でデータベースの認証を行えれば、次のようなセキュリティ面での有利さを得ることができます。
- Webアプリがデータベースに接続するためのIDとパスワードをconfigファイルなどでシステム内に保存不要
- 利用者ごとにデータベースに接続するIDが異なるため、データベースのアクセス権限を使って不要なデータへの操作を禁止
- トリガーなどの機能を使い、ユーザーIDと操作などを記録することで操作ログをRDBMS側で出力
今回のサンプルではmdbファイルを使用しているので、データベースの個別ユーザー認証がないため、IDとパスワードが一致すればログインできるという簡単な認証方式としています。
今回のソリューション構造
今回のサンプルのソリューションは、「CZ1001Service」と「CZ1001Web」の2つのプロジェクトから構成されています。
CZ1001ServiceはXML Webサービスプロジェクトで、App_Dataフォルダには今回のサンプルのデータストアであるBill.mdbファイルを配置しています。
CZ1001WebはWebアプリプロジェクトで、スタートアッププロジェクトとしておき、IDEでソリューションを実行するとWebアプリ側の実行結果がブラウザに表示されます。
フォーム認証とログイン画面の実現
ASP.NETでWebアプリを作成した場合、フォーム認証やログイン画面の指定は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にリダイレクトされてログイン画面が表示されることになります。
今回のサンプルでは、積極的にCSSを活用しています。そのため、ログイン画面のように認証前の画面でCSSを使う場合、aspxファイルに直接CSSを記述するときはいいのですが、外部定義ファイルにCSSを記述してリスト2のように使用する場合、認証前なのでCSS定義ファイルにアクセスできずにCSSが効きません。
<link href="css/stylesheet.css" rel="stylesheet" type="text/css" />
それを回避するために、web.configに[location]要素を定義します。
<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イベントが発生します。
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
isOK = (userID = password)
ならば認証OKとします- 認証OKの場合、FormsAuthentication.SetAuthCookieメソッドにより、指定したユーザーに対して認証チケットを作成してCookieに格納します
- 認証に成功したらCZ1001Menu.aspxにリダイレクトして、必ずトップ画面を表示します
自分が使いたいURLをブックマークしておき、ブックマークからURLを指定するとログイン画面が表示され、ログインしたら自動的に指定したURLにリダイレクトされた方が使い勝手がよい気がするのですが、業務アプリでは「ログインしたら必ずトップ画面が表示されること」という要件がよくあるので、サンプルではリスト3のように認証成功した場合に特定の画面に飛ぶ動きにしています。
なお、FormsAuthentication.RedirectFromLoginPageメソッドにより、ログイン画面が表示される要因となったURLにリダイレクトすることもできます。
トップ画面を作成する
サンプルアプリでは、トップ画面で請求書番号を選んで[更新]をクリックすると更新画面、[印刷]をクリックするとPDFを表示します。
図4のようにサンプルアプリでは、意図的に2つのコンボボックスを配置しています。上が標準のDropDownListコントロール、下がInputManのComboコントロールです。この2つのコントロールにドロップダウンリストの一覧としてデータセットを割り当てます。
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にすると、コード値をドロップダウンリストでは見せないようにもできます。
更新画面を作成する
トップ画面で[更新]をクリックされたときに動作するイベントプロシージャには次のように記述します。
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
- [BillNo]セッション変数に選択された請求書番号を設定
- RedirectメソッドによりCZ1001Entry.aspxにリダイレクト
更新画面にデータを表示する
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
- Page_Loadでは、セッション変数がウィンドウ間で共通のため、複数ウィンドウで使われたときにも正常に動作するようにViewState変数を併用
- Page_Loadでは、PostBack以外のときはGetRecords関数を呼んで値を設定
- GetRecordsでは、XML WebサービスのGetRecordsメソッドによりデータを取得
- 画面左側の項目は、データテーブルより該当値を取得して設定
- 右側の一覧は、DataSourceプロパティにデータセットを指定してDataBindメソッドにより反映
- RecalculateAllメソッドで「金額」欄を計算
更新画面の内容でデータを更新する
更新画面で[更新]をクリックすると、画面での変更値をbill.mdbファイルに書きだすXML Webサービスを呼び出します。XML Webサービスを呼び出すときに画面の値をDatasetに格納する必要があるので、次のリスト8のようなコードを実行します。
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
- SaveChangesメソッドにより画面上の変更をデータセットに反映
- Spreadにバインドしているデータセットをds変数に設定
- 画面の左側の値をds.Tables("BillCondition").Rows(0)に反映
- データを更新するXML Webメソッド(SetRecordメソッド)をコール
私がこのサンプルアプリを作成しているとき、はじめはSaveChangesメソッドの存在に気が付かず、画面上で変更しているのにデータセットが更新されないという事象に悩みましたが、FAQの[全般]-[クライアント側で変更した値をサーバー側で取得したい]の内容を読んで解決しました。非常に重要なメソッドなので、記述忘れのないようにしましょう。
PDFを作成する
今回のサンプル帳票の定義は、前回の帳票定義を流用する予定でした。しかし、WindowsアプリプロジェクトでActiveReportsの帳票定義を追加すると、コードビハインドにより定義部分とコード部分が分離されたファイルになります。しかし、Webアプリプロジェクトではすべてが1ファイルになります。
そこで、コードビハインドされていた内容を1ファイルの中の適切な部分にコピーすることで流用しました。
PDFを生成するXML Webサービスを作成する
<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
- XML Webサービスの戻り値はバイト配列として定義
- PDFの生成先をMemoryStreamとして定義
- XML Webサービスが稼働するサーバーに想定したプリンタが定義されていない可能性があるので、仮想プリンタを定義
- Runメソッドで帳票をメモリ上で作成
- PDFの属性を指定し、ExportメソッドでMemoryStreamに出力
- MemoryStreamの内容をToArrayメソッドでバイト配列に変換して返却
XML Webサービス側でPDFを作成しているので、ActiveReportsの配布モジュールもXML Webサービスが稼働している内部セグメントにあるサーバーにだけに配置します。PDFに設定するOwnerPasswordなどの設定値や、今回は使っていませんがUserPasswordの設定値などを、DMZよりもさらに安全な内部セグメントにおけるのは安心感も増す配置だと思います。
XML Webサービスで作成したPDFを表示する
リスト9で生成したPDFを表示するWebページは、図8にあるようにブラウザがPDFと分かるようにContext-Typeを指定しなければなりません。
そこで、ResponseにContentTypeとして"application/pdf"
を指定し、AddHeaderを使ってPDFのバイナリデータである旨を指定します。
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
まとめ
業務アプリをWebアプリとして作成するときの注意点を踏まえて、SPREAD、InputMan、ActiveReportsを使ってサンプルを作成して基本的な技法を説明してきました。
Windowsアプリの使い勝手と若干違いはありますが、これら市販コンポーネントの使い方の基本には大きな違いはありませんでした。また、できあがったサンプルアプリの操作性もコードの記述量からは考えられないくらい良いと思います。これは、クライアント側のスクリプトがコンポーネントにより自動生成されているという事が大きいでしょう。
もし、SPREADとInputManを使わなかったならば、クライアント側スクリプトのコーディングやデバッグなど多くの手間が必要です。Webアプリ開発にも市販コンポーネントを導入するのが費用対効果という面でも有利でしょう。