Webアプリケーション開発において、特にフォーム要素が多い場合に、繰り返し発生するデータバインディングやAJAX処理の記述は開発者の負担となりがちです。本稿では、ASP.NETマスターページとjQuery EasyUIを組み合わせることで、これらの処理を汎用的に実装し、開発効率を向上させる手法について解説します。
マスターページによるクライアントサイド処理の一元化
jQuery EasyUIのフォーム機能は、データの検証、送信、リセット、そしてフォームへの値の自動割り当てといった便利な機能を提供します。これらの機能を最大限に活用し、かつ共通化するために、ASP.NETマスターページを利用します。
マスターページでは、必要なJavaScriptライブラリ(jQuery本体、jQuery EasyUI関連スクリプト)を一元的に読み込みます。さらに、フォームのデータ読み込みや送信に使用する共通のJavaScript変数を定義し、ページの読み込み時にデータ自動バインディングを実行するロジックを配置します。
以下に、マスターページに埋め込むJavaScriptの概念的なコードを示します。
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="WebApp.SiteMaster" %>
<!-- ... HTML head and body structure ... -->
<head runat="server">
<title></title>
<!-- jQuery EasyUI CSS -->
<link href="/Assets/easyui/themes/default/easyui.css" rel="stylesheet" type="text/css" />
<link href="/Assets/easyui/themes/icon.css" rel="stylesheet" type="text/css" />
<!-- Custom CSS -->
<link href="/Styles/app.css" rel="stylesheet" type="text/css" />
<!-- jQuery Core -->
<script src="/Scripts/jquery-3.x.x.min.js" type="text/javascript"></script>
<!-- jQuery EasyUI -->
<script src="/Assets/easyui/jquery.easyui.min.js" type="text/javascript"></script>
<script src="/Assets/easyui/locale/easyui-lang-ja.js" type="text/javascript"></script>
<!-- Application-specific common scripts -->
<script src="/Scripts/common-app.js" type="text/javascript"></script>
<asp:ContentPlaceHolder ID="HeadContent" runat="server">
</asp:ContentPlaceHolder>
</head>
<body>
<script type="text/javascript">
// AJAXエンドポイントのURL
var dataProcessorUrl = '/Api/DataHandler.ashx';
// 現在のフォームの識別子(例: ページ名)
var currentFormType = "<%=System.IO.Path.GetFileNameWithoutExtension(HttpContext.Current.Request.Path) %>";
// クエリ文字列からエンティティIDを取得
var entityId = '<%=Request.QueryString["ID"] ?? "" %>';
// クエリ文字列から自動ロードフラグを取得 (編集モードか新規モードか)
var shouldAutoLoad = ('<%=Request.QueryString["loadData"] ?? "0" %>' === '1');
$(function () {
// カスタム初期化関数が存在すれば実行(例: dataProcessorUrlなどを変更する場合)
if (typeof window.onFormInit === 'function') {
window.onFormInit();
}
// 編集モードでIDが指定されている場合、フォームデータを自動ロード
if (shouldAutoLoad && entityId !== '') {
$('#dataForm').form('load', dataProcessorUrl +
'?action=load&type=' + currentFormType + '&id=' + entityId +
"&ts=" + new Date().getTime()); // キャッシュ対策
}
});
</script>
<asp:ContentPlaceHolder ID="MainContent" runat="server">
</asp:ContentPlaceHolder>
</body>
上記のコードからわかる主な点は以下の通りです。
dataProcessorUrl: 全てのAJAXリクエストを処理する共通ハンドラのURLを定義します。currentFormType: 現在のページ名をサーバー側で取得し、このJavaScript変数に割り当てます。これにより、バックエンドでどのフォームからのリクエストかを識別できます。entityId,shouldAutoLoad: クエリ文字列から取得したIDと自動ロードフラグに基づいて、フォームのデータバインディングの要否を判断します。$('#dataForm').form('load', ...):shouldAutoLoadが真の場合、jQuery EasyUIのform('load')メソッドを使って、指定されたURLからJSONデータを取得し、フォーム要素に自動的に値を割り当てます。フォームのIDがdataFormであることに注意してください。onFormInit関数: フォームの自動ロードが実行される前に、これらのグローバル変数の値を変更したり、その他の初期化処理を行うためのフックポイントを提供します。
バックエンドAJAXハンドラの実装
クライアントからのAJAXリクエストを処理するために、汎用的なASHXハンドラ(例: DataHandler.ashx)を実装します。このハンドラは、リクエストに含まれるtypeパラメータ(currentFormTypeに対応)とactionパラメータ(データロード、追加、更新など)に基づいて、適切な処理ロジックを実行し、結果をJSON形式で返します。
以下に、ユーザー管理フォーム(例: UserManagementForm)のデータ処理ロジックの抜粋を示します。
// DataHandler.ashx.cs (または類似の汎用ハンドラ)
public class DataHandler : IHttpHandler
{
private readonly AppDataContext _dbContext = new AppDataContext(); // LINQ to SQL DataContext
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "application/json"; // デフォルトをJSONに設定
string formType = context.Request["type"] ?? string.Empty;
string action = context.Request["action"] ?? string.Empty;
switch (formType)
{
case "UserManagementForm":
HandleUserManagementForm(context, action);
break;
// 他のフォームタイプに対する処理...
case "ProductEntryForm":
// ...
break;
default:
context.Response.Write("{\"success\":false,\"message\":\"不明なフォームタイプです。\"}");
break;
}
}
private void HandleUserManagementForm(HttpContext context, string action)
{
int? recordId = null;
if (!string.IsNullOrEmpty(context.Request["id"]))
{
recordId = int.Parse(context.Request["id"]);
}
switch (action)
{
case "load": // データロード(編集用)
if (recordId.HasValue)
{
var userRecord = _dbContext.AppUsers.FirstOrDefault(u => u.Id == recordId.Value);
if (userRecord != null)
{
context.Response.Write(userRecord.ToJson()); // ToJson() はカスタム拡張メソッド
}
else
{
context.Response.Write("{\"success\":false,\"message\":\"ユーザーが見つかりません。\"}");
}
}
break;
case "save": // データ保存(新規または更新)
if (recordId.HasValue) // 更新の場合
{
var existingUser = _dbContext.AppUsers.FirstOrDefault(u => u.Id == recordId.Value);
if (existingUser != null)
{
existingUser.LoginName = context.Request["LoginName"]?.Trim();
existingUser.RoleId = int.Parse(context.Request["RoleId"] ?? "0");
if (!string.IsNullOrEmpty(context.Request["Password"]?.Trim()))
{
existingUser.PasswordHash = PasswordUtility.HashPassword(context.Request["Password"].Trim());
}
existingUser.IsActive = (context.Request["IsActive"] == "1");
_dbContext.SubmitChanges();
context.Response.Write("{\"success\":true,\"message\":\"更新しました。\"}");
}
else
{
context.Response.Write("{\"success\":false,\"message\":\"更新対象が見つかりません。\"}");
}
}
else // 新規追加の場合
{
if (string.IsNullOrEmpty(context.Request["LoginName"]?.Trim()))
{
context.Response.Write("{\"success\":false,\"message\":\"ログイン名を入力してください。\"}");
return;
}
if (_dbContext.AppUsers.Any(u => u.LoginName == context.Request["LoginName"].Trim()))
{
context.Response.Write("{\"success\":false,\"message\":\"このログイン名は既に使用されています。\"}");
return;
}
if (string.IsNullOrEmpty(context.Request["Password"]?.Trim()))
{
context.Response.Write("{\"success\":false,\"message\":\"パスワードを入力してください。\"}");
return;
}
if (context.Request["Password"] != context.Request["ConfirmPassword"])
{
context.Response.Write("{\"success\":false,\"message\":\"パスワードが一致しません。\"}");
return;
}
AppUser newUser = new AppUser
{
LoginName = context.Request["LoginName"].Trim(),
PasswordHash = PasswordUtility.HashPassword(context.Request["Password"].Trim()),
RoleId = int.Parse(context.Request["RoleId"] ?? "0"),
IsActive = (context.Request["IsActive"] == "1"),
CreatedAt = DateTime.Now
};
_dbContext.AppUsers.InsertOnSubmit(newUser);
_dbContext.SubmitChanges();
context.Response.Write("{\"success\":true,\"message\":\"ユーザーを追加しました。\"}");
}
break;
// ... その他のアクション(削除、一覧取得など)
case "getRoles": // 例: ComboBox用の役割リスト取得
var roles = _dbContext.UserRoles.Select(r => new { r.Id, r.RoleName }).ToList();
context.Response.Write(roles.ToJson());
break;
default:
context.Response.Write("{\"success\":false,\"message\":\"不明なアクションです。\"}");
break;
}
}
// ToJson() 拡張メソッドの概念(例: Newtonsoft.Jsonを使用)
// public static string ToJson(this object obj) { return JsonConvert.SerializeObject(obj); }
public bool IsReusable => false;
}
このバックエンドロジックのポイントは以下の通りです。
- リクエストの
typeとactionパラメータに基づいて処理を分岐させます。 loadアクションの場合、idパラメータを使用してデータベースから対応するレコードを取得し、カスタムのToJson()拡張メソッド(通常はNewtonsoft.Jsonのようなライブラリを使用)でJSON文字列に変換して返します。saveアクションの場合、idパラメータの有無で新規作成か更新かを判断し、フォームから送信されたデータを使ってデータベース操作(LINQ to SQLのInsertOnSubmit,SubmitChanges)を行います。- 入力検証もサーバーサイドで行い、エラーメッセージをJSON形式で返します。
- 例として、コンボボックス用のデータ取得アクション
getRolesも示されています。
LINQ to SQLによる汎用ページング処理
データグリッドなどの一覧表示で必要となるページング処理も、LINQ to SQLを使用して汎用的に実装できます。パフォーマンス要件が高い場合はストアドプロシージャの利用も検討されますが、小規模プロジェクトでは以下の手法が迅速な開発に寄与します。
// データハンドラ内、または共通ユーティリティクラスに定義
private int _currentPage = 1; // クエリ文字列 "page" から取得
private int _itemsPerPage = 10; // クエリ文字列 "rows" から取得
// コンストラクタや初期化メソッドでリクエストパラメータを解析
public DataHandler()
{
if (HttpContext.Current.Request["page"] != null)
_currentPage = int.Parse(HttpContext.Current.Request["page"]);
if (HttpContext.Current.Request["rows"] != null)
_itemsPerPage = int.Parse(HttpContext.Current.Request["rows"]);
}
/// <summary>
/// IQueryable<T>からページングされたJSONデータを生成します。
/// </summary>
/// <typeparam name="T">エンティティの型</typeparam>
/// <param name="sourceData">ページング対象のIQueryableデータ</param>
/// <returns>totalとrowsを含むJSON文字列</returns>
private string GeneratePaginatedJson<T>(IQueryable<T> sourceData)
{
int totalCount = sourceData.Count();
var paginatedItems = sourceData
.Skip((_currentPage - 1) * _itemsPerPage)
.Take(_itemsPerPage)
.ToList(); // 実際にDBからデータを取得
return string.Format("{{\"total\":{0},\"rows\":{1}}}", totalCount, paginatedItems.ToJson());
}
フロントエンドHTMLの構成
マスターページと連携する個別のASP.NETコンテンツページは、以下の構造を持ちます。ここでもHTML標準のフォーム要素を使用し、jQuery EasyUIのコンポーネントとして構成します。
<%@ Page Title="ユーザー編集" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
CodeBehind="UserEdit.aspx.cs" Inherits="WebApp.Pages.UserManagement.UserEdit" %>
<asp:Content ID="ContentHead" ContentPlaceHolderID="HeadContent" runat="server">
<!-- ページ固有のCSSやJSがあればここに記述 -->
</asp:Content>
<asp:Content ID="ContentMain" ContentPlaceHolderID="MainContent" runat="server">
<div class="easyui-layout" data-options="fit:true" style="text-align: left;">
<div data-options="region:'center',border:false" style="padding: 10px; background: #fff; border: 1px solid #ccc;">
<form id="dataForm" method="post">
<input type="hidden" name="id" value="<%=Request.QueryString["ID"] %>" />
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td><label for="LoginName">ログイン名:</label></td>
<td>
<input class="easyui-validatebox" style="width: 300px;" type="text"
required="true" data-options="validType:'length[0,20]'" name="LoginName" />
</td>
</tr>
<tr>
<td><label for="RoleId">所属役割:</label></td>
<td>
<input class="easyui-combobox" style="width: 300px;"
data-options="valueField:'Id',textField:'RoleName',panelHeight:'auto',editable:false,
url:'/Api/DataHandler.ashx?action=getRoles&type=UserManagementForm',required:true"
name="RoleId" />
</td>
</tr>
<tr>
<td colspan="2" style='color: Red'>
編集時にパスワードを入力すると、パスワードが再設定されます。
</td>
</tr>
<tr>
<td><label for="Password">パスワード:</label></td>
<td>
<input class="easyui-validatebox" style="width: 300px;" type="password"
data-options="validType:'length[6,20]'" id="passwordField" name="Password" />
</td>
</tr>
<tr>
<td><label for="ConfirmPassword">パスワード確認:</label></td>
<td>
<input class="easyui-validatebox" style="width: 300px;" type="password"
data-options="validType:'length[6,20]'" id="confirmPasswordField" name="ConfirmPassword" />
</td>
</tr>
<tr>
<td><label for="IsActive">アクティブ:</label></td>
<td>
<input type="checkbox" name="IsActive" value="1" />
</td>
</tr>
</table>
</form>
</div>
<div data-options="region:'south',border:false" style="text-align: right; padding: 5px 5px 5px 0;">
<a class="easyui-linkbutton" data-options="iconCls:'icon-save'" href="javascript:void(0)" onclick="submitFormData();">
保存</a>
</div>
</div>
<script type="text/javascript">
// フォームデータ送信関数
function submitFormData() {
$('#dataForm').form('submit', {
url: dataProcessorUrl + '?action=save&type=' + currentFormType,
onSubmit: function () {
return $(this).form('validate'); // クライアント側検証
},
success: function (result) {
var data = JSON.parse(result);
if (data.success) {
$.messager.alert('情報', data.message, 'info', function () {
// 成功時の処理(例: ダイアログを閉じる、一覧をリロードするなど)
});
} else {
$.messager.alert('エラー', data.message, 'error');
}
}
});
}
$(function () {
// 新規作成時(shouldAutoLoadがfalse)はパスワードフィールドを必須にする
if (!shouldAutoLoad) {
$('#passwordField, #confirmPasswordField').validatebox({ required: true });
} else {
// 編集時(shouldAutoLoadがtrue)はパスワードフィールドが入力された場合のみ確認を必須にする
$('#passwordField').on('input change', function () {
var isPasswordEntered = ($(this).val().length > 0);
$('#confirmPasswordField').validatebox({ required: isPasswordEntered });
});
}
});
</script>
</asp:Content>
フロントエンドのHTMLページでは、以下の点に注目してください。
- マスターページから継承し、
<asp:Content>コントロールで特定の内容を埋め込みます。 - フォーム要素(
<input>,<select>など)には、jQuery EasyUIのクラス(例:easyui-validatebox,easyui-combobox)とdata-options属性を使用して、EasyUIコンポーネントとして動作させます。 - 各フォーム要素の
name属性は、バックエンドで処理されるJSONデータまたはPOSTパラメータの名前と一致させる必要があります。 - パスワードフィールドの検証ロジックは、新規作成時と編集時で異なる要件に対応するため、JavaScriptで条件分岐しています。編集時はパスワードが入力された場合にのみ、確認用パスワードを必須とします。
submitFormData()関数は、jQuery EasyUIのform('submit')メソッドを利用して、フォームデータをAJAXで送信します。