Showing posts with label c#. Show all posts
Showing posts with label c#. Show all posts

Tuesday, November 29, 2011

Clunky old Javascript input mask

 

I was working with the MaskedEditExtender recently to control input into some textboxes but got fed up with the “quirks” of it – namely the inability of it to select() the value of the textbox as well as weirdness of the masks in general.   For instance, if you were working with a currency mask set up as #,###.## and blurred off of the control and then went back to it (focus) your cursor would appear at the left-most character instead of the right-most.   This required an extra click to edit the value if you didn’t feel like retyping the entire thing.

Be that as it may, I still liked the ability to limit the keys used on the inputs and used a trusty old JavaScript function to achieve just that.  

My function uses passed variables to determine which keycodes to allow and what not to.   It’s a little clunky but it does the trick.  Simply specify the keys you’ll allow (by passing a “1) and which ones will be ignored (passing any other value besides “1”)

function LimitInput(NumbersOK, DecimalPointOK, CommasOK, QuotesOK, AlphaOK, SpacesOK, DateTimeSymbolsOK) {
x = window.event.keyCode;

// 0-9 on top row keys as well as num-pad
if (NumbersOK == 1 && ((x >= 48 && x <= 57) || (x >= 96 && x <= 105))) return true;

// decimal point and period key
if (DecimalPointOK == 1 && (x == 190 || x == 110)) return true;

//commas
if (CommasOK == 1 && x == 188) return true;

//single and double quotation marks
if (QuotesOK == 1 && x == 222) return true;

//letters only - upper and lower
if (AlphaOK == 1 && x >= 65 && x <= 90) return true;

// spacebar
if (SpacesOK == 1 && x == 32) return true;

// the dash, colon and forward-slash symbols
if (DateTimeSymbolsOK == 1 && (x == 191 || x == 109 || x == 16)) return true;

//always allow Backspace, Delete, TAB and Left/Right arrow keys
if (x == 46 || x == 8 || x == 9 || x == 37 || x == 39) return true;

return false;
}



Usage  Simply wire-up a call to this function from the control’s onkeydown event as such:


TextBox1.onkeydown = return LimitInput(#,#,#,#,#,#,#);


Example:  Wiring up a textbox “TextMoney” in code-behind so it only accepts currency-formatted input would look like


TextMoney.Attributes.Add("onkeydown", "return LimitInput(1,1,1,0,0,0,0);");


TIP:  Always remember to include “return” when making the call or else it won’t work Smile

Wednesday, April 28, 2010

AutoCompleteExtender using Application-Cached Data

A recent project required me to build a search function that looked for existing customers by name.  Our company has thousands of customers so simply putting the list into a dropdownlist or bulleted list was not feasible.  I decided to build a simple textbox input search function which was wired up with the AJAX AutoCompleteExtender.  The user would enter a name of a customer and the list would filter down until either a match was made or it wasn't. 

Working with the extender is quite simple.  You add a textbox control to your form, associate the extender with it and point the extender to a webservice.  Each character entered into the textbox triggers a webmethod of the webservice which queries data and returns results as a string array [].

On the backend the webservice would execute a simple query against a table on our SQL Server based on the contents of the text entered in the textbox.  As more and more people use this function however, the traffic and performance of all of this activity becomes a concern.   Since the data being queried against rarely changed (names of customers), I decided to utilize an application cache which the webmethod would use as the basis of finding matches via the extender.

The original setup of my webmethod was quite simple.   It would execute a query against the database that returned X amount of rows (to limit the number shown in the autocomplete results).  These rows (containing 1 column) would be inserted into a string array and returned to the extender for display.

Original code:

[WebMethod]
public string[] GetCustomers(string prefixText, int ResultsReturned)
{
string strSQL = "SELECT TOP " + ResultsReturned.ToString() + " CUSTOMER_NAME + ' - ' + CUSTOMER_STATUS FROM tblCUSTOMERS ";
strSQL += "WHERE REPLACE(REPLACE(UPPER(CUSTOMER_NAME),'.',''),',','') LIKE '%" + prefixText.ToUpper().Replace(".", "").Replace(",", "") + "%' ";
strSQL += "ORDER BY CUSTOMER_NAME";
SqlConnection conn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["myConn"].ToString());
conn.Open();
SqlDataAdapter sda = new SqlDataAdapter(strSQL, conn);
DataSet ds = new DataSet();
sda.Fill(ds);
conn.Close();
string[] items = new string[ds.Tables[0].Rows.Count];
int i = 0;
foreach (DataRow dr in ds.Tables[0].Rows)
{
items.SetValue(dr[0].ToString(), i);
i++;
}
ds.Dispose();
sda.Dispose();
conn.Dispose();

return items;
}

To utilize caching I had to determine the best scope to use.  If the data being cached was different or would change depending on the user accessing that cache, I would use a Session-level cache.   If the data was the same across the board, regardless of the user, an Application cache is the best bet.   Since this was the case, I went the application route. 


Implementing a cached data source takes a little extra coding since I now have to filter my data from a persistant (sortof) dataset as opposed to requerying the database and building a new dataset each time.  The first step is to add a constructor that can build a basic, unfiltered dataset (using a DataTable) and add it into the Application.Cache space as such:


private DataTable GetCustomersDT()
{
DataTable dt = (DataTable)HttpContext.Current.Cache["CustomersDT"];
if (dt == null)
{
string strSQL = "SELECT CUSTOMER_NAME, CUSTOMER_STATUS FROM tblCUSTOMERS";

SqlConnection conn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["myConn"].ToString());
conn.Open();
SqlDataAdapter sda = new SqlDataAdapter(strSQL, conn);
DataSet ds = new DataSet();
sda.Fill(ds);
dt = ds.Tables[0];
HttpContext.Current.Cache.Insert("CustomersDT", dt, null, Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(5));
sda.Dispose();
ds.Dispose();
conn.Close();
}
else
{
dt = (DataTable)HttpContext.Current.Cache["CustomersDT"];
}
return dt;
}

Now that we have a Cached DataTable containing the data we'll use to filter for our extender, lets look at the WebMethod that utilizes it:


[WebMethod]
public string[] GetCustomersFromCache(string prefixText, int ResultsReturned)
{
DataTable dt = GetCustomersDT();
DataView dv = new DataView(dt);
dv.RowFilter = "CUSTOMER_NAME LIKE '%" + prefixText.ToUpper().Replace(".", "").Replace(",", "") + "%'";
dv.Sort = "CUSTOMER_NAME";
int rowcount = dv.Count;
int counter = 5;
if (rowcount < 5)
{
counter = rowcount;
}
string[] items = new string[counter];

int x = 0;
foreach (DataRowView drv in dv)
{
if (x < counter)
{
items.SetValue(drv[0].ToString(), x);
x++;
}
else
{
break;
}

}
return items;
}

And there you have it.   You'll see that the first line of the WebMethod calls GetCustomersDT() which returns the DataTable used for filtering our results.  This DataTable will either exist in the Application's Cache or be created and then added to the Cache.    This should save many, many round-trips to the database.

Thursday, March 25, 2010

Avoiding "Timeout Expired" errors from long running SELECT statements

a SQLDataSource object has a default command timeout of 90 seconds. Often you may be tasked with returning large amounts of data or doing some other tedious processing on the database side that takes while. If you exceed this you'll get the pain-in-the-ass runtime exception - Sys.WebForms.PageRequestManagerServerErrorException: Timeout expired.

 error

 

To work-around this when all other methods have failed, I increase the command timeout value of the SqlDataSource object via it's SELECTING event as such:

protected void sdsYourSQLDataSource_Selecting(object sender, SqlDataSourceSelectingEventArgs e)
{
e.Command.CommandTimeout = 360;
}


The value is set in SECONDS. Hope this helps