Tag Archive | MVC

ExtJs Grid Dynamic Columns

I had to use ExtJs grid in my recent MVC project. The grid and all other controls are really good, both functionality and usability wise but I stuck into the problem when I needed to make the grid columns dynamic. Most of the examples I found on forums and online tutorials were about using it with fix definition of columns. After searching forums and blogs on Internet, I was able to fix the issue but I realized that there is no thorough tutorial or help to address this issue for ASP.Net MVC. In this blog, I will be giving a complete end to end solution to use dynamic grid columns from ASP.Net. It can be further improved and make more dynamic but it will fulfill most of the requirements.

First of all, I found this ExtJs extension method on ExtJs forums. I didn’t save the link otherwise I would have added the link to that post here. This script needs to be just saved in .js file and referenced from the code. It will take care of most of the heavy lifting on the client side.
Although most of the code is same as I copied it from the forums but I modified the “DynamicColumnModel” function to add my own code for handling the “Column” metadata sent from server. I have commented those line in the code so it can be modified if JSon result returning from server is not in the same format as mine.

//*****************************************
//ExtJS method for dynamic columns
//*****************************************
Ext.data.DynamicJsonReader = function(config)
{
    Ext.data.DynamicJsonReader.superclass.constructor.call(this, config, []);
};

Ext.extend(Ext.data.DynamicJsonReader, Ext.data.JsonReader, {
    getRecordType: function(data)
    {
        var i = 0, arr = [];
        for (var name in data[0]) { arr[i++] = name; } // is there a built-in to do this?

        this.recordType = Ext.data.Record.create(arr);
        return this.recordType;
    },

    readRecords: function(o)
    { // this is just the same as base class, with call to getRecordType injected
        this.jsonData = o;
        var s = this.meta;
        var sid = s.id;

        var totalRecords = 0;
        if (s.totalProperty)
        {
            var v = parseInt(eval("o." + s.totalProperty), 10);
            if (!isNaN(v))
            {
                totalRecords = v;
            }
        }
        var root = s.root ? eval("o." + s.root) : o;

        var recordType = this.getRecordType(root);
        var fields = recordType.prototype.fields;

        var records = [];
        for (var i = 0; i < root.length; i++)
        {
            var n = root[i];
            var values = {};
            var id = (n[sid] !== undefined && n[sid] !== "" ? n[sid] : null);
            for (var j = 0, jlen = fields.length; j < jlen; j++)
            {
                var f = fields.items[j];
                var map = f.mapping || f.name;
                var v = n[map] !== undefined ? n[map] : f.defaultValue;
                v = f.convert(v);
                values[f.name] = v;
            }
            var record = new recordType(values, id);
            record.json = n;
            records[records.length] = record;
        }
        return {
            records: records,
            totalRecords: totalRecords || records.length,
            totalProperty: 'totalRecords'
        };
    }
});

Ext.grid.DynamicColumnModel = function(store)
{
    var cols = [];
    var recordType = store.recordType;
    var fields = recordType.prototype.fields;

    //for dynamic columns we need to return the columnInfo from server so we can build the columns here.
    //in this example, the ResultData is a JSON object, returned from the server which contains a ColumnInfo
    //object with "fields" collection. Each Field in Fields Collection holds the information column
    //we are using the "renderer" here as well to show one important feature of displaying the MVC JSon Date
    $.each(store.reader.jsonData.ResultData.columnInfo.fields, function(index, metaValue)
    {
        cols[index] = { header: metaValue.header, dataIndex: metaValue.dataIndex, width: metaValue.width,
            sortable: metaValue.sortable, hidden: metaValue.hidden,
            renderer: function(dtData) { if (metaValue.renderer) { return eval(metaValue.renderer + "('" + dtData + "')"); } else return dtData; }
        };
    });

    Ext.grid.DynamicColumnModel.superclass.constructor.call(this, cols);
};
Ext.extend(Ext.grid.DynamicColumnModel, Ext.grid.ColumnModel, {});
//*****************************************
//End of dynamic columns
//*****************************************

One of the issues which I had faced when using the ExtJs Grid with ASP.Net MVC was the date format of Json serializer. It was returning the date as a special value of “/Date(xxxxx)” format and it needed to be converted before displaying it on the Grid. This is where the “renderer” property of columns comes into play. I had passed the name of the renderer from ASP.net code so there are no long list of checks in the javascript code. It will just call the “eval” and execute whatever server render method is specified. In case of dates, I am using the following javascript code to parse and convert the date into short date time format.

//this method is used to convert the MS JSON date format to the ExtJS Grid Date Column Value
function dateFormatter(dt)
{
    /// <summary>this method is used to convert the MS JSON date format to the ExtJS Grid Date Column Value</summary>
    /// <param name="dt">Actual JSON Date Value</param>
    try
    {
        //microsoft JSON date format needs to convert into Javascript date
        var newdata = dt.replace(/\/Date\((-?[0-9]+)([+-][0-9]+)?\)\//g, "new Date($1)");
        newdata = eval('(' + newdata + ')');
        return newdata.format('m/d/Y');
    }
    catch (e)
    {
        return dt;
    }
}

Enough of ExtJs Code :). Now let’s look at some server side code which will help us build the dynamic columns and the meta data for ExtJs. First thing which I needed was to convert the datatable into an object which can be serialized by Json. If you try to serialize a datatable with ASP Json serializer then it will through the “circular reference” error so this utility method will take the datatable and convert it into a list which can be easily serialized.

/// <summary>
/// Normalizes the datatable into simple collection of row objects, that later on can be used to create the JSON object.
/// </summary>
/// <remarks>
/// whenever we need to return the datatable from view as a JSON object, then we need to first convert that datatable into a collection of rows so that it can be properly serialized.
/// </remarks>
/// <param name="dt">Source datatable that will be used to create the rows collection.</param>
/// <returns></returns>
protected List<Dictionary<string, object>> GetNormalizedRows( DataTable dt )
{
    List<Dictionary<string, object>> rows = new List<Dictionary<string, object>>();
    Array.ForEach(
        dt.Select(),
        row =>
        {
            Dictionary<string, object> Dictionary = new Dictionary<string, object>();
            foreach ( DataColumn col in dt.Columns )
                Dictionary.Add( col.ColumnName, row[col] );
            rows.Add( Dictionary );
        }
    );
    return rows;
}

Once we have the helper method ready to convert our datatable, we can write a “Controller” action, which will be called from the UI to access the data for the grid.

/// <summary>
/// This method will execute the SQL Query to create the Search results.
/// </summary>
/// <returns></returns>
[HttpPost]
public ActionResult GetData( string anyServerData )
{
        //Execute the query and return the data.
        Datatable resultTable = <get your data from server>;

        //datatables can't be serialized as JSON object, so we need to normalize them and also we need to pass the column meta
        //data information to only show specific grid columns.
        return JSon( new ResultData {
	 new
        {
            GridData = GetNormalizedRows( resultTable ),
            columnInfo = GetColumnMetaData( resultTable )
        } } 
	);
}

That method was really simple and if you have been following it then there was another helper method used in it to get the column meta data, “GetColumnMetaData”. This method will simply loop through the table, create the meta data objects for the ExtJs. It will set the required fields which we need in ExtJs to create the columns.

/// <summary>
/// Extracts the column meta data for ExtJS grid. 
/// </summary>
/// <param name="searchResult"></param>
/// <returns></returns>
private Dictionary<string, object> GetColumnMetaData( DataTable searchResult )
{
    Dictionary<string, object> metaData = new Dictionary<string, object>();
    List<Dictionary<string, object>> colMeta = new List<Dictionary<string, object>>();
    Dictionary<string, object> colDefinition = new Dictionary<string, object>();

    //loop through each datatable column. Get the column name type and other meta specific information and store them in this specific 
    //format so that it can be used by the ExtJS grid on client side.
    foreach ( DataColumn col in searchResult.Columns )
    {
        colDefinition = new Dictionary<string, object>();
        colDefinition.Add( "header", col.ColumnName );
        colDefinition.Add( "dataIndex", col.ColumnName );
        colDefinition.Add( "width", 100 );
        colDefinition.Add( "sortable", true );
        colDefinition.Add( "hidden", false );

	//for datetime columns, we need to render them specially as the date returned from MVC Json is not in format to display to the user
        if ( col.DataType == typeof( DateTime ) )
            colDefinition.Add( "renderer", "dateFormatter" );

        colMeta.Add( colDefinition );
    }

    metaData.Add( "fields", colMeta );
    metaData.Add( "root", "ResultData" );
    metaData.Add( "totalProperty", "results" );
    metaData.Add( "id", "id" );

    return metaData;
}

Finally, to make these all pieces to work, we need to make an AJAX call from client to this Controller action. Any button or link on client can be used to attach a click event handler and in that event handler, we can create the ExtJs store and grid.

//attach a function to the click of HTML Element
$("#btnGetData").click(LoadGridData);

//This will make an AJAX call to the server, passing all the form data and get the result of the search.
function LoadGridData(e)
{
    /// <summary>This will make an AJAX call to the server, passing all the form data and get the result of the search.</summary>
    /// <param name="e">Event object for the clicked Element.</param>

    e.preventDefault();

    //show the AJAX loader... the code is in my previous posts. it is not required so it can be removed if not needed
    objUIHelper.ShowAJAXLoader(true);

    //create the data store to load the data from AJAX web call
    jstore = new Ext.data.Store({
        proxy: new Ext.data.HttpProxy({ url: 'MyController/GetData', method: 'POST' }),
        reader: new Ext.data.DynamicJsonReader({ root: 'ResultData.GridData' }),
        remoteSort: false
    });

    //in case of any AJAX call exception, hide the uiBlocker and show the error message	
    jstore.on('loadexception', function(event, options, response, error)
    {
        //unblock the UI, another helper method to remove the ajax loader
        objUIHelper.HideAJAXLoader();
        
        //show the server error... 
        alert( response.responseText );
    });

    //we need to set the extra params here so it can be used on the grid refresh click
    jstore.on('beforeload', function()
    {
        jstore.baseParams = {
            anyServerData: $("#myHTMLForm").serialize()
        };
    });

    //after the data load in store, create the grid and display the data
    jstore.on('load', function(gridStore)
    {
        //remove any exisitng items from grid div. Grid div is a placeholder to show the extJsGrid
        $("#grid").empty();

        // Reset the Store's recordType
        gridStore.recordType = gridStore.reader.recordType;
        gridStore.fields = gridStore.recordType.prototype.fields;

        //create the paging toolbar
        var bar = new Ext.PagingToolbar({
            store: gridStore,
            pageSize: resultPageSize,
            displayInfo: true,
            displayMsg: 'Displaying record {0} - {1} of {2}',
            emptyMsg: 'No rows to show'
        });

        // Create the grid and bind it with the data store.
        grid = new Ext.grid.GridPanel({
            store: gridStore,
            cm: new Ext.grid.DynamicColumnModel(gridStore),
            selModel: new Ext.grid.RowSelectionModel({ singleSelect: true }),
            enableColLock: true,
            renderTo: 'grid',
            width: 940,
            autoHeight: true,
            title: 'Results',
            bbar: bar,
            pageSize: resultPageSize
        });

        // render the grid on UI
        grid.render();

        //unblock the UI
        objUIHelper.HideAJAXLoader();
    });

    //store's load will call the web URL and load the data
    jstore.load({
        params: {
            start: 0,
            limit: resultPageSize
        }
    });
}

MVC Action Authroization

One good option to apply the action level authorization is to subclass the “AuthorizeAttribute” and in the “AuthorizeCore” method, check for user authorization. If we are using the “Form” authentication then simply we can return “false” and it will redirect the user to the Login Form. In case of windows authentication and running on IIS, it will just keep displaying the Windows Login dialog to the user, so it is better to redirect the user to an Error View for windows Authentication.

[AttributeUsage(AttributeTargets.Method, AllowMultiple=false)]
public class AuthorizeAction : AuthorizeAttribute
{
    private int currAction;

    public AuthorizeAction( int currAction )
    {
        this.currAction = currAction;
    }

    protected override bool AuthorizeCore( HttpContextBase httpContext )
    {
        //check "currAction" against custom database user authroization and set the allowed variable
	bool allowed = true;

	//if user is not authanticated then return the error
        if ( !httpContext.User.Identity.IsAuthenticated )
            allowed = false;

        if ( ! allowed )
        {
            //if it's an AJAX call then simply return the error to the client and show error dialog there
            if ( !string.IsNullOrEmpty( httpContext.Request.Headers["X-Requested-With"] ) && httpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest" )
                return false;
            else
            {
                //in case of a normal URL access, redirect to the special error controller/action.
                httpContext.Response.Redirect( "~/Error/UnAuthorized/" );
                return true;
            }
        }
        else
        {
            return true;
        }
    }
}

Generate HTML string for a MVC View

Sometimes you don’t want to render the view on the standard output stream but you need it to return the output HTML of a view for a JSON call. So, instead of adding the ActionResult of that view to the JSON property, we can force the view to return the output as a string and pass that string as one of the JSON property.
Following Code sample can be used to retrieve the HTML string of a given view and pass the model data to it. The controller context is a required parameter and it can easily be retrieved by the controller.ControllerContext property of the caller.

/// <summary>
/// Generates the HTML string represenation, of a target view.
/// </summary>
/// <param name="c">Controller context is required.</param>
/// <param name="viewName">Target view name.</param>
/// <param name="data">Data object for the view.</param>
/// <returns>HTML string output of the actual view.</returns>
protected string ConvertViewToHtmlString( ControllerContext c, string viewName, object data )
{
    if ( data != null )
    {
        //set view data notifications
        ViewData.Model = data;

        StringWriter output = new StringWriter();
        //find the view to general html string
        ViewEngineResult result = ViewEngines.Engines.FindView( c, viewName, "" );
        ViewContext viewContext = new ViewContext( c, result.View, ViewData, TempData, output );

        try
        {
            //render the view to stringWriter, rather than on Response stream
            result.View.Render( viewContext, output );
        }
        finally
        {
            //release the view so that it can be used by other controllers
            result.ViewEngine.ReleaseView( c, result.View );
        }

        //html string for the view
        return output.ToString();
    }
    else
        return "";
}