Custom state provider and grid with dynamic colums

  1. #1

    Custom state provider and grid with dynamic colums

    Hello

    This is the continuation of the https://forums.ext.net/showthread.ph...ovider-problem thread
    (I think question in the thread was answered more then fully, so I'm starting new one)


    So the idea is
    - Stored procedure is executed, which return pretty big datatable(and it changes little time to time)
    - once loaded, grid columns are rendered accordingly and the state will restore to previous state of the grid


    Now my problem is
    a) (minor one) Grid take its state from the "empty grid" - ie without columns.
    I solved it by skipping statesave event, till the grid columns will not come from server. Of course any nice and elegant solution welcomed
    b) my main problem is that reordering of columnns works fine however if I change sorting of columns I fell into infinite loop of loading. I spent some time here too, and I understand that its due to the sorters end update method - which basically load the data again ( sounds logical - when sorters are applied, grid require resorting, and due to the remote sort reload) but not sure why after load state is reapplied again. Any hint welcome here


    sample below has both problems - simply click grid refresh button, change column order and refresh the page, state will be lost then ( problem a))
    the probem b) is reproducible when you refrehs grid and change soprting of any column, on next refresh of grid , infinite loop of refreshs will be started

    The sample is made as simple as possible( so for exaplestate manager is same for all users), data are comming from the array and so...

    Page

    <%@ Page Language="C#" %>
    
    <script runat="server">
    
    	public class DTO
    	{
    		public int Id { get; set; }
    		public string Name { get; set; }
    		public string Description { get; set; }
    	}
    
    	private static DTO[] Data
    	{
    		get
    		{
    			var ret = new List<DTO>();
    			for (var i = 0; i < 300; i++)
    			{
    				ret.Add(new DTO
    				{
    					Id = i,
    					Name = "Company"+i,
    					Description = "Description"+i
    				});
    			}
    			return ret.ToArray();
    
    		}
    	}
    	
    	private object[] GetData(StoreReadDataEventArgs prms)
    	{
    		IEnumerable<DTO> ret = Data.AsQueryable();
    		
    		if (prms.Sort.Length > 0)
    		{
    			if (prms.Sort[0].Property == "Name")
    			{
    				if (prms.Sort[0].Direction == Ext.Net.SortDirection.ASC)
    				{
    					ret = ret.OrderBy(item => item.Name).ToList();
    				}
    				else
    				{
    					ret = ret.OrderByDescending(item => item.Name).ToList();
    				}
    			}
    			if (prms.Sort[0].Property == "Id")
    			{
    				if (prms.Sort[0].Direction == Ext.Net.SortDirection.ASC)
    				{
    					ret = ret.OrderBy(item => item.Id).ToList();
    				}
    				else
    				{
    					ret = ret.OrderByDescending(item => item.Id).ToList();
    				}
    			}
    			if (prms.Sort[0].Property == "Description")
    			{
    				if (prms.Sort[0].Direction == Ext.Net.SortDirection.ASC)
    				{
    					ret = ret.OrderBy(item => item.Description).ToList();
    				}
    				else
    				{
    					ret = ret.OrderByDescending(item => item.Description).ToList();
    				}
    			}
    		}
    		return ret.Skip(prms.Start).Take(prms.Limit).ToArray();
    	}
    	
    	private void PrepareColumns(object[] data)
    	{
    		ColumnBase extColumn = null;
    		ColumnBase lastStringColumn = null;
    		ModelField field = null;
    		int index = 0;
    
    		foreach (var propertyInfo in data[0].GetType().GetProperties())
    		{
    			if (propertyInfo.Name == "PK_CustomsExportFeed")
    				continue;
    
    
    			var dataIndex = propertyInfo.Name;
    			if (propertyInfo.PropertyType == typeof(DateTime))
    			{
    				extColumn = new DateColumn
    				{
    					Format = "yyyy-MM-dd"
    				};
    				field = new ModelField(dataIndex, ModelFieldType.Date, "yyyy-MM-ddTHH:mm:ss");
    			}
    			else
    			{
    
    				extColumn = new Column
    				{
    					Flex = 1,
    					MinWidth = 80
    				};
    				field = new ModelField(dataIndex);
    			}
    			AddField(field);
    
    			extColumn.DataIndex = dataIndex;
    			extColumn.Text = FixHeader(dataIndex);
    			extColumn.ItemID = dataIndex;
    			extColumn.ID = dataIndex;
    			extColumn.Width = Unit.Pixel((int)Math.Max(90, 6.5 * extColumn.Text.Length));
    
    
    			dgvExportData.ColumnModel.Columns.Insert(index++, extColumn);
    		}
    	}
    
    	private string FixHeader(string columnName)
    	{
    		columnName = columnName.Replace("_", "");
    		var ret = Regex.Replace(columnName, @"([a-z])([A-Z])", "$1 $2");
    		return char.ToUpper(ret[0]) + ret.Substring(1);
    	}
    
    	private void AddField(ModelField field)
    	{
    		if (X.IsAjaxRequest)
    		{
    			storeRequest.AddField(field);
    		}
    		else
    		{
    			storeRequest.Model[0].Fields.Add(field);
    		}
    	}
    
    	private bool ColumsCreated
    	{
    		get
    		{
    			if (ViewState["ColumsCreated"] == null)
    				return false;
    			return (bool)ViewState["ColumsCreated"];
    		}
    		set
    		{
    			ViewState["ColumsCreated"] = value;
    		}
    	}
    
    
    	private void storeRequest_OnRefreshData(object sender, StoreReadDataEventArgs e)
    	{
    // main difference to previous samples on forums
    		var totalCount = Data.Length;
    		var data = GetData(e);
    
    		((PageProxy)storeRequest.Proxy[0]).Total = totalCount;
    
    		if (ColumsCreated == false)
    		{
    			PrepareColumns(data);
    
    			storeRequest.RebuildMeta();
    			dgvExportData.Reconfigure();
    			ColumsCreated = true;
    		}
    
    		storeRequest.DataSource = data;
    		storeRequest.DataBind();
    	}
    
    
    </script>
    
    <!DOCTYPE html>
    
    <html>
    <head runat="server">
        <title>Simple Array Grid With Paging and Remote Reloading - Ext.NET Examples</title>
    
        
    
    <script>
    
    	Ext.define('App.util.HttpStateProvider', {
    		extend: 'Ext.state.Provider',
    		requires: ['Ext.state.Provider', 'Ext.Ajax'],
    		alias: 'util.HttpProvider',
    		processStateSave: false,
    		config: {
    			userId: null,
    			url: null,
    			stateRestoredCallback: null
    		},
    
    		constructor: function (config) {
    			if (!config.userId) {
    				throw 'App.util.HttpStateProvider: Missing userId';
    			}
    			if (!config.url) {
    				throw 'App.util.HttpStateProvider: Missing url';
    			}
    
    			this.initConfig(config);
    			var me = this;
    
    			me.callParent(arguments);
    		},
    		get: function () {
    			return this.callParent(arguments);
    		},
    		set: function (name, value) {
    
    			//if (this.processStateSave == false)
    			//return;
    			var me = this;
    
    			if (typeof value == 'undefined' || value === null) {
    				me.clear(name);
    				return;
    			}
    
    			me.saveStateForKey(name, value);
    			me.callParent(arguments);
    		},
    
    		initStateFromData: function (result) {
    
    			for (var property in result) {
    				if (result.hasOwnProperty(property)) {
    					//this.state[property] = this.decodeValue(result[property]);
    					this.state[property] = this.decodeValue(result[property].Value);
    				}
    			}
    		},
    		// private
    		restoreState: function () {
    
    			var me = this,
    				callback = me.getStateRestoredCallback();
    
    			
    			Ext.Ajax.request({
    				url: me.getUrl(),
    				method: 'GET',
    				params: {
    					userId: me.getUserId()
    				},
    				success: function (response, options) {
    					var result = JSON.parse(response.responseText.trim());
    					//localStorage["alaState"] = result;
    					me.initStateFromData(data);
    
    					if (callback) {
    						callback();
    					}
    				},
    				failure: function () {
    					console.log('App.util.HttpStateProvider: restoreState failed', arguments);
    					if (callback) { callback(); }
    				}
    			});
    		},
    
    		// private
    		clear: function (name) {
    
    			var me = this;
    
    			me.clearStateForKey(name);
    			me.callParent(arguments);
    		},
    
    		// private
    		saveStateForKey: function (key, value) {
    
    			var me = this;
    			Ext.Ajax.request({
    				url: me.getUrl(),
    				method: 'POST',
    				params: {
    					userId: me.getUserId(),
    					key: key,
    					value: me.encodeValue(value)
    				},
    				failure: function () {
    					console.log('App.util.HttpStateProvider: saveStateForKey failed', arguments);
    				}
    			});
    		},
    
    		// private
    		clearStateForKey: function (key) {
    
    			var me = this;
    
    			Ext.Ajax.request({
    				url: me.getUrl(),
    				method: 'DELETE',
    				params: {
    					userId: me.getUserId(),
    					key: key
    				},
    				failure: function () {
    					console.log('App.util.HttpStateProvider: clearStateForKey failed', arguments);
    				}
    			});
    		}
    	});
    
    	var alaStateProvider = new App.util.HttpStateProvider({
    		userId: 1,
    		url: 'HttpStateProviderHandler.ashx',
    		stateRestoredCallback: function () {
    
    		}
    	});
    	alaStateProvider.initStateFromData(<%= SampleStateHandler.GetSerialized() %>);
    
    	Ext.state.Manager.setProvider(alaStateProvider);
    </script>
    
     
    </head>
    <body>
        <form runat="server">
            <ext:ResourceManager runat="server" Namespace="" />
    
            <ext:GridPanel
                ID="dgvExportData"
                runat="server"
                Stateful="True"
                StateID="dgvExportData"
                Title="Array Grid"
                Width="800">
                <Store>
                    <ext:Store  ID="storeRequest" runat="server"  AutoLoad="False" PageSize="10" RemoteSort="true" RemotePaging="True" OnReadData="storeRequest_OnRefreshData">
                        <Model>
                         <ext:Model runat="server" IDProperty="Id">
                                <%--  these are now supposed to be generated from csharp code on binding <Fields>
    	                            <ext:ModelField Name="Id" />
                                    <ext:ModelField Name="Name" />
    	                            <ext:ModelField Name="Description" />
                                   
                                </Fields>--%>
                            </ext:Model>
                        </Model>
    	                <Proxy>
    		                <ext:PageProxy />
    	                </Proxy>
                    </ext:Store>
                </Store>
                <ColumnModel runat="server">
                    <Columns>
    	                <%--<ext:Column runat="server" Text="Id" DataIndex="Id" Flex="1" />
                        <ext:Column runat="server" Text="Name" DataIndex="Name" Flex="1" />
    	                <ext:Column runat="server" Text="Description" DataIndex="Description" Flex="1" />--%>
                    </Columns>
                </ColumnModel>
             
                <View>
                    <ext:GridView runat="server" StripeRows="true" />
                </View>
                <BottomBar>
                     <ext:PagingToolbar runat="server">
                       
                    </ext:PagingToolbar>
                </BottomBar>
            </ext:GridPanel>
        </form>
    
    </body>
    </html>
    -

    additionally state handler:

    public class HttpStateProviderHandler : SampleStateHandler {
    
    }
    using Ext.Net;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.SessionState;
    
    /// <summary>
    /// Summary description for SampleStateHandler
    /// </summary>
    public class SampleStateHandler: IHttpHandler, IReadOnlySessionState
    {
    
    	public class StateData
    	{
    		public string Key { get; set; }
    		public string Value { get; set; }
    	}
    
    
    	private static Dictionary<string, StateData> _dctState = new Dictionary<string, StateData>();
    
    	public static string GetSerialized()
    	{
    		return JSON.Serialize(_dctState.Select(item => new { item.Key, item.Value.Value }).ToDictionary(item => item.Key));
    	}
    
    	public void ProcessRequest(HttpContext context)
    	{
    
    
    		if (context.Request["clear"] == "1")
    		{
    			_dctState.Clear();
    			return;
    
    		}
    		if (context.Request.HttpMethod == "POST")
    		{
    			StateData data = new StateData()
    			{
    				Key = context.Request["Key"],
    				Value = context.Request["Value"]
    			};
    
    			if (_dctState.ContainsKey(data.Key))
    			{
    				_dctState[data.Key] = data;
    			}
    			else
    			{
    				_dctState.Add(data.Key, data);
    			}
    
    
    		}
    		else if (context.Request.HttpMethod == "GET")
    		{
    			var ret = JSON.Serialize(_dctState.Select(item => new { item.Key, item.Value.Value }).ToDictionary(item => item.Key));
    			context.Response.Write(ret);
    		}
    		else if (context.Request.HttpMethod == "DELETE")
    		{
    			var userId = int.Parse(context.Request["userId"]);
    			var key = context.Request["key"];
    
    			_dctState.Remove(key);
    
    			var result = new Dictionary<string, string>();
    			result.Add("success", "true");
    			JSON.Serialize(result);
    		}
    
    		context.Session["StateValue"] = _dctState;
    
    	}
    
    	public bool IsReusable
    	{
    		get
    		{
    			return false;
    		}
    	}
    }
  2. #2
    Hello, Jiri!

    We'll need to put some thought on that to understand exactly what's going on and provide advice. At first, it looks like every refresh you may be receiving a new grid config (columns, fields) and this requires it to re-fetch state which, in turn, may trigger a reorder and then another server request, so basically, before getting deep in your test case, this sounds like a case where you should avoid pulling the whole grid configuration every time.

    But we still need to take a better look on your test case to tell if that's really the case. Anyhow, by the description alone, it seems that maybe there's coming more data than it should, and some better client-server balance should help avoid the issues. Not to put aside, chances are, this very use case might need specific extension of the grid functionality to fully work.

    We'll get back to you soon!
    Fabrício Murta
    Developer & Support Expert
  3. #3
    Sure - I spend over 3 days creating that sample, so feel free to take time, even more then usual or guaranteed reply time requires
    I'm glad to hear you have reproduced my (main) problem

    So to recap requirement, this is actually "sample" for two different pages

    1) data are comming from "database" (as datatable, in compare to my sample above), many columns inside, and time to time new column is created.
    Obvious workaround here is to provide set of columns in advance ( but client is not too happy with that)

    2) grid is connected to number of quite different Stored procedures, and bound dynamically
    based on output from SP grid columns are generated and data loaded (StateID changes accordingly for example)

    both examples works for us fine, excluding state functionality
    Note- as client is cleaning cookies every end of day for security reason, we need to use custom state provider

    One workaround on my mind is "ignore" storing "sorters" ( i.e sorters property) on state provider level.


    Any beter ideas welcome

    Note that for "not dynamic grids" ( i.e grids with predefined columns) all works fine for us
  4. #4
    Hello again, Jiri!

    Trying some things on your test case and putting more thought to it, I'm afraid the answer here may be a bit frustrating to you.

    As much as it makes sense to your test case, remote sorting columns in a grid doesn't expect a new column set. For instance, what would happen if sorting a column that no longer belongs to the returned data set? It's up to the server to decide what to return, and whatever it returns is what would be displayed on the grid. Also, generic column IDs brought in could make a different column than the chosen one to be sorted.

    In order to have this test case working with dynamic grids, I believe comprehensive extensions would be required to the grid and perhaps store loading, so that it checks if the previous column set matches the one being loaded before determining that grid has been reconfigured. Then some specialized server-side code should be parsing of session state string passed during save, such that it could provide a "sane" state string when requested, considering only the available columns in the current show. Besides, state saving should lose track of temporarily absent columns, the server should be able to keep track of those somehow, or the project should "live" with columns losing their saved position/order/width.

    The state string mentioned above is a condensed string similar to this:

    o%3Acolumns%3Da%253Ao%25253Aid%25253Ds%2525253AId%255Eo%25253Aid%25253Ds%2525253AName%255Eo%25253Aid%25253Ds%2525253ADescription%5EstoreState%3Do%253Asorters%253Da%25253Ao%2525253Aroot%2525253Ds%252525253Adata%2525255Eproperty%2525253Ds%252525253AName%2525255Edirection%2525253Ds%252525253AASC%2525255Eid%2525253Ds%252525253AName
    But unfortunately, there's litte -- to no -- documentation about the syntax it uses in the Grid Panel, for instance. Best reference was the save argument passed to Ext.state.Stateful.statesave event, and it just points that the string is referred to as "hash of state values".

    Perhaps you'd better off with a different approach? Static grid like the initial thread, but have an asynchornous task check for column changes and give an option to reload the page (or pull data with columns afresh)?

    In that context, new columns coming only and only with specific user interactions outside OnReadData, so every sorting and filtering would always bring dataset pertaining the same model. Perhaps the grid columns being determined at initial load, and working as static until the user clicks a "fetch new columns" command in the page. I mean, determine the grid columns during page load and not ever load an "empty" grid (with no columns), getting rid of the 1st roadblock you mentioned (saving empty grid state). Then the asynchronous task fetches whether new columns became available and inform the user and enable/display a "refresh columns" button.

    And if not implemented the server-side state string handling there would still be the issue with columns gone having their state lost the next save is called and new ones with default, potentially undesired, state when introduced. So perhaps the server-side maintenance of the state string (and a state "reset" to properly clear it) would still be of essence.

    I believe, in this "static" concept, you can also simplify your logic a bit, so you don't need a custom (and asyncrhonous) restoreState() client-side implementation, but "hard wire" it on page load. And then only save state changes to the server so that it could be used next time the page is refreshed -- or during a specialized fetch with new columns, also make a request to pull an updated column ruleset into the state provider.

    You could keep the async restoreState() logic to be called when new column data is pulled without the need for a full page reload, but that would only be relevant if there's some server implementation that keeps memory of removed columns' ordering, position, etc.

    For instance, I may be repeating myself here, but to make the state defined at load-time, you could create the following property in SampleStateHandler class:

    public static string CurrentStateJS
    {
        get
        {
            var retVal = "{ ";
    
            foreach (var entry in _dctState)
            {
                // Warning: this is going to output a client script with the value from the key, e.g.:
                // myGrid: { Key: "myGrid", Value: "stateValueHere" }
                // Make sure 'myGrid' is a valid JavaScript keyword (perform some validation, maybe?)!
                retVal += entry.Key + ": { Key: \"" + entry.Value.Key + "\", Value: \"" + entry.Value.Value + "\" }, ";
            }
            retVal += "}";
    
            return retVal;
        }
    }
    Then in page, remove the whole restoreState() definition (or rename it to something you can manually call, but free restoreState() so Ext.NET uses client-side state data), and change the constructor of the state provider to include the server-contained state:

    constructor: function (config) {
    	if (!config.userId) {
    		throw 'App.util.HttpStateProvider: Missing userId';
    	}
    	if (!config.url) {
    		throw 'App.util.HttpStateProvider: Missing url';
    	}
    
    	this.initConfig(config);
    	var me = this;
    
            me.initStateFromData(<%= SampleStateHandler.CurrentStateJS %>);
    	me.callParent(arguments);
    },
    At least this could save some (several?) unnecessary server queries at the same time it prevents some race conditions.

    As for the implementation of server-side handling of the component state string, that's pretty much uncharted territory and, as mentioned, not really documented. If anything, the grid panel's inherited Ext.state.Stateful.saveState() function implementation should be a good start: source code

    Hope this helps!
    Fabrício Murta
    Developer & Support Expert
  5. #5
    Hello, Jiri!

    It's been some days since we replied your inquire and still no feedback from you. Do you still need help with this issue? If you do not post back in 7+ days from now, we may mark this thread as closed/completed. This won't prevent tough, you from posting back here whenever you're up to continue the discussion shall you need more help about component state handling.
    Fabrício Murta
    Developer & Support Expert

Similar Threads

  1. Custom state provider problem
    By jirihost in forum 5.x Legacy Premium Help
    Replies: 10
    Last Post: Jul 08, 2023, 5:47 PM
  2. [CLOSED] Problem with state and command column on dynamic grid
    By jirihost in forum 4.x Legacy Premium Help
    Replies: 1
    Last Post: Aug 02, 2022, 2:43 PM
  3. Problem with state and command column on dynamic grid
    By jirihost in forum 5.x Legacy Premium Help
    Replies: 2
    Last Post: Jun 05, 2022, 6:44 AM
  4. Replies: 2
    Last Post: Jul 28, 2011, 5:08 PM
  5. Replies: 0
    Last Post: Jan 09, 2009, 5:18 PM

Posting Permissions