Introduction
I'm quite new to ASP.NET, but the technology seems to be very interesting, so I've decided to try to build a custom ASP.NET control. To make this task more interesting, I've tried to build an Ajax-enabled control. The new control had to do something useful, so I've opted to build a mortgage (loan) payment schedule. The resulting code rests on the following assumptions:
- The discount rate, used to calculate monthly payments, is calculated using the following formula:
((Math.Pow((1 + monthRate), monLength)) - 1) / (monthRate * (Math.Pow((1 + monthRate), monLength)))
- The interest payment fraction of the monthly payment is calculated by multiplying the monthly interest rate by the balance (yet unpaid mortgage). This means that each month, the interest part of the payment diminishes and the mortgage re-payment part increases.
These assumptions should not apply in practice, but the result is roughly equal to what banks usually use to calculate your mortgage payments. Anyway, the purpose of this control is not to be used on official websites or to be totally precise, but to see how a custom Ajax-enabled ASP.NET control works in reality. I can say in advance that it works great!
Using the Code
Any Ajax control consists of two main parts: server side (ASP.NET) and client side (JavaScript). To be fully Ajax-enabled, a control would normally bring any UI rendering resulting from user input -- in our case, displaying a payment schedule table -- to the client side.
To minimize bandwidth, the control should also exchange only data between the JavaScript and server side code, no mark-up. This means that the UI is rendered using not HTML, but JavaScript, calling webpage DOM methods. Well, HTML can be built by JavaScript too, but this is not that interesting or effective, right?
Server Side
The main server side method is buildScheduleForClient
. This method returns a JSON-encoded array of data to be rendered on the client side, according to the aforementioned assumptions. This function is called from ICallbackEventHandler.GetCallbackResult()
, which itself is being called by the ASP.NET Ajax architecture each time a particular control initiates callback.
private string buildScheduleForClient(int iMonthCount,
double dInterestRate, double dLoanAmount)
{
StringBuilder sb = new StringBuilder();
sb.Append("[");
sb.Append(String.Format("['{0}','{1}','{2}','{3}','{4}']",
_COL_MONTH, _COL_TOTAL_PAY, _COL_INTEREST_PAY,
_COL_LOAN_PAY, _COL_BAL));
double dDiscountRate =
LoanMath.CalculateDiscountRate(dInterestRate / 1200, iMonthCount);
double dMonthPayment = dLoanAmount / dDiscountRate;
double dMonthPaymentRounded = Math.Round(dMonthPayment, 2);
double balance = dLoanAmount;
for (int i = 1; i <= iMonthCount; i++)
{
double dInterestPayment = (dInterestRate / 1200) * balance;
double dCreaditRepayment = (dMonthPayment - dInterestPayment);
balance -= dCreaditRepayment;
sb.Append(String.Format(",['{0}','{1}','{2}','{3}','{4}']",
i.ToString(), dMonthPaymentRounded.ToString(),
Math.Round(dInterestPayment, 2).ToString(),
Math.Round(dCreaditRepayment, 2).ToString(),
Math.Ceiling(balance).ToString()));
if (i % 12 == 0)
{
sb.Append(String.Format(",['Year {0}, repayed {1}%']",
(i / 12),Math.Round((100 - ((balance / dLoanAmount) * 100)),
1)));
}
}
sb.Append("]");
return sb.ToString();
}
One more notable thing about the server side is how to include all related CSS and JavaScript material. JavaScript inclusion is easier:
Adding CSS is a bit more complicated. The code in the OnPreRender
method should be different and you should check if any other instance of your control has already added a CSS reference to the page:
string cssUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"LoanCalculationControl.LoanCalculation.css");
LoanHtmlLink cssLink = new LoanHtmlLink();
cssLink.Href = cssUrl;
cssLink.Attributes.Add("rel", "stylesheet");
cssLink.Attributes.Add("type", "text/css");
bool cssAdded = false;
foreach(Control control in this.Page.Header.Controls)
{
if (control.Equals(cssLink))
{
cssAdded = true;
break;
}
}
if(!cssAdded)
this.Page.Header.Controls.Add(cssLink);
LoanHtmlLink
is just a class inheriting from HtmlLink
and overriding the Equals
method.
Client Side
As it very often is with Ajax, the client side is more interesting (and challenging). The client side begins by including a proper handler with the onClick
event of the button. ASP.NET offers a convenient method for outputting proper JavaScript for callback invocation:
Page.ClientScript.GetCallbackEventReference(this, null,
"LoanCalculation.UpdateUI", String.Format("'{0}'",
this.UniqueID), "LoanCalculation.CallbackError", true)
In the method arguments, we list a JavaScript function to handle callback results (LoanCalculation.UpdateUI
), context -- in our case, control Id, which is part of all UI element Ids to make it possible to include multiple instances of the control in one page -- and a JavaScript function to handle server errors in the UI.
During tests, I've learned that this is not enough to have a correct callback from the client. I just couldn't get form data to be transferred with the callback. That is, data from the input controls was present, but it was empty. Browsing through the ASP.NET Ajax infrastructure-supporting JavaScript, I learned that we also need to call the function WebForm_InitCallback
, which populates the __theFormPostData
variable with all necessary input data.
So, all necessary callback initialization was brought inside one function, LoanCalculation.InitRequest
:
InitRequest: function(context)
{
__theFormPostData = '';
WebForm_InitCallback();
var divContents = document.getElementById(context+'_results');
var oldtable = document.getElementById(context+'_results_table');
if(oldtable!=null)
divContents.removeChild(oldtable);
divContents.innerHTML = "Requesting data...";
}
The main function on the client side is LoanCalculation.UpdateUI
:
UpdateUI: function(strData, context)
{
var divContents = document.getElementById(context+'_results');
var tbl = document.createElement('table');
tbl.className = 'LoanCalculationTable';
tbl.id = context+'_results_table';
var arData = eval(strData);
var cell;
var row;
var normalCellCount = 0;
for(var i=0;i<arData.length;i++)
{
row = tbl.insertRow(i);
if(i==0)
normalCellCount = arData[i].length;
for(var m=0;m<arData[i].length;m++)
{
cell = row.insertCell(m);
if(i==0)
cell.className = 'LoanCalculationTDFirst';
else
cell.className = 'LoanCalculationTD';
var textNode = document.createTextNode(arData[i][m]);
cell.appendChild(textNode);
}
if(m<normalCellCount)
{
cell.colSpan = (normalCellCount-m+1);
cell.className = 'LoanCalculationTDFirst';
}
}
divContents.innerHTML = "";
divContents.appendChild(tbl);
}
This function evaluates the received string, which builds the JavaScript array. The array is parsed and the data is used to dynamically build a schedule table.
One more interesting thing is that with ASP.NET Ajax, you don't actually need to catch all exceptions on the server side. Exceptions can be handled in the UI. This control even throws an exception explicitly if the input data exceeds the allowed boundaries. The following is the JavaScript function to display a server error to the users:
CallbackError: function(error,context)
{
var divContents = document.getElementById(context+'_results');
if(divContents!=null)
divContents.innerHTML = ""+error+"";
}
Points of Interest
The resulting control does not claim to display correct results -- the actual formula can be much more complicated -- but it offers an example of how great the Ajax concept is and how great it works with ASP.NET.
History
- 16 August, 2007 -- Original version posted