'Practice'에 해당되는 글 2건

  1. 2010.02.22 Practice : jQuery Dialog & Validity
  2. 2010.02.09 Practice : jQuery DataTables


이번 포스트에서는 jQuery UI Dialog Validity라는 플러그인을 사용해보도록 하겠습니다. 먼저 아래의 사이트에서 다운로드 받으시면 됩니다.
http://jqueryui.com/
http://validity.thatscaptaintoyou.com/

Validity
에서 Validation 메세지를 보여주는 방식은 3가지 있습니다. Label, Modal, Summary입니다. Summary방식을 사용하면 Validation 메세지를 한꺼번에 지정된 위치에 보여줄 수 있어 Summary 방식을 사용하였습니다. 그런데 여러 개의 Dialog Summary를 사용하다 보니 Dialog에서 Validation를 체크하는 코드가 실행되고 난 이후에 다른 Dialog에의 Validation메세지가 한꺼번에 보여지는 문제점이 있었습니다. Validity 코드를 살펴보니 validity-summary-container라고 class가 지정된 Dialog Validation를 사용하고 있었기 때문입니다. 그래서 저는 마스터 페이지에 Validation메세지를 보여주는 div태그를 하나 두고 해당 div태그를 Dialog prepend해주는 방식으로 사용했습니다.

마스터 페이지 추가 코드

<div id="validity" style="display:none" class="validity-summary-container">

    <ul></ul>

</div>


초기화 코드

$(function() {

 

    $.validity.setup({ outputMode: "summary" });

 

    $("#dialog").dialog({

        width: "auto",

        bgiframe: true,

        autoOpen: false,

        modal: true,

        resizable: false,

        close: function() {

            var allFields = $([]).add($("#id")).add($("#name"));

            allFields.val('');

            $.validity.clear();

        }

    });

 

    $("#create").click(Create);

    $("#modify").click(Modify);

 

});

 

 

<input type="button" id="create" value="Create" />

<input type="button" id="modify" value="Modify" />

   

<div id="dialog" >

    <table>

        <tr>

            <td>ID</td>

            <td><input type="text" id="id" /></td>

        </tr>

        <tr>

            <td>Name</td>

            <td><input type="text" id="name"/></td>

        </tr>

    </table>

</div>

=> 먼저 Validity의 모드를 Summary로 해준 것을 볼 수 있고 dialog()를 사용하여 해당 div태그를 초기화 해주었습니다. 이렇게 초기화를 해주게 되면 해당 div태그는 화면에 보이지 않는 효과가 있습니다. 그리고 Create, Modify 이벤트를 등록해주는 것을 볼 수 있습니다. Delete와 같은 경우에는 Delete에 해당하는 Dialog를 따로 만들어서 사용하는 것이 바람직해 보여 여기서는 제외하였습니다.
close
를 살펴보면 재미있는 문법이 보입니다. jQuery Object를 배열 형태로 만들어서 id name이라는 jQuery Object Add해주고 val('')이라는 메소드를 사용하여 배열 안에 존재하는 모든 jQuery Object를 초기화 해주는 것을 볼 수 있습니다. 실질적으로 여러 데이터가 존재하고 초기화 해주는 내용이 다르다면 하나씩 초기화 해주는 것이 맞지만 여기서는 해당 문법이 재미있어 보여 한 번 사용해보았습니다. 그리고 Validity Clear()메소드를 호출해주어서 Validation메세지도 초기화 해줍니다. 여기서 close가 가장 중요한 부분인데 이제 Create, Modify에서 만들게 될 Buttons에서 이 close를 호출해줌으로써 Dialog의 상태가 항상 초기화 상태로 돌아갈 수 있게 되는 것입니다. 그러면 Create에 대한 코드를 먼저 보도록 하겠습니다.

function Validate() {

    $.validity.start();

    $("#id").require();

    $("#name").require();

    var result = $.validity.end();

 

    $("#dialog").prepend($("#validity"));

    return result.valid;

}

          

function Create() {

 

    var options = {

        title: "Create",

        buttons: {

            OK: function() {

                if (Validate()) {

                    // Execute Server Side Call

                    $(this).dialog('close');

                }

            },

            Cancle: function() {

                $(this).dialog('close');

            }

        }

    };

   

    $('#dialog').dialog('option', options);

    $('#dialog').dialog('open');

}

=> $(function(){})에서 Create이벤트를 추가해주었기 때문에 Create라는 button를 클릭하게 되면 위의 Create 함수가 실행이 됩니다. 먼저 Dialog title를 지정해주고 button를 만들어줍니다. 여기서는 ok, cancle 두 개의 버튼을 만들어주는데 공통적으로 $(this).dialog('close')를 호출하는 것을 볼 수 있습니다. 바로 이 부분이 이전에 초기화 코드에서의 close부분을 호출해주는 것입니다. ok버튼에서는 Validate라는 함수를 호출해줍니다. Validate라는 함수를 살펴보면 require라는 Validity에서 제공해주는 함수를 사용하는데 id, name중에 하나라도 공백으로 놔두고 ok버튼을 클릭하면 false가 리턴되어 Validation를 체크하는 것입니다. 이외에도 match, range, greaterThan 등등 많은 함수를 Validity에서 제공해주며 사용방법은 Validity 사이트의 문서를 참고하시기 바랍니다.

거의 모든 경우의 Create Modify의 양식은 똑같을 것입니다. 이번에는 Modify 함수의 코드를 보도록 하겠습니다.

function Modify() {

   
var
options = {
        title:
"Modify"
,
        buttons: {
            OK:
function
() {
               
if
(validate()) {
                   
// Execute Server Side Call                   
                    $(this).dialog('close');

                }

            },

            Cancle: function() {

                $(this).dialog('close');

            }

        }

    };

 

    $('#dialog').dialog('option', options);

    $('#dialog').dialog('open');

}

=> Create와 유사합니다. 실질적으로 모두 구현을 한다면 buttons에의 ok버튼의 Server Side를 호출해주는 부분과 Modify기 때문에 Dialog를 띄우기 전에 id, name에 수정되기 전의 값을 조회해서 넣어주는 코드가 필요합니다.
jQuery UI
Dialog를 이용하여 CRUD를 해줄 경우에 이러한 방법으로 구현하는 것이 제가 사용하는 가장 심플한 방법입니다.

 

 

Posted by resisa
,

이전 포스트에서 jQuery Table Plugin 중에 DataTables을 소개해드렸습니다. 이번 포스트에서는 DataTables를 사용하는 제가 알고 있는 가장 Best Practics한 사용법을 설명드리도록 하겠습니다.

먼저 필요한 기능들을 나열해보도록 하겠습니다.
 - 헤더를 클릭하여 열 정렬이 가능해야 한다.
 - Paging이 가능해야 한다. (1, 2, 3, 4 숫자로 표시되는 Paging)
 - 해당 Page의 보이는 Data만을 조회하여야 한다. (성능이 떨어지면 안된다.)
 - CSS를 통해서 사용자 정의 스타일이 가능해야 한다.

가장 중요한 기능은 Page를 나타내는 테이블을 구현하려고 하다 보니 성능적인 문제가 제일 문제가 되는데 이러한 것은 DataTables의 Server-Side를 호출해주는 방법으로 해결할 수 있습니다. DataTables의 Example을 살펴보면 Server-Side코드들이 php로 작성된 코드들이라 ASP.NET MVC에 맞는 코드를 Forum에서 찾아보았습니다. 여기서 한가지 알아야 할 사항이 있는데 Client에서 Sever쪽 액션 메소드(MVC)를 호출하고 액션 메소드의 결과를 Json타입으로 직렬화하여 리턴해줄 때의 Data형태를 DataTables이 알 수 있는 형태로 리턴을 해주어야 합니다. (물론 일반적인 List<T>타입을 Json타입으로 Serialize해준 후에 Client측에서 DataTables이 원하는 Data형태로 변경해줄 수도 있습니다.)

1. http://weblogs.asp.net/zowens/archive/2010/01/19/jquery-datatables-plugin-meets-c.aspx
2. http://datatables.net/forums/comments.php?DiscussionID=932&page=1#Item_0

첫 번째 방법으로 먼저 시도를 해보았는데 사용하는 방법이 쉽지만 'Caveat'부분을 보면 버그가 있다는 경고 메세지가 있고 실제로 시도해보았을 경우에 단순히 하나의 테이블에서 string의 타입만 있을 경우에는 문제가 없었지만 여러 가지 타입이나 테이블 조인의 경우에 문제가 있어 아쉽게 사용하지 못하게 되었습니다.

두 번째 방법을 살짝 Custom하게 고쳐서 사용해보도록 하겠습니다.

ORM은 Entity Framework를 사용하였으면 DB모델은 아래 그림과 같으며 Script파일을 첨부하였습니다.



Client-Side
var logTable;

 

$(function() {

 

    $("#logList tbody").click(function(event) {

        $(logTable.fnSettings().aoData).each(function() {

            $(this.nTr).removeClass('row_selected');

        });

        $(event.target.parentNode).addClass('row_selected');

    });

 

    logTable = $("#logList").dataTable({

        "bAutoWidth": false,

        "iDisplayLength": 10,

        "bLengthChange": false,

        "bInfo": false,

        "sPaginationType": "full_numbers",

        "bProcessing": true,

        "bServerSide": true,

        "aaSorting": [[0, "asc"]],

        "aoColumns": [

                        { "bVisible": true, "sClass": "Center" },

                        null,

                        null

                            ],

        "sAjaxSource": '<%= Url.Action("GetAllChildren", "Home") %>'

    });

});

 

<table id="logList" class="display">

    <thead>

        <tr>

            <th>P_ID</th>

            <th>P_Name</th>

            <th>C_COUNT</th>

        </tr>

    </thead>

    <tbody></tbody>

</table>

=> click()부분은 Table에서 해당 로우를 선택하였을 경우에 해당 로우만 다른 색깔로 표시하는 것이며 클릭하였을 경우에 액션을 취하고 싶을 경우에 사용하시면 됩니다. logTable이라는 변수를 선언해주는 이유는 DataTable의 sAjaxSource부분이 MVC의 액션 메소드를 호출해주는 부분인데 액션 메소드 호출시에 특정한 상황에 따라서 동적으로 변하는 매개변수를 넘길 경우가 있을 수 있습니다. 이럴경우에 logTable이라는 변수를 사용해서 sAjaxSource를 내용을 고치고 Table의 내용도 변경된 내용으로 Refresh하기 위함입니다. 기타 옵션들은 그 단어만 봐도 어떤 것인지 어느 정도는 알 수 있으며 자세한 사항은 DataTables 사이트를 참조하시기 바랍니다. DataTables를 다운 받아보시면 Demo_Table.css파일이 있는데 이 파일에서 스타일을 원하는 형태로 변경하셔서 사용하실 수 있습니다.

Server-Side Helper Class
public class GridParams

{

    public int iDisplayStart { get; set; }

    public int iDisplayLength { get; set; }

    public string sSearch { get; set; }

    public bool bEscapeRegex { get; set; }

    public int iColumns { get; set; }

    public int iSortingCols { get; set; }

    public int iSortCol_0 { get; set; }

    public string sSortDir_0 { get; set; }

    public int sEcho { get; set; }

    public bool bSortable_0 { get; set; }

    public bool bSearchable_0 { get; set; }

}

 

public class FormatedList

{

    public int sEcho { get; set; }

    public int iTotalRecords { get; set; }

    public int iTotalDisplayRecords { get; set; }

    public string[][] aaData { get; set; }

}

 

public static class QuerableExtensions

{

    public static IQueryable<T> OrderBy<T>(this IQueryable<T> source, string propertyName, bool asc)

    {

        var type = typeof(T);

        string methodName = asc ? "OrderBy" : "OrderByDescending";

        var property = type.GetProperty(propertyName);

        var parameter = Expression.Parameter(type, "p");

        var propertyAccess = Expression.MakeMemberAccess(parameter, property);

        var orderByExp = Expression.Lambda(propertyAccess, parameter);

        MethodCallExpression resultExp = Expression.Call(typeof(Queryable), methodName, new Type[] { type, property.PropertyType }, source.Expression, Expression.Quote(orderByExp));

        return source.Provider.CreateQuery<T>(resultExp);

    }

}

 

public static class GridsUtils

{

    public delegate string[] GetStringDelegate<T>(T obj);

 

    public static string[][] GetStrings<T>(this IQueryable<T> source, GetStringDelegate<T> del)

    {

        string[][] retArray = new string[source.Count()][];

        int i = 0;

        foreach (var s in source)

        {

            retArray.SetValue(del(s), i);

            i++;

        }

        return retArray;

    }

}

=> GridParams클래스는 열을 클릭하거나 페이지를 클릭하거나 조회 상자에서 조회하는 내용 등의 정보를 Server쪽에서 받기 쉽게 정의해놓은 클래스입니다. FormatList클래스는 위의에 말했듯이 DataTable이 인식할 수 있는 정보를 받기 위해 정의해놓은 클래스입니다. 참고적으로 iTotalDisplayRecord는 Page에 나타나는 숫자를 의미합니다. QuerableExtensions클래스는 정렬 시에 사용하기 위해서 확장해놓은 클래스인데 해당 클래스의 프로퍼티와 asc, desc인지 여부를 Expression으로 구현해놓았습니다. 그런데 헤더 정렬시에 꼭 프로퍼티 이름만으로 정렬을 하는 것이 아니라 자식 프로퍼티의 개수로도 소트할 수 있기 때문에 꼭 사용하지는 않습니다. GridsUtils클래스는 IQeuryable<T>형태로 반환된 결과물을 string[][]의 형태로 만들어주기 위한 Helper 클래스입니다.

Server-Side 액션 메소드
public ActionResult GetAllParent(GridParams gridParams)

{

    using (TestEntities Context = new TestEntities())

    {

        //data

        var query = Context.Parent.Include("Children").AsQueryable();

 

        int total = query.Count();

 

        //search whatever property you like

        if (!string.IsNullOrEmpty(gridParams.sSearch))

        {

            query = from x in query

                    where x.P_Name.Contains(gridParams.sSearch)

                    select x;

        }

 

        //sort

        if (gridParams.iSortingCols > 0)

        {

            var propertyName = "P_ID";

 

            switch (gridParams.iSortCol_0)

            {

                case 0: propertyName = "P_ID"; break;

                case 1: propertyName = "P_Name"; break;

            }

 

            query = query.OrderBy(propertyName, (gridParams.sSortDir_0 == "asc"));

        }

 

        //display

        int totaldisp = query.Count();

        if (gridParams.iDisplayLength > 0)

            query = query.Skip(gridParams.iDisplayStart).Take(gridParams.iDisplayLength);

 

        return Json(new FormatedList

        {

            sEcho = gridParams.sEcho,

            iTotalRecords = total,

            iTotalDisplayRecords = totaldisp,

            aaData = query.GetStrings(t => new string[]

            {

                t.P_ID.ToString(),

                t.P_Name,

                t.Children.Count.ToString()

            })

        });

    }

}

=> 여기서 제일 중요한 부분은 AsQueryable()로 IQueryable<T>로 리턴해주는 부분입니다. 필요한 기능목록에서 Sort와 Page시에 사용되는 매개변수(GridParams)와 함께 실질적인 쿼리만을 만들어주며 Json타입으로 Serialize해주는 부분에서 실질적인 쿼리가 DB에 질의 됩니다. 물론 그 전에 Count를 알기 위한 쿼리는 Count()를 호출해줄 때마다 질의됩니다.

그런데 아마 Page, Sort에 대해서 궁금증이 생기실 수 있습니다. 과연 얼마나 효율적으로 데이터를 가져올까요? 전체 데이터를 가져와서 asc, desc로 정렬한 이후에 가져오는 걸까요? 정답은 row_number()라는 SQL함수에 있습니다. (이전에 row_number() 대한 포스트가 있는데 참고하시기 바랍니다.)
SQL Server Profiler로 질의된 쿼리들을 살펴보도록 하겠습니다.

SELECT

[Project2].[P_ID] AS [P_ID],

[Project2].[P_Name] AS [P_Name],

[Project2].[C1] AS [C1],

[Project2].[C3] AS [C2],

[Project2].[C2] AS [C3],

[Project2].[C_ID] AS [C_ID],

[Project2].[C_Name] AS [C_Name],

[Project2].[P_ID1] AS [P_ID1]

FROM ( SELECT

       [Limit1].[P_ID] AS [P_ID],

       [Limit1].[P_Name] AS [P_Name],

       [Limit1].[C1] AS [C1],

       [Extent2].[C_ID] AS [C_ID],

       [Extent2].[C_Name] AS [C_Name],

       [Extent2].[P_ID] AS [P_ID1],

       CASE WHEN ([Extent2].[C_ID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2],

       CASE WHEN ([Extent2].[C_ID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C3]

       FROM   (SELECT TOP (10) [Project1].[P_ID] AS [P_ID], [Project1].[P_Name] AS [P_Name], [Project1].[C1] AS [C1]

             FROM ( SELECT [Project1].[P_ID] AS [P_ID], [Project1].[P_Name] AS [P_Name], [Project1].[C1] AS [C1], row_number() OVER (ORDER BY [Project1].[P_ID] ASC) AS [row_number]

                    FROM ( SELECT

                           [Extent1].[P_ID] AS [P_ID],

                           [Extent1].[P_Name] AS [P_Name],

                           1 AS [C1]

                           FROM [dbo].[Parent] AS [Extent1]

                    )  AS [Project1]

             )  AS [Project1]

             WHERE [Project1].[row_number] > 0

             ORDER BY [Project1].[P_ID] ASC ) AS [Limit1]

       LEFT OUTER JOIN [dbo].[Children] AS [Extent2] ON [Limit1].[P_ID] = [Extent2].[P_ID]

)  AS [Project2]

ORDER BY [Project2].[P_ID] ASC, [Project2].[C3] ASC

=> 빨간색으로 표시된 부분을 보면 row_number()함수를 사용하였고 select절에는 TOP(10)과 where절에 row_number > 0이 눈에 들어옵니다. Page시에 사용하는 Skip()과 Take()가 쿼리로 변경되는 부분이 바로 이 부분입니다. Sort시에는 row_number()함수안에서 사용하는 Order By절과 제일 마지막에 보이는 Order By절에 의해서 Data가 정렬됩니다.
예를 들어 일반적인 게시판을 생각해보시면 보여지는 글의 건수를 10건이라고 하고 첫 번째 페이지를 보여준다면 쿼리는 TOP(10), row_number > 0이라는 조건절이 만들어져 질의됩니다. 만약에 세 번째 페이지를 보여준다면 TOP(10), row_number > 20이 조건절로 되는 것입니다. 마찬가지로 Order By는 해당 열을 처음 클릭하면 asc 두 번째로 클릭하면 desc으로 조건절이 만들어져 질의되는 것입니다. 실질적으로 row_number()가 어떻게 만들어져 있는지는 알 수 없지만 최소한 화면에 보이는 Data를 빠르게 가져온다는 사실만은 분명합니다. 또한 P_ID가 아닌 P_Name으로 정렬을 해보면 속도가 P_ID보다 느린 것을 알 수 있는데 이것은 row_number()가 인덱스와 상관이 있다는 것을 간접적으로 알 수 있습니다.
Posted by resisa
,