Sunday, November 27, 2011

Flexigrid CRUD(inline form) with ASP.NET MVC

In this post, I'll show you the supporting code for placing a form within flexigrid.


First, declare a variable for the html string of your form:

<script type="text/javascript">
    var formHtml = "";
    

    function canRender() {
        return _canRender;
    }

    $(function () {
        _canRender = false;
    });

</script>


We use canRender so we can defer the execution of javascript of the form's html string

<div id="editor" style="visibility: hidden">
    @using (Html.BeginForm(null, null, FormMethod.Post, new { id = "theForm" }))
    {
        <fieldset>
            <legend>Person</legend>

            @Html.HiddenFor(x => x.PersonId)
            @Html.HiddenFor(x => x.RowVersion)

            <div class="editor-label">
                @Html.LabelFor(x => x.Username)
            </div>
            <div class="editor-field">
                @Html.EditorFor(x => x.Username)
                @Html.ValidationMessageFor(x => x.Username)
            </div>


            <div class="editor-label">
                @Html.LabelFor(x => x.Firstname)
            </div>
            <div class="editor-field">
                @Html.EditorFor(x => x.Firstname)
                @Html.ValidationMessageFor(x => x.Firstname)
            </div>

            <div class="editor-label">
                @Html.LabelFor(x => x.Lastname)
            </div>
            <div class="editor-field">
                @Html.EditorFor(x => x.Lastname)
                @Html.ValidationMessageFor(x => x.Lastname)
            </div>


            <div class="editor-label">
                @Html.LabelFor(x => x.Country.CountryId)
            </div>
            <div class="editor-field">

                <table style="border-style: none">
                <tr>
                <td style="border-style: none">
                @Html.AjaxComboBoxFor(x => x.Country.CountryId, "/Countries/Management/Lookup/", "/Countries/Management/Caption/",
                    new { }, new { sub_info = true, can_render_callback = "canRender", textbox_width = 200 })    
                </td>
                <td style="border-style: none">
                @Html.ValidationMessageFor(x => x.Country.CountryId)
                </td>
                </tr>
                </table>                     
            </div>
            

            <div class="editor-label">
                @Html.LabelFor(x => x.FavoriteNumber)
            </div>
            <div class="editor-field">
                @Html.EditorFor(x => x.FavoriteNumber)
                @Html.ValidationMessageFor(x => x.FavoriteNumber)                
            </div>

            <p>
                <input type="submit" value="Save" />
                <input type="button" id="Closer" value="Close" />
            </p>


            
        </fieldset>
        
        <div style="max-width: 500px; width: 500px;">
            @Html.JsAccessibleValidationSummary(excludePropertyErrors: true)            
        </div>
    }
<script type="text/javascript">

    $(function () {


        if (!canRender()) return;


        var scope = $('#theForm');
        parseDynamicContent(scope);




        $('#Closer', scope).click(function (e) {
            closeForm($(scope));
        });

        $('input[type=submit]', scope).click(function (e) {
            try {


                e.preventDefault();

                if (!scope.valid()) {
                    // alert('has invalid');
                    return;
                }

                save(scope);

                // closeForm(scope);
            } catch (e) {
                alert("Error " + e);
            }

        });

        $(scope).attr('id', guid());
    });

    function save(scope) {        

        $.ajax({
            url: '/People/Management/SaveViaAjax',
            type: 'POST',
            data: $(scope).serialize(),
            success: function (result) {

                var isOk = $(scope).modelValidation(result);


                if (isOk) {

                    var isNew = $('#PersonId', scope).val() == '';

                    if (isNew) {
                        $('#PersonId', scope).val(result.PersonId);
                    }

                    $('#RowVersion', scope).val(result.RowVersion);


                    if (isNew) {
                        $(scope).closest('table').flexReload();
                    }
                    else {


                        setFgEditText(scope, 'Username', $('#Username', scope).val());
                        setFgEditText(scope, 'Firstname', $('#Firstname', scope).val());
                        setFgEditText(scope, 'Lastname', $('#Lastname', scope).val());
                        setFgEditText(scope, 'FavoriteNumber', $('#FavoriteNumber', scope).val());

                        setFgEditText(scope, 'Country', $('#Country_CountryId', scope).ajc().getText());

                        closeForm(scope);
                    }

                }


                // $('#firstTable').flexReload();



            },
            error: function (a, b, c) {
                alert(a.statusText);
                alert(b);
                alert(c);
            }
        });                       //ajax
    }//save

</script>
</div>


Here's the flexigrid setup and getting the form's html string:

<script type="text/javascript">


    $(function () {
        // main..
        setupForm();

        setupFirstTable();
        

        
        // ..main
    });


    function setupForm() {
        formHtml = $('#editor').html();
        $('#editor').remove();        
    }

    function setupFirstTable() {
        $('#firstTable').flexigrid({
            url: '/People/Management/List',
            dataType: 'json',
            colModel: [
                    { display: 'User', name: 'Username', width: 150, sortable: true, align: 'left' },
                    { display: 'Firstname', name: 'Firstname', width: 150, sortable: true, align: 'left' },
                    { display: 'Lastname', name: 'Lastname', width: 150, sortable: true, align: 'left' },
                    { display: 'Favorite#', name: 'FavoriteNumber', width: 150, sortable: true, align: 'left' },
                    { display: 'Country', name: 'Country', width: 150, sortable: true, align: 'left' },
                    { display: 'RowVersion', name: 'RowVersion', width: 150, sortable: true, align: 'left', hide: true }
                ],

            buttons: [
                    { name: 'Add', bclass: 'add', onpress: add },
                    { separator: true },
                    { name: 'Edit', bclass: 'edit', onpress: edit },
                    { separator: true },
                    { name: 'Delete', bclass: 'delete', onpress: del }
                ],

            singleSelect: true,
            sortname: 'Lastname',
            sortorder: 'asc',
            usepager: true,
            title: 'Persons',
            useRp: true,
            rp: 5,
            rpOptions: [5, 10, 15, 20, 25, 40],
            showTableToggleBtn: true,
            width: 900,
            height: 'auto',
            preProcess: function (data) {
                var rp = getFgRowsPerPage($('#firstTable'));
                for (i = data.rows.length; i < rp; ++i) {
                    data.rows.push({ 'id': '', 'cell': ['', '', '', '', '', ''] });
                }                
                return data;
            }
        });    // flexigrid

        setupGrid($('#firstTable'));

    } //setupFirstTable



    function add(com, grid) {
        try {            
            closeFormByGrid(grid);            
            showAddFormByGrid(grid, formHtml);            
        } catch (e) {
            alert('error ' + e);
        }
    }


    function edit(com, grid) {

        closeFormByGrid(grid);


        var items = $('.trSelected', grid);
        var item = items[0];
        var pk = item.id.substr(3);

        if (pk.length == 0) return;

        

        $.ajax({
            url: '/People/Management/GetUpdated/' + pk,
            type: 'POST',
            success: function (data) {



                showEditForm(item, formHtml, function () {

                    var form = $('form', grid);


                    $('#PersonId', form).val(data.Record.PersonId);
                    $('#Username', form).val(data.Record.Username);
                    $('#Firstname', form).val(data.Record.Firstname);
                    $('#Lastname', form).val(data.Record.Lastname);


                    $('input[id=Country_CountryId]', form).val(data.Record.CountryId);
                    $('#FavoriteNumber', form).val(data.Record.FavoriteNumber);
                    $('#RowVersion', form).val(data.Record.RowVersion);

                    
                    $('#Country_CountryId', form).ajc().showCaption();
                    

                    setFgEditText(grid, 'Username', data.Record.Username);
                    setFgEditText(grid, 'Firstname', data.Record.Firstname);
                    setFgEditText(grid, 'Lastname', data.Record.Lastname);                    
                    setFgEditText(grid, 'FavoriteNumber', data.Record.FavoriteNumber);


                }); //showEditForm

            } //success
        }); //ajax
    }//edit

    function del(com, grid) {

        var deleteIt = confirm('Do you want to delete the selected record?');

        if (!deleteIt) return;

        var pk = getCurrentRowPk(grid);
        var version = getFgGridColumnText(grid, 'RowVersion');

        // alert(pk + " " + version + " " + encodeURIComponent(version));


        $.ajax({
            url: '/People/Management/Delete',
            type: 'POST',
            data: 'pk=' + pk + '&version=' + encodeURIComponent(version),
            success: function (result) {
                if (result.IsOk) {
                    $('#firstTable').flexReload();
                }
            }
        });

    }


</script>


Lastly we put this at the end of the html:
<script>
    $(function () {        
        _canRender = true;
    });

</script>


Here's the Flexigrid form-inliner helper functions:

function showEditForm(selectedTr, html, assignerFunc) {

    $(selectedTr).after('<tr class="fgEdit" editId=' + selectedTr.id + '><td width="1" colspan="20" style="border-width: thin; border-top: thick; border-color: #EEE; white-space: normal;"><span></span></td></tr>');
    

    var content = $('td > span', $(selectedTr).next());

    // var form = $(content).hide().html(html);
    var form = $(content).html(html);
    
    assignerFunc();
    
    $(content).show();
    
}


function showAddFormByGrid(grid, formHtml) {

    var tbl = $('.bDiv table', grid);
    showAddForm(tbl, formHtml);    
}

function showAddForm(tbl, formHtml) {
    var tbody = $('tbody', tbl);
    if (tbody.length == 0) {
        $(tbl).append($('<tbody/>'));
        tbody = $('tbody', tbl);
    }
    

    $(tbody).prepend('<tr class="fgEdit"><td width="1" colspan="20" style="border-width: thin; border-top: thick; border-color: #EEE; white-space: normal"><span></span></td></tr>');
    var content = $('tr td span', tbody);
        
    $(content).html(formHtml);


}





Flexigrid CRUD(inline form) example with ASP.NET MVC, complete with model validation:
http://code.google.com/p/flexigrid-crud-example/downloads/list


Known issues:

1. Not with flexigrid per se; but when inside of flexigrid, the jQuery Ajax ComboBox's result area is far from its textbox when using Firefox or IE, have made a work-around in Chrome though. If you could lend a help in correcting that problem of jQuery Ajax ComboBox, I'll be more than glad to accept a patch.

2. If the flexigrid has no rows, the inline form is not in full width. Accepting patch for flexigrid code or helper code.



Sample output


jQuery Ajax ComboBox's detached result screenshot(IE and Firefox problem)

$.post returns incorrect empty string

You'll receive [object XMLDocument] in your jQuery $.post's data ...

var $xhr2 = $.post( 
 options.init_src,
 {
  'lookupId': theValue,
  'field': options.field,
  'primary_key': options.primary_key,
  'db_table': options.db_table
 },
 function (data) {
  $input.val(data); // textbox receives [object XMLDocument]  
 }
);


..., when you return empty string from your controller:

public string Caption(string lookupId)
{
 return "";
}


In order to prevent that problem, just add "text" parameter to your $.post method return type


var $xhr2 = $.post( 
 options.init_src,
 {
  'lookupId': theValue,
  'field': options.field,
  'primary_key': options.primary_key,
  'db_table': options.db_table
 },
 function (data) {
  $input.val(data); // receives "" string now.  
 },
 "text"
);

Monday, October 31, 2011

Not all Linq provider approaches are the same

Whereas NHibernate Linq don't have a problem immediately reaching C#'s functions..

public JsonResult GetUpdated(Guid id)
{
    var js = new JsonResult();

    js.Data =
        new
        {
            Record = (from p in _person.All
                      where p.PersonId == id
                      select new { 
                          p.PersonId, 
                          p.Username, 
                          p.Firstname, 
                          p.Lastname, 
                          p.FavoriteNumber, 
                          p.Country,
                          RowVersion = CustomStringFunctionHere(p.RowVersion ?? new byte[] { })
                      } ).Single()
        };

    return js;
}

string CustomStringFunctionHere(byte[] a)
{
    return "AAA" + Convert.ToBase64String(a) + "BBB";
}

..Entity Framework does:

LINQ to Entities does not recognize the method 'System.String CustomStringFunctionHere(Byte[])' method, and this method cannot be translated into a store expression.


NHibernate do the projection differently, its select expression is not being translated to its store equivalent.

Sunday, October 23, 2011

Updates on jQuery Ajax ComboBox ASP.NET MVC Helper

Added string-based name on the helpers.

Download the code from: http://code.google.com/p/ienablemuch-jquery-ajax-combobox-aspnetmvc/downloads/list

Get the demo code at: http://code.google.com/p/jquery-ajax-combobox-aspnet-mvc-helper/downloads/list


Example:

@Html.AjaxComboBox("CategoryId", 
    "/Category/Lookup",
    "/Category/Caption",
    new { style = "width: 300px " },
    new { sub_info = true })


Nothing was changed on strongly-typed helpers, it's still the preferred way:
@Html.AjaxComboBoxFor(model => model.CategoryId,
  "/Category/Lookup",
  "/Category/Caption",
  new { style = "width: 300px " },
  new { sub_info = true })

Wednesday, October 12, 2011

Sample ASP.NET Ajax Leaky Abstraction

Below is the work-around for problem on non-working ASP.NET UpdateProgress(with AssociatedUpdatePanelID) when the control(e.g. Button) that causes the post-back is outside of the UpdatePanel. This is the cause of the error:

http://stackoverflow.com/questions/1187953/updateprogessbar-is-not-working-when-setting-associatedupdatepanelid


This is the solution for ASP.NET AJAX leaky abstraction :-)

http://stackoverflow.com/questions/996957/why-does-update-progress-does-not-fire-when-associatedupdatepanelid-is-set


An implementation:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="WebApplication7._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
    <script type="text/JavaScript" language="JavaScript">
        function pageLoad() {
            var manager = Sys.WebForms.PageRequestManager.getInstance();
            manager.add_beginRequest(OnBeginRequest);
        }
        function OnBeginRequest(sender, args) {
            var postBackElement = args.get_postBackElement();
            if (postBackElement.id == 'MainContent_Button1') {
                var up = $get('MainContent_UpdateProgress1');
                up.style.display = "block";
            }
        }
    </script>
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>
        Welcome to ASP.NET!
    </h2>
    <p>
        To learn more about ASP.NET visit <a href="http://www.asp.net" title="ASP.NET Website">
            www.asp.net</a>.
    </p>
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    </asp:ScriptManager>
    <asp:UpdatePanel ID="UpdatePanel1" runat="server">
        <Triggers>
            <asp:AsyncPostBackTrigger ControlID="Button1" />
        </Triggers>
        <ContentTemplate>
            <asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
        </ContentTemplate>
    </asp:UpdatePanel>
    <asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
    <asp:UpdateProgress ID="UpdateProgress1" AssociatedUpdatePanelID="UpdatePanel1" runat="server">
        <ProgressTemplate>
            Loading ...</ProgressTemplate>
    </asp:UpdateProgress>
</asp:Content>


This sort of leaky abstraction on ASP.NET of simulating states in an otherwise stateless protocol is making me embrace MVC+jQuery combo even more.

And someday, you won't get any answer on this type of question on forums, as it will be deemed too localized a few years from now. Five years or more, give or take, most developers will be using MVC+jQuery or node.js+jQuery already, and you will hardly find someone able to answer this ASP.NET type of question.