ASP.NETマスターページとjQuery EasyUIを用いた汎用フォームのデータ連携

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;
}

このバックエンドロジックのポイントは以下の通りです。

  • リクエストのtypeactionパラメータに基づいて処理を分岐させます。
  • 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で送信します。

タグ: jQuery EasyUI ASP.NET Web Forms AJAX処理 マスターページ C#

7月27日 21:15 投稿