Click here to Skip to main content
11,428,497 members (66,857 online)
Click here to Skip to main content

Complex Business Rules on your ASP.NET Website in Several Minutes

, 5 Jul 2011 CPOL
Rate this:
Please Sign up or sign in to vote.
This article describes a new approach to business rule authoring in ASP.NET without the use of traditional decision tables

Traditional Rule Engines (Very Briefly)

These days, most applications implement business rules, directly or indirectly. Most of those rules are static, meaning they are part of the code and they never change.

For example, consider the following tiny static rule:

if(RightNow.IsDayTime) SetLightBackground();
else SetDarkBackground(); 

There will never be anything other than day time and night time. So, unless the business owner of this code dramatically changes UI requirements, this code will stay the same forever. There is no need for rule management here.

But what if a company has many business rules and those rules change frequently? Take car insurance premiums, for example. Rules to calculate insurance rates get updated monthly, if not daily. They depend on driver's age and gender, model of the car, its mileage, year, body type, etc. Lots of variables are involved in those calculations. Or take any large web form, such as shopping cart, that processes and validates mission-critical input from a user. Validation rules for such forms could be very complicated and they may change often.

Does insurance company compile, test and deploy its main application every time a single rule gets updated? Or does the developer need to deploy a new build of web app simply because some small change was introduced to data validation requirements? Of course not. They use business rules engines (BRE) - software that allows them to author, test and deploy business rules without recompiling the entire code base.

Rule Basics (Also Very Briefly)

A rule is nothing more than a collection of equations, typically grouped in larger rule sets by their execution priority. The end result of a rule execution could be a boolean value that indicates success or failure (evaluation type rules) or some sort of action that the code must invoke if the ruleset was executed successfully (execution type rules). I'd say that about a billion people would argue that this definition of a rule is not correct/valid/exact/complete/polite, but I think it'll serve the purpose of this article just fine.

The purpose of any rule is to validate (or to be executed against) some data. For example, car insurance rule would be executed against a person who's applying for a coverage. The object that represents this person contains that data as values of its properties. Such object is usually called "fact" or "source" object. The example of a simple rule would look like this:

When
    Person.Age < 18
	Person.Car.Price > 100000.00
Then
	SetPremium(Standard + 50%)
Else
	SetPremium(Standard - Discount) 

In this example, the Person is the source object for the rule.

Seemingly simple syntax of the above example doesn't tell the full story, though. Real-life rules are usually not that primitive. They often must include things like in-line calculations, or calls to external data sources, or declaration and use of global variables.

Requirements to Rules Engine

Because companies have different business needs, they typically have a broad list of very specific requirements to rule authoring and management. But it's expected that a solid engine must provide at least the following:

  1. It has to allow rule authoring, testing and deployment without recompilation of the main code.
  2. It has to have an acceptable performance when executing rules.
  3. It has to allow authoring and management of rules with as little involvement of IT department as possible.

The # 1 is a tricky one. There are bunch of BREs that don't mess with the base code but still they expect the rules to be implemented as native objects (for example, C# classes). In such engines, a rule needs to be recompiled and redeployed every time it gets updated. Obviously, with those engines, you have to be a programmer to author rules. This makes the point # 3 pretty much obsolete. And yet lots and lots of companies use these type of engines in part because having compiled rules running from memory gives them performance advantage without the need to employ complicated caching scenarios. Of course, there are engines that can take a text rule, parse it and "inject" into the code. They usually claim to have the best of both worlds.

The # 2 is just boring. Obviously, all engines say that their performance is exceptional. They all post very convincing charts that clearly outline the advantage of using their solution in comparison to all other competitors. So, at the end, you either have to believe one of them or run their demo to test it for yourself.

But # 3 raises the question that, as far as I know, is absolutely favorite among IT decision makers when it comes to BRE selection: "Using your engine, can a typical business analyst create and manage complicated rules without learning anything extra?" The typical answer to this question is "If your business analysts can follow certain formats then yes, they can. By using decision tables".

To quickly illustrate what decision tables are, let's consider a high-level description of process of working with them:

  1. Business folks create or update rules based on specific tabular format and rules (yep - rules to create rules). That format and rules vary greatly from engine to engine. Rule authors save those decision tables as Excel spreadsheets, or Word documents, or plain text files, or XML docs and send them to IT.
  2. IT guys receive those tables and load/deploy them into the engine. Typically, engine is a separately installed and maintained process or group of processes that is "familiar" with the format in which rules are stored. It translates rules into native objects and gets ready.
  3. When fact/source object enters the engine, the engine finds an appropriate business rule and fires it against the source, returning the result back to the main application or invoking specific action(s).

Of course, there is also a matter of approval of changes, re-testing, debugging, versioning, etc. But let's save that discussion for future articles and get to the main point here.

Look Ma, No Decision Tables!

In our company, we have worked with dozens of clients of all sizes. About 50% of them utilise all kinds of BREs - open source, commercial or custom. Unless their rules are compiled or stored in a database, those companies use decision tables. And we are yet to find a single client that in a long run is absolutely comfortable with it.

The thing is, decision tables have been around since 1960s and their concept didn't change much in all those years. They ARE complicated. In order to include more and more features, BREs have no choice but expand their formats. Increasing business expectations also contribute to this process. Look at the example of decision table from the well-known and widely used open source BRE here. The document insists that you need at least two people to author rules with decision tables - "business analyst" and "technical person". Indeed, just by looking at the last image on that document, it becomes clear that a regular business user simply cannot author business rules without some extensive training. The goal of this article is to prove the last statement false.

The # 1 question we are asked by rule authors is this: "Can we just type the rule as we speak it and you guys translate our typing into a valid rule?" This is almost always followed by question # 2: "Can we do it on the web?" Armed with a new breed of rule engines that we are about to explore, we can finally answer "Yes" to both of those questions.

Let's create a new ASP.NET web application and transform it into a full-blown business rule engine in several minutes. To do that, we are going to use the new ASP.NET server control called Web Rule. It supports ASP.NET 3.5 and up and comes in a small single assembly. Download its free version here.

Before we begin, I think we need to refine the requirements:

  1. Rule authoring must be integrated into a web app so any authorized user can have access to it from anywhere.
  2. After initial integration of engine into our web app and defining the source object(s), there must be no need for IT personnel at all to create, edit, save, load, validate and execute rules.
  3. Rules that were created, validated and saved online can be transported to (or called from) anywhere and executed by any process as long as it's .NET.
  4. There must be no code recompilation or any kind of a rule deployment needed in order to execute saved rule(s), no matter where that execution takes place.
  5. Anyone with elementary school degree must be able to create and edit complicated business rules.

Sounds exciting and hard to believe at the same time, right? Notice that this web app is not going to have a rule versioning, or rule approval process, or role-based access management (but the rule that defines who has access to rules can be build with this rule engine! Yeah, I know...) Any seasoned web developer can build those features on top of the existing engine in a matter of weeks using common ASP.NET features and components. Our main goal is online rule authoring and execution with no decision tables.

Building a Business Rules Engine

Because we begun this article with car insurance example, let's build a BRE for car insurance applications. Let me rephrase that: let's build a business rules engine that would be capable of handling any business rule, including rules from insurance industry. I've never worked in car insurance business so I have no idea how their rules look like. But that's the whole point: let insurance specialists create those rules! We just need to come up with a source object that would represent an imaginary insurance applicant. The structure of our BRE is going to be very simple:

  • A web application is going to host Web Rule control to allow users to create, validate, save and edit business rules. I'm going to use VS 2010 Web Application project template but you can use a website as well. VS 2008 is fine, too.
  • We also need a source object (C# class) that represents our applicant.
  • In real life, a typical insurance company would receive thousands if not millions applications per day. To handle such load, their rule execution would be delegated to a separate always-on processes, likely Windows services, that know how to handle incoming source objects, how to load rules and how to execute those rules against the received data. But here I'm just going to add another web page that would do all that. Not quite enterprisey but educational nonetheless. You'll see that execution of a rule takes only one line of code in Web Rule. Therefore, in the context of this article, it's really irrelevant where that line is being executed as long as it's technically valid.

So, let's do it (the final project is attached to this article):

  1. Open Visual Studio and create a new web application named CodeProject.WebRule.Site. If not already present, add new Default.aspx page to the root of the app.
  2. Reference the downloaded CodeEffects.Rule.dll assembly. To do that, right-click the web project node in Solution Explorer, select "Add Reference...", click Browse tab, navigate to and select the Web Rule assembly and click OK button.
  3. Add a new class to the web app. This class will be our applicant source object. Note that we could declare it in any other project or library and reuse it here by referencing library's DLL. But in this case I'd rather keep things simple. So, right-click the project in Solution Explorer, select "Add - Class...", name it Applicant.cs, click OK and let's talk about source objects.

    In Web Rule, source objects are .NET types/classes that you define to hold business data and execute rules against that data. For example, when applicant asks our imaginary agent for coverage, the agent would create an instance of the Applicant class, fill it up with applicant's data (name, address, car info, etc.) and send this instance to some kind of a queue for execution against existing rules, the rules that were created and saved by other people using our web application.

    As I mentioned above, rules can be of two types: execution and evaluation. Web Rule needs to know what type of rules our Applicant will be used with. It also lets you specify lots of optional meta data with the use of attributes located in CodeEffects.Rule.Attributes namespace. There is an attribute for every rule element and then some. Even though we'll discuss several of them later in the article, its scope prevents me from going into greater details. You can learn everything about Web Rule attributes and source object customization here. For this project, we are going to create execution type rules, as they are more interesting than rules of evaluation type. To do that, we must decorate our source object with at least one attribute - CodeEffects.Rule.Attributes.SourceAttribute - and set its RuleType property to Execution. We also need to declare some properties that would define our applicant and her vehicle:

    using System;
    using CodeEffects.Rule.Attributes;
    using CodeEffects.Rule.Common;
    
    namespace CodeProject.WebRule.Site
    {
    	[Source(RuleType.Execution)]
    	public class Applicant
    	{
    		public Applicant()
    		{
    			this.DOB = DateTime.MinValue;
    			this.Gender = Gender.Unknown;
    		}
    
    		public string Name { get; set; }
    		[Field(DisplayName = "Date of Birth", 
    				DateTimeFormat = "MMM dd, yyyy")]
    		public DateTime DOB { get; set; }
    		public Gender Gender { get; set; }
    		public decimal? Income { get; set; }
    		public decimal? Debt { get; set; }
    		public Address Home { get; set; }
    		public Address Work { get; set; }
    		public Vehicle Car { get; set; }
    		[ExcludeFromEvaluation]
    		public Policy Policy { get; set; }
    
    		public void Approve(Applicant applicant, decimal premium)
    		{
    			this.Report(applicant, true, premium);
    		}
    
    		public void Decline(Applicant applicant)
    		{
    			this.Report(applicant, false, 0);
    		}
    
    		private void Report(Applicant applicant, 
    				bool approved, decimal premium)
    		{
    			applicant.Policy = new Policy();
    			if (approved)
    			{
    				applicant.Policy.Approved = true;
    				applicant.Policy.Number = Guid.NewGuid();
    				applicant.Policy.Total = premium;
    			}
    		}
    	}
    
    	public class Address
    	{
    		public Address() { }
    
    		public string Street { get; set; }
    		public string City { get; set; }
    		public string Postal { get; set; }
    		public string State { get; set; }
    	}
    
    	public class Vehicle
    	{
    		public Vehicle()
    		{
    			this.Manufactured = DateTime.MinValue;
    			this.Engine = Engine.Unknown;
    			this.Turbo = false;
    		}
    
    		[Field(DisplayName = "????? ??????????", 
    			Max = 30, ValueInputType = ValueInputType.User)]
    		public string Brand { get; set; }
    		public DateTime Manufactured { get; set; }
    		public Engine Engine { get; set; }
    		public bool Turbo { get; set; }
    	}
    
    	public class Policy
    	{
    		public Policy()
    		{
    			this.Number = Guid.Empty;
    			this.Approved = false;
    			this.Total = 0;
    		}
    
    		public Guid Number { get; set; }
    		public bool Approved{get;set;}
    		public decimal Total { get; set; }
    	}
    
    	public enum Engine
    	{
    		Gasoline,
    		Diesel,
    		Hybrid,
    		[ExcludeFromEvaluation]
    		Unknown
    	}
    
    	public enum Gender
    	{
    		Male,
    		Female,
    		[ExcludeFromEvaluation]
    		Unknown
    	}
    } 

    Web Rule converts public value type properties of the source object into rule fields. But I intentionally added several reference type properties to our source object as well - Home, Work and Car, to demonstrate that Web Rule is not limited to value types only. When initiated on a web page, Web Rule scans public properties of reference types, looking for their internal value typed properties. It'll use those that were found as rule fields as well. By default, rule fields will be displayed in rule area with their correspondent property names. In our example, it'd be Name, DOB, Home.Street, Work.City, Car.Brand, etc. For more complicated objects, fields might look like Home.Phone.AreaCode or Atlanta.IT.Development.Web.FirstName. You can change those ugly names (as well as many other things) by using the optional Field attribute. This is especially useful in multilingual environments. To demonstrate this, I decorated properties DOB and Vehicle.Brand with the Field attribute and set certain parameters appropriate for their types.

    You can prevent any property or method from being used as a rule element by decorating it with ExcludeFromEvaluation attribute. For instance, we don't want our insurance analysts to be able to input "unknown" gender or engine into rules. We also don't want them to be able to use the Policy property in rules. The Policy type represents the result of rule execution and will be used by rule actions to report back to the system. Therefore, I excluded those items from rule's UI with ExcludeFromEvaluation attribute.

    Execution type rules require actions. I declared two of them. Any public non-static method that returns void can be a rule action. They can be declared inside of the source object or in any other referenced .NET class (external actions). Actions can either be parameterless or they can take value type parameters or instances of source object. As with Field attribute for properties, use optional Action or ExternalAction attributes if you need to change action's name from its default value. Any method can be excluded from rules by using the ExcludeFromEvaluation. In this example, methods Approve and Decline will be automatically used as rule actions because both of them are public non-static methods that return void and both take parameters of value type and/or source object's type. But the method Report was excluded because it's a private method.

    To keep code samples as readable as possible, I'm not going to use all possible attributes for all properties and actions in this source object. But you can download the project attached to this article and take a look at the source object there - it's decorated as a Christmas tree. Very useful stuff.

    Now that we defined our source object, let's get back to our web application.

  4. Open the Default.aspx page we created earlier and register Web Rule control by including the following code right after the @Page directive:
    <%@ Register Assembly="CodeEffects.Rule"
    	Namespace="CodeEffects.Rule" TagPrefix="rule" %> 
  5. Add an instance of the control to the page and set our Applicant class as control's source object by placing the following declaration anywhere inside of HTML tag:
    <rule:AspControl ID="ruleControl" runat="server"
    	SourceAssembly="CodeProject.WebRule.Site"
    	SourceType="CodeProject.WebRule.Site.Applicant" /> 

    Web Rule has many more properties and methods at your disposal to customize the way your engine works but I just set only the required values to get us going. Believe it or not, at this point we have everything we need to run this page and begin authoring rules. We cannot save those rules, neither can we execute them yet, but we sure can play with control's UI to build rules, which is a lot of fun... to some people I know, anyway Smile | :)

    By the way, a couple of words about Web Rule's UI. To create a rule, simply click inside of the rule area and begin selecting appropriate items from context menus. Those menus render only those items that are relevant to your current position inside of the rule. Hit the space bar every time you need to bring the menu. To navigate away from rule area, just click anywhere outside of its borders. You can also indent rule lines by inserting tabs. Hit Enter key to insert a new line or finish value input. Read "live" instructions that are being displayed by control's Help String as you go through the rule. Overall, it's extremely intuitive and fun to use. And it's under non-stop development, many new cool features are planned for implementation in the near future.

    Normally, you would save rules in a database and serve them from there for execution. But, of course, you are not limited to a database only. Web Rule outputs rules in XML format, so it's easy to store them anywhere. To simplify this example, let's store our rules in a folder as XML file.

  6. Add a new folder to the root of our application. Right-click the project node in Solution Explorer, select Add - New Folder and name it Common. Hit Enter to save it.
  7. Now we need a button. Declare it right below the control:
    <asp:Button ID="btnSave" runat="server" Text="Save" Width="100"  onclick="Save" /> 
  8. Include the button's "click" event handler in your code-behind file or HTML:
    protected void Save(object sender, EventArgs e)
    {
    	if (this.ruleTestControl.IsEmpty || !this.ruleTestControl.IsValid) return;
    	// Use .config extension to prevent IIS from serving this file
    	string file = Server.MapPath("/Common/Rule.config");
    	string rule = this.ruleTestControl.GetRuleXml();
    	System.IO.File.WriteAllText(file, rule);
    } 

    Even though this handler contains only 4 lines, let's discuss it in details. Obviously, there is no point of doing anything if the control is empty, meaning that no rule has been entered in the rule area. But the second statement of the first line of the handler is the most interesting one. Web Rule has automatic rule validation built in. This validation guarantees that every rule that has passed the check is valid and ready to be saved/tested/used. There is no need to code anything extra other than checking control's IsValid property in order to invoke the validation. If Web Rule detects an invalid rule, it halts further processing, displays a warning message in its Help String (if its enabled) and highlights all invalid rule elements. Author can hover her mouse over each invalid element to see detailed description of the issue. You can even overwrite those default descriptions with your own if your site is in different language or you don't like the default messages for some reason. But again, this is beyond the scope of this article, refer to Web Rule documentation for more. Even if you forget to check the IsValid property while handling the rule, Web Rule throws the CodeEffects.Rule.Common.InvalidRuleException if the rule is invalid and your code tries to read it. So, it's pretty solid and elegant solution.

    To see this automatic validation in action, simply run the code that we have so far (or go to Web Rule's demo page) and try to submit an invalid rule. It's fun, I promise.

  9. You can also load existing rules for editing. We do this in page's OnLoad handler:
    protected void Page_Load(object sender, EventArgs e)
    {
    	string file = Server.MapPath("/Common/Rule.config");
    	if (!this.IsPostBack && System.IO.File.Exists(file))
    		this.ruleTestControl.LoadRuleFile(file);
    } 

    That's all it takes to load the existing rule. If you saved rule's XML in a database, the code to load it is very simple as well:

    protected void Page_Load(object sender, EventArgs e)
    {
    	if (!this.IsPostBack)
    	{
    		string xml = Database.GetRuleXML();
    		this.ruleTestControl.LoadRuleXml(xml);
    	}
    } 

    By loading the existing rule, authors can modify it and save new version back to the server or database. At this point, we got everything we need to be able to create, save and modify our business rules.

    Notice that after we build our web application and create a way to save and retrieve rules to/from our data storage, our imaginary insurance analysts don't need us developers any more. No decision tables, no rule deployments... Time to look for a new job? Not yet. We still need to finish the rule execution part. Then we can begin bothering recruiters Smile | :)

    The execution itself takes only one line of code:

    CodeEffects.Rule.Evaluator.Execute(ruleXml, sourceObjectInstance); 

    By calling this line, we command Web Rule to invoke the appropriate action if any of the rule equations or group of equations evaluates to true. Contrary to other rule engines that install their own execution processes, Web Rule allows you to call its static Evaluator.Execute or Evaluator.Evaluate methods (for execution and evaluation types of rule, respectively) from any of your .NET code, as long as the CodeEffects.Rule.dll is referenced. This means that you can manage rules in any ASP.NET web application or website and execute rules in any of your new or existing applications or processes, wether it's a Windows service, WPF, Silverlight, ASP.NET MVC, MSI installer, code library, etc, etc.

    So, if our insurance company would be real, rules would be managed in a web application by a group of business analysts. Those rules would be executed against instances of the Applicant class (perhaps, millions of them) by a completely separate processes that run continuously somewhere on corp network, waiting for incoming applicants. We obviously cannot reproduce such a global scenario here in this article. Instead, let's just add another web page and make it execute our rule with a click of a button.

  10. Add a new web form to the project. Right-click the project node in Solution Explorer, select "Add - New Item...", select WebForm from template list, name it Executor.aspx and click OK.
  11. We don't need to register Web Rule on the page in order to execute rules, just a button, its click handler and a label to display the result:
    <asp:Label ID="lblInfo" runat="server" />
    <br/><br/>
    <asp:Button ID="btnSave" runat="server" 
    	Text="Execute" Width="100" onclick="Execute" /> 
  12. Handler will create a new instance of Applicant class, fill it with test data and pass it to Evaluator class, together with the rule's XML that we created earlier and saved to the /Common folder. Then it'll examine the Policy property of the Applicant to see if our applicant was declined or approved.
    protected void Execute(object sender, EventArgs e)
    {
    	Applicant applicant = new Applicant
    	{
    		Name = "Susan Doe",
    		DOB = DateTime.Now.AddYears(-25),
    		Gender = Gender.Female,
    		Income = 50000,
    		Debt = 10000
    	};
    	applicant.Car = new Vehicle
    	{
    		Brand = "Jeep",
    		Engine = Engine.Gasoline,
    		Manufactured = DateTime.Now.AddYears(-3),
    		Turbo = false
    	};
    	applicant.Home = new Address
    	{
    		City = "Alpharetta",
    		Postal = "30004",
    		State = "GA",
    		Street = "123 Main Street"
    	};
    	applicant.Work = new Address
    	{
    		City = "Atlanta",
    		Postal = "30315",
    		State = "GA",
    		Street = "987 Office Drive"
    	};
    
    	string rule = System.IO.File.ReadAllText(
    		Server.MapPath("/Common/Rule.config"),
    		System.Text.Encoding.UTF8);
    
    	CodeEffects.Rule.Evaluator.Execute(rule, applicant);
    
    	System.Text.StringBuilder sb =
    		new System.Text.StringBuilder("Application ");
    
    	if (applicant.Policy == null) sb.Append("errored.");
    	else
    	{
    		if (applicant.Policy.Approved)
    			sb
    				.Append("approved. Premium: ")
    				.Append(applicant.Policy.Total)
    				.Append(" USD, Policy ID: ")
    				.Append(applicant.Policy.Number);
    		else sb.Append("declined.");
    	}
    
    	this.lblInfo.Text = sb.ToString();
    } 

That's it. We got a business rules engine. Build and run the project. Create and save some rule on the Default page first. Then open the Executor page and click the Execute button to execute that rule.

Summary

Let's summarize everything that we have done. We have built a new web app that is capable of managing complex business rules without the need for IT intervention. Our rule authoring UI is clean and intuitive. It's online, accessible from anywhere. And it took us just minutes to do that. Now imagine what can be accomplished in general by using this new concept of business rules management.

But don't think that rule engines should be used only by huge industries or governments. It's not about them - it's about us. You probably already have some large complicated web form somewhere in one of your projects. The form that takes and validates a lot of user input. The form that is critical or very important to the company and yet the one that you keep changing once a month or so because those ... guys from the ... department don't seem to agree on their validation requirements/policies/rules. Stop messing with that form. Build them a page with Web Rule control somewhere on company's intranet. Add a new table to your database with column of XML or varchar(max) type and tell the page to save rules there. Show this new page to your ... department. While they explore new possibilities, all excited and stuff, comment out existing validation logic and add the code to your form that would fetch the current rule from database and execute it against the user input. Just like we did here.

Happy programming!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Kikoz68
Web Developer
United States United States
No Biography provided

Comments and Discussions

 
Questioncan this be use for normal vb windows form? Pin
Patrick Khong29-Nov-11 1:50
memberPatrick Khong29-Nov-11 1:50 
QuestionNew version Pin
Artem7216-Aug-11 15:51
memberArtem7216-Aug-11 15:51 
QuestionGood control and concept Pin
Artem7216-Aug-11 15:48
memberArtem7216-Aug-11 15:48 
GeneralMy vote of 5 Pin
SteveSM214-Jul-11 11:10
memberSteveSM214-Jul-11 11:10 
GeneralRe: My vote of 5 Pin
Kikoz6818-Jul-11 11:16
groupKikoz6818-Jul-11 11:16 
SuggestionAdd screenshots. [modified] Pin
SteveSM211-Jul-11 20:14
memberSteveSM211-Jul-11 20:14 
AnswerRe: Add screenshots. Pin
Kikoz6812-Jul-11 6:14
groupKikoz6812-Jul-11 6:14 
QuestionAction parameters Pin
Misha19645-Jul-11 6:48
memberMisha19645-Jul-11 6:48 
AnswerRe: Action parameters Pin
Kikoz685-Jul-11 8:25
groupKikoz685-Jul-11 8:25 
AnswerRe: Action parameters Pin
Kikoz685-Jul-11 13:47
groupKikoz685-Jul-11 13:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150428.2 | Last Updated 5 Jul 2011
Article Copyright 2011 by Kikoz68
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid