Abstract
An idea to compose a page from data bound hierarchical scopes is a new web development approach intended to simplify building of reach Web 2.0 applications, maximizing a level of separation between presentation and server-side logic, and minimizing the impact of framework architecture and infrastructure on the resulting back-end code design. The way data is generated and presented using this approach leads to a new level of structural simplicity and transparency of server-side code bringing the web application back-end design to perfection.
ASP.NET Scopes Framework (further SF) is the first attempt to implement the concept on a most popular web development platform and it is an interesting alternative to the standard ASP.NET Forms and MVC development. A page in SF consists of a template to control presentation and a controller class having all server-side logic. The template has nothing but a valid W3C compliant HTML in it without any server controls. SF treats the template as a set of hierarchically structured HTML fragments called data scopes. Due to its hierarchical structure I call this set a data scope tree. Every data scope in the tree has a set of placeholders substituted by real data when template is rendered. This real data, in its turn, is generated by the controller that template is associated with. The controller provides data individually for each scope in the scope tree, and handles various events (or actions) raised by the client side. AJAX and partial updates are achieved by refreshing only the selected data scopes in the scope tree on the async postback initiated by an action. And this is basically it! Sounds simple? It's actually even simpler than it sounds :)
NOTE: English is my second language, so please be tolerant and forgive me my language errors throughout the article :)
Current version of ASP.NET Scopes Framework is Alpha 1, meaning that it is still missing a lot of planned features, has a number of bugs and contains lots of quick-and-dirty coding. I'm also still deciding on some framework requirements. In this article I provide the framework as binary .dll together with the Students Appliciation demo website just in order that you can see the new approach in action with a real Web 2.0 application and feel the power of the new web development concept. Starting from the 1st stable release of the framework which I hope to have in a couple of months, the ASP.NET SF will become an open-source project available for everyone. Until then, I do not recommend using Alpha or Beta versions of ASP.NET SF in production applications.
UPDATED (NOV 23, 2010): I opened a public discussion blog devoted to new web-development concepts and ASP.NET Scopes Framework based on them. If you have any questions or suggestions regarding SF, please visit my blog at www.rgubarenko.net
Contents
1. Students Application Demo Site
First thing you need to do is to download the source code that comes with the article. The code contains a demo website based on Scopes Framework (further SF). The site is called Students Application and it is built on VS 2008 ASP.NET 3.5. Browse the site source and familiarize yourself with its structure – the whole site is just a couple of files. In the Bin folder you may notice the AspNetScopes.dll binary file which contains the entire framework and must be put into the Bin folder of any application wishing to use SF. In this article I will investigate how Students Application site is developed from scratch, step by step, providing all necessary theoretical knowledge and reference. I organized this article to give new matetial incrementally i.e. each SF theoretical portion is followed by the practice where I explain in details how theory is applied in Students Application demo site.
The Students Application site consists of a single default page displaying a list of student records. There are 10 students in total, but the page only displays up to 3 of them and has a pager at the bottom to do pagination. Each student record consists of a profile information and a list of courses that student is enrolled in. The list of student courses is also limited to 3 items and has its own pager. Paging of courses and students is done in AJAX way, without updating the entire page. Fig. 1 shows how Students Application default page looks in the client browser:
Fig. 1: Default.aspx page UI of Students Application
You have noticed "Popup 1" and "Popup 2" buttons under student profile. These are needed to demonstrate different techniques how to create dialog windows using SF. Visually behaviour of both dialogs invoked by these buttons is the same (except that second dialog has a title bar in different color), but the underlying implementation is different: while "Popup 2" dialog uses the traditional 3rd party jQuery plugin approach, the "Popup 1" dialog is made only by using ASP.NET SF with zero popup window JavaScript. Fig. 2 shows how both dialogs look in the client browser:
Fig. 2: Dialog windows in Students Application
This application does not do much by itself, but it does the main thing – it proves the concept of scope-based server pages and demonstrates the power of SF built around this concept. Applying the techniques I used to build Students Application demo site, the developers can create their own SF-based applications similar to the richest and the most complex Web 2.0 sites in WWW nowadays. In this article I will try to mix a tutorial, an architectural overview, and a programming reference for SF based web applications in order to give you a good picture of what SF is capable of.
NOTE: Student Application does not demonstrate client input processing. I'll add this part to the demo application in the near future. I also intend to add more functionalities to the client-side SF to simplify user input implementation. So, watch for framework updates.
2. Why Scopes Framework?
Before I start discussing the SF architecture, I'd like to talk a bit about the reasons that made me search for an alternative web development approach resulted in appearance of the new concept and the SF built around it. I'd also like to share my original thoughts that significantly influenced the entire architecture of the framework.
2.1. Presentation Separation
Standard ASP.NET, besides being the most popular web development platform, has a number of problems that we all know about. The biggest one is a practical inability to TDD the pages caused by a lack of separation between the server-side code and the presentation. MVC Framework is an elegant solution of testability problem; however, the lack of presentation separation problem is still not solved. I will explain this point.
I think all of you agree that one of the most annoying things during programming process is that actual development work has to go back and forth between graphics designers and programmers multiple times. They come to you with questions like "ooh, we need a small code change to swap two columns in this table" or "ooh, I cannot tweak this layout by CSS, seems like we need you to make some code changes". Such small things actually waste a lot of time and cost money, interrupting programmers from ongoing work again and again until the desired GUI is reached and the client is happy. Nether standard ASP.NET Forms, nor MVC allow the programmers and graphic designers to work separately. In Forms the .aspx/.ascx markups contain tons of server controls that the developer has to insert from the beginning to test the overall functionality of the page. The separation is impossible by definition as long as we have server controls functionally bound to the back-end on the presentation layer. Moreover, graphics teams usually have difficulties understanding how to tweak the appearance of server controls on the pages. MVC makes the situation even worse introducing control flow statements in views (.aspx) and partial views (.ascx) killing the last hope of web developers to separate graphics designer work from application programming.
So, while designing the SF, the number 1 simple idea that I departed from was that server-side programming should be completely independent of presentation. And vice-versa, the presentation should be done by graphics expert without need for code changes from the application developer. More than that, the designer should have 100% freedom to do anything with the presentation markup without any possible impact to the overall application functionality how it was for .aspx/.ascx when careless change in the markup could cause the entire application to break. You probably think now that a complete presentation separation is too good to be true? It's actually not, the concept is quite feasible and even simple to implement.
2.2. Web applications revisited
Developing the separation idea further I came to a million dollar question ... What is a web application? And my answer to it is that technically a web application is a bunch of data produced by server-side logic and presented to the user in the client browser. Just think about it. Your server side code executes until the data is produced, and after that, presentation engine takes the data and inserts it into the page making the final output for the end user. This is it! The server-side is only responsible for producing data, not knowing anything about the presentation, and the presentation only expects data totally ignoring the fact how this data is generated.
But what's happening now in the existing ASP.NET applications? The back-end code is always mixed with presentation due to the server controls inside the markup. The developer always have to worry about page lifecycle, about events sequence, about where and when the controls are data bound, about making certain parts of the code not execute on async postbacks, and so on. As a result, your back-end design is severely impacted by the peculiarities of the framework.
So besides a complete presentation separation, another reason to search for a better development approach was that I wanted to allow the developer to completely focus on an application design instead of thinking how to better fit his design ideas into the existing architecture of the framework.
2.3. Development speed vs. flexibility vs. other attributes
In general, to do the development work we, software developers, use different languages, platforms, frameworks, APIs, tools, etc. Choosing among these, we have to compare the certain dev process attributes such as speed of development, functional flexibility, availability of features, resulting performance, etc. Usually these attributes exclude each other, so choosing the certain development tool we give preferences to some attributes ignoring the others. For example, we can use CMS (like DotNetNuke) to speed-up building websites and this works great as long as we don't need deep customization of the certain modules. So the CMS based solution is only flexible to a certain point. Or we can use the low-level programming language to have a better control over the system, but even simple software products take huge amounts of time to put into the production. So, the situation when we have to sacrifice one of the software attributes for the sake of another one is quite normal in the programming world.
Developing the concept of templated data scopes and the framework based on them I tried to break this dependency. You'll see how dramatically SF speeds up the development of Web 2.0 sites, not losing any flexibility at all. In the same time SF is very lightweight from performance point of view comparing to standard Forms rendering or MVC. And one of my favourite features of SF is that its design allows your backend code to be absolutely transparent and beautiful.
2.4. SF and ASP.NET Forms
The condition that I set from the beginning of my implementation is that SF must live together with the standard ASP.NET not denying any of its features. Instead, the SF should compliment Forms allowing a programmer to use all of the valuable standard ASP.NET features such as session tracking, enhanced security, role-based membership, registering client scripts, data caching, etc.
Furthermore, I want the SF pages to coexist with Forms pages in one application. This design allows easy migration of Forms based applications or their certain parts to more flexible SF based applications. Same as in Forms, the request processing in SF should be based on a physical page location specified in a request URL. This is different from the MVC where request URLs does not contain physical paths to the application resources and the processing controller is selected based on a defined pattern in the request URL. I give a preference to Forms over MVC approach in this situation, because, in my opinion, reflecting physical page locations in the request URLs is more transparent and consistent from the development point of view. And when we need something like search engine friendly URLs, we can always plug in and use one of the available URL rewriting modules.
Having summarized all this, about 6 months ago I came up with an idea of fully templated server pages representing data using tree-structured data scopes, where each data scope can be refreshed individually to achieve unlimited AJAX-like capabilities. So I started the project resulted in ASP.NET Scopes Framework that current article is devoted to.
3. ASPX Pages and Top-Level Request Processing in SF
3.1. Templates and controllers
To process incoming requests, execute business logic, and render output, the SF uses controller/template pairs which are the SF substitution for .aspx(.ascx)/codebehind pairs used in standard ASP.NET Forms applications. So, in order to create a page in the SF, the developer must accomplish 2 steps:
- Create a controller class containing all server-side logic.
- Create a template file driving entire page presentation.
To tell the system that current request should be serviced by the certain controller/template pair, the developer creates an .aspx page that associates the incoming request with the specific controller for processing. This .aspx page contains no logic – the SF uses it just as a request entry point that transfer all further work to the desired controller.
3.2. Top-level execution flow
Fig. 3 shows the high-level steps (blue circle numbers) how the incoming request is processed by the SF page. Initially the request comes to an .aspx page in a regular way (step 1). This page contains special markup telling the system that this is actually an SF page. The page also specifies the controller that should be used for processing (step 2). Then the control transfers to an SF engine that executes logic in the controller and generates output data (step 3). Then template associated with the controller is used to represent that data and generate final output (step 4). Finally, the control comes back to the .aspx page that takes the output and adds it in the page response (step 5).
Fig 3: Top-Level Request Processing Steps is SF
3.3. Making .aspx work as SF page
To make an .aspx page work as SF page, the developer has to use the special ScopesManagerControl
provided by SF. The diagram on Fig. 4 shows the members of this control class. It also shows an additional ProvideRootControlEventArgs
class used by the control as argument type for its ProvideRootControl
event hander.
Fig. 4: ScopesManagerControl and ProvideRootControlEventArgs classes diagrams
To convert a regular .aspx page to an SF page, the developer has to make the following changes:
- Add
ScopesManagerControl
right after a ScriptManager
on the .aspx page.
- Handle
ScopesManagerControl.ProvideRootControl
event. In the body of event handler implementation assign the instance of the desired controller to RootScopeControl
property of ProvideRootControlEventArgs
object passed as handler argument.
- Add head and body content literals in the .aspx markup and pass their IDs to
ScopesManagerControl
using its BodyLiteralID
and HeadLiteralID
properties.
Now it becomes more clear how steps 2 and 5 from Fig. 3 actually work. On step 2 the SF needs to get an actual controller that is used for further processing. We explicitly pass the instance of a controller to the SF using ProvideRootControl
event handler. On step 5 all processing is finished and output is ready. ScopesManagerControl
on the .aspx page uses literals to insert head and body output from controller/template pair into the page before sending the response back to the end user.
NOTE: Modifying an .aspx manually to bind the request entry point to the controller is a quick-and-dirty solution used in Alpha version. It works, but will have to be changed to some better option in the future versions which does not require manual markup and head and body literals to insert output. I'm still weighing the pros and cons of various approaches and I'll try to have the final design in the next Beta release of SF.
3.4. Practice: Default.aspx in Students Application
As I already mentioned, our simple Students Application consists of a single Default.aspx page displaying UI depicted on Fig. 1 and Fig. 2. This page is an SF request entry point providing the control/template pair to process the request and to do the output. The following is the listing of the Default.aspx markup:
Listing 1: ~/Default.aspx
1 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
2 <%@ Register assembly="AspNetScopes" namespace="AspNetScopes.Framework.Controls" tagprefix="AspNetScopes" %>
3
4 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
5
6 <html xmlns="http://www.w3.org/1999/xhtml">
7 <head id="Head1" runat="server">
8 <asp:Literal ID="LiteralHeadContent" runat="server"></asp:Literal>
9 </head>
10 <body>
11 <form id="form1" runat="server">
12 <asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="true" ScriptMode="Debug">
13 </asp:ScriptManager>
14 <AspNetScopes:ScopesManagerControl ID="ScopesManagerControl1" runat="server"
15 HeadLiteralID="LiteralHeadContent"
16 BodyLiteralID="LiteralBodyContent"
17 onproviderootcontrol="ScopesManagerControl1_ProvideRootControl" />
18
19 <div>
20 <asp:Literal ID="LiteralBodyContent" runat="server"></asp:Literal>
21 </div>
22
23 </form>
24 </body>
25 </html>
The next listing is the Default.aspx.cs codebehind file:
Listing 2: ~/Default.aspx.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Web;
5 using System.Web.UI;
6 using System.Web.UI.WebControls;
7
8 using AspNetScopes.Framework.Controls;
9
10 public partial class _Default : System.Web.UI.Page
11 {
12 protected void ScopesManagerControl1_ProvideRootControl(object sender, ProvideRootControlEventArgs e)
13 {
14 e.RootScopeControl = new PageStudents();
15 }
16 }
As you can see, the page is built according to the rules we have just discussed in previous section. On listing 1 at line 14 the ScopesManagerControl
is added to the page after ScriptManager
. Literals to insert the head and the body portions of the rendered HTML into the resulting output are on lines 8 and 20 respectively. On lines 15 and 16 the IDs of these literals are passed to the ScopesManagerControl
. ProvideRootControl
event is handled on line 17. On listing 2 at lines 12-15 the event handler is implemented and the instance of PageStudents
controller is explicitly passed to the SF using RootScopeControl
property of the argument object.
So, everything is quite trivial here. When the request is made to the Default.aspx page, the ScopeManagerControl
fires ProvideRootControl
event, gets the instance of PageStudents
controller, and uses this controller for all further processing until the output is ready.
4. Scope Trees and Page Templates in SF
4.1. Idea of templates
Recall our discussion on web applications in section 2.2. The SF is designed to maximize the presentation separation, so the back-end code located inside the controller class is only responsible for producing data, which is, in general, a list of string values. Then presentation engine takes all these data values and renders final output using the template associated with the controller.
So, how does that data get rendered in SF? Rendering means constructing final HTML output presented to the end user. Imagine we want to construct an HTML output to present just the student profile area for one of the student records on Fig. 1. What would be the most "presentation separate" way to do that given that we already have a list of values for student name, SSN, major, etc? And my answer to this question is that the simplest and most flexible way render those values would be to use a pure HTML template with placeholders replaced by the corresponding values on rendering stage! Exactly, only valid W3C compliant and beautiful HTML with placeholders and nothing else! No server controls, no control flow statements, no inline data binding, no anything that tells us about existence of the server side processing. Next, if certain area of the page can be presented by HTML fragment with placeholders, then the entire page can be presented by a set of different HTML fragments with placeholders. And all we need to get the final HTML output is a list of string values to replace the placeholders! This simple thought about pure HTML templates is actually an underlying idea of the entire concept of scope based server pages that this article is about.
Now we are going to formalize the things and derive the idea of data scopes that is a logical representation of HTML fragments inside the HTML templates.
4.2. Data scopes
Data scope is a central concept of SF and the main logical unit used throughout. Abstractly data scope is a group of strongly cohesive data values. Within data scope data values can be grouped into the smaller data scopes based on more narrow cohesion criteria. This means that data scope can consist of any number of child data scopes. Physically data scope is an HTML fragment wrapped into a DIV
tag having attribute scope
equal to some scope name chosen by the developer. The name of the scope must be unique with respect to the scopes having the same direct parent. Each data scope can have a number of placeholders inside the wrapping DIV
tag. Placeholders are string tokens replaced by corresponding data values when data scope is rendered. During rendering process, each data scope can repeat the content of its wrapping DIV
tag multiple times and this is how repeater-like functionality is achieved in SF. Instead of saying that scope DIV
repeats its content, let's just say that scope is repeated. Often we don't want to repeat the entire content of the scope, but only a part of it. For example, if we have a grid represented by a table inside a scope DIV
, we want to repeat only table rows, not the table itself. For this purpose you can use "<!--scope-from-->" and "<!--scope-stop-->" comments marking the beginning and the end of content that should be repeated inside the data scope. Data scopes approach allows elegant AJAX implementation in SF. Every data scope in SF can be refreshed individually providing the most powerful and transparent partial update functionality without any additional coding.
We can now redefine our HTML templates in terms of data scopes. HTML template is just a set of data scopes and every web page can be rendered using a set of data scopes in a certain nesting configuration. How to partition a page into data scopes and what cohesion criteria to use is totally up to the developer, but this process is quite trivial. For example, on Fig. 1 a single student record could be represented by Student scope, which, in its turn, could be further partitioned into profile info area represented by Profile scope and courses schedule area represented by Schedule scope. The part of the actual HTML template representing a student record could look like the following:
Fig. 5: Example of a part of HTML template
Notice that on Fig. 5 I called the scope StudentRepeater instead of just Student, because I want to emphasize that content of this scope is repeated to display multiple records. I used "<!--scope-from-->" and "<!--scope-stop-->" comments to narrow the repeated content, because I only need to repeat table rows containing the Profile and the Schedule scopes. In Profile scope I specified two example placeholders – "{StudentName}" and "{StudentSSN}" – which are replaced by the corresponding data values when the page is rendered. The placeholder does not have to have a curly bracketed token format – it's just a string replaced by another string by simple find-and-replace operation within data scope, so the format can really be anything.
4.3. Scope trees
I already mentioned that data scopes in an HTML template have a hierarchical structure, so I call this structure a scope tree. It is very convenient and visual to view and depict HTML templates as scope trees while planning your HTML template structure. Inside the SF the HTML templates and the rendered results are also represented by scope tree data structures. The developer can access these data structures inside the controller classes to manipulate specific data scopes in the tree.
Scope tree representing the structure of DIV
tags in the HTML template is called a model scope tree. Let's depict the model scope tree for the part of HTML template on Fig. 5. We simply convert hierarchical DIV
structure of HTML template into the tree with nodes named as corresponding scopes. The result is on Fig. 6:
Fig. 6: Model scope tree for HTML template on Fig. 5
After the template is rendered, the result is represented by a rendered scope tree. It can be different from a model scope tree, because during rendering process some scopes can repeat their contents with all child data scopes. So the rendered scope tree gets new scope branches coming from the nodes corresponding to the scopes that repeat their contents. If none of the scopes is repeated, then rendered scope tree is the same as model scope tree. Otherwise, the model scope tree is a sub-tree of a rendered scope tree. Assuming that our student is repeated 3 times, let's now depict the rendered scope tree representing DIV
tags structure of the final HTML output resulted from rendering a partial HTML template on Fig. 5. If there are 3 students, the Profile and Schedule scopes are repeated 3 times. I put the model part of the tree on the darker background to emphasize that rendered scope tree is simply a model tree with some branches multiplied. The result is on Fig. 7:
Fig. 7: Rendered scope tree for HTML template on Fig. 5 (student record repeated 3 times)
Scope trees fit very well into the concept of presentation separation. All the developer needs to do, is to create a model scope tree which physically is just a set of nested DIVs. The model scope tree defines the structure of the HTML template unambiguously and this is all the system needs to use this template to display data from the controller. After the model tree is ready, the skeleton template containing just DIV
tags with placeholders can be given to the HTML expert. Then that expert has 100% freedom to do anything with the template as long as the scope tree structure is kept. So the developer does not have to bother about a single line of makrup, and the designer does not have to know anything about the server-side just keeping the scope DIV
tags nesting structure untouched.
4.4. Practice: Scope trees for Students Application
A process of creating a model scope tree that defines the HTML template actually requires sitting a little bit with a pen and a paper. In the previous section we already created the model and the rendered scope trees for a smaller part of an HTML template on Fig. 5. Now, same way we did it for a smaller part of the page, let's build a complete model scope tree for an HTML template to output the entire page from Fig. 1. Obviously, scope DIV
tags in HTML template can be composed in many different ways and there are no strict rules of how to build the model trees, but after practicing a couple of times this task actually becomes quite trivial for the developer with any level of skills.
So, now look at Fig. 1. We need to plan structure of data scopes and decide which placeholders we need. Reasoning here is quite simple. Let's start from the student record that we are already familiar with from section 4.2. So, we need a StudentRepeater scope consisting of a Profile scope and a Schedule scope. Inside Profile scope we need placeholders to display student name, SSN, etc. Let's stick to the format we used already in section 4.2 so the placeholder tokens are "{StudentName}", "{StudentSSN}", etc. Next, Schedule scope consists of an area repeating student courses, the pager to do courses pagination, and a brief summary saying what range of courses is currently displayed. We represent these three areas by three different scopes: CourseRepeater, Pager, and Summary. CourseRepeater has no child scopes, only placeholders to display course ID, name, and time. This scope is repeated to display multiple course records. Summary scope also has only placeholders for start and end page numbers in range. Pager scope is more interesting. This has to display prev and next buttons and be able to disable prev button if current page is 0, and next button if current page is the last one. This is achieved by nesting into the parent Pager scope 4 new child scopes: PrevDisabled, PrevEnabled, NextEnabled, and NextDisabled. The idea is to show enabled scopes when corresponding buttons are enabled, and show disabled scopes otherwise. Now we're done with the student record. We need another pager under StudentRepeater to do pagination of student records. Let's also call it Pager scope and this scope has exactly the same structure as the second Pager scope used to page student courses. Finally, to have neater structure let's wrap StudentRepeater and Pager scopes into the GridArea data scope. And we're done! Model scope tree is ready which automatically means that skeleton HTML template is also ready, since it's just a physical reflection of the model scope tree.
Fig. 8 shows both the model scope tree and the complete rendered scope tree for Default.aspx page of Students Application. Fig. 8 has many details that we did not discuss yet. This is because I decided to use this single figure to visualize all further explanations and discussions on SF architecture, and have one consolidated picture instead of multiple smaller ones. So, currently we are interested only in a model tree depicted in yellow on a darker gray background titled "model scope tree". Placeholders for corresponding scopes are displayed as tree node comments in light gray font.
Fig. 8: Model and rendered scope trees for Students Application
First of all, you should notice is that there is a NULL scope in the root of the scope tree. NULL scope is used as a container for all other data scopes on a page. If there are no scopes in the template, then scope tree consists of a single root node which is NULL scope. Root scope is treated by the system just like any other data scope, except that all other data scopes are physically represented by HTML fragments wrapped in DIV
tags inside the HTML template, while NULL scope does not have a DIV
tag around its content, because its HTML fragment is the entire HTML template. Placeholders placed outside of all other scopes belong to the root scope.
Second, there are PopupPlaceholder and Popup2Placeholder data scopes that we did not discuss yet. These two are needed for "Popup 1" and "Popup 2" functionalities described in section 1. On Fig. 8 I was too lazy to draw the complete model tree branches coming out of these scopes and just depicted them using ellipsis, but in reality these two data scopes have branches coming out of them similar to ones coming out from StudentRepeater scope. I will briefly explain the popup windows in the end of the article, but for now just ignore these two scopes.
Third, CourseRepeater scope has only markup with placeholders inside it and does not contain any child scopes, but since its content is repeated, I needed to emphasize this somehow on Fig. 8, so I just depicted the content by ellipsis.
We're done with the model tree! It's actually quite simple to come up with an optimal scope tree structure for web page of any complexity. Again, we do not have to do any markup for the page – all we need is a structure done in 10 minutes and everything else is totally up to graphics designer. Now that we have a model tree, it's interesting to examine what would be the rendered scope tree after the model tree is rendered.
Let's assume that student record is repeated 3 times and courses for each student record are also repeated 3 times as on a screenshot on Fig. 1. The resulting rendered scope tree is depicted on Fig. 8 as a combination of trees on darker-gray and light-gray backgrounds. I already mentioned that model scope tree is always a subtree of a rendered scope tree, so Fig. 8 illustrates this very well. Bold white arrows beside StudentRepeater and CourseRepeater in the model scope tree mean that contents of these scopes are repeated. By assumption CourseRepeater repeats its content 3 times. On Fig. 8 in a model tree node for CourseRepeater scope you see that its content depicted by ellipsis is repeated 3 times. I intentionally put 2 of these ellipses on the light-gray background to emphasize that repeated content already belongs to the rendered scope tree. Next, look at the StudentRepeater. It has two branches coming out of it starting from Profile and Schedule scopes. In the rendered scope tree these branches are repeated 3 times, so I put two additional branches on the light-gray background meaning that these branches appear only when the model tree is rendered.
Ok, I hope you got the idea how the model scope tree becomes a rendered tree. Although everything here is trivial, you should make sure that you understand scopes and trees completely before moving any further, because everything else that we're going to look at in SF is build around data scopes and scope trees.
4.5. Practice: StudentsPage.htm template
It's now time to build an actual template. First, I'm going to build a skeleton template containing only scopes and placeholders without any markup. Then I'll provide the template that we have in our demo application so that you can compare the differences. To build a skeleton template represented by a model scope tree, we don't really need to do anything. It's just a simple translation of logical scope structure into physical DIV
structure. To make skeleton template smaller, I ignore data scopes for both popup windows. Here is a listing of a skeleton template corresponding to the model scope tree on Fig. 8:
Listing 3: Skeleton HTML template
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml">
3 <head>
4 <title></title>
5 <style>div {border:solid 1px black;margin:5px;padding:5px;}</style>
6 </head>
7 <body>
8 <div scope="GridArea">
9 <div scope="StudentRepeater">
10 <div scope="Profile">
11 {StudentName} {StudentSSN} {Major} {PhoneNumber}
12 </div>
13 <div scope="Schedule">
14 <div scope="CourseRepeater">
15 {CourseID} {CourseFullName} {StartTime} {EndTime}
16 </div>
17 <div scope="Summary">
18 {FromCourse} {ToCourse}
19 </div>
20 <div scope="Pager">
21 <div scope="PrevDisabled"></div>
22 <div scope="PrevEnabled">{PrevPageIdx}</div>
23 {CurrPageNum} {TotalPageCount}
24 <div scope="NextEnabled">{NextPageIdx}</div>
25 <div scope="NextDisabled"></div>
26 </div>
27 </div>
28 </div>
29 <div scope="Pager">
30 <div scope="PrevDisabled"></div>
31 <div scope="PrevEnabled">{PrevPageIdx}</div>
32 {CurrPageNum} {TotalPageCount}
33 <div scope="NextEnabled">{NextPageIdx}</div>
34 <div scope="NextDisabled"></div>
35 </div>
36 </div>
37 </body>
38 </html>
I added just a bit of CSS to make this structure look neater in the browser. Fig. 9 shows how the skeleton HTML template looks in the IE window:
Fig. 9: Skeleton HTML template in IE
Although this page is not much like the one on a screenshot on Fig. 1, from the scope tree point of view these two pages are identical and our skeleton template can be easily turned to the desired template by adding more HTML markup and CSS, but keeping the same structure of the scope tree. So, finally, listing 4 shows the complete StudentsPage.htm template used in Students Application. This is how it would look after the graphics designer worked on it. Notice the structure of scope DIV
tags – it's the same as in skeleton template.
Listing 4: ../App_Data/Templates/StudentsPage.htm
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2
3 <html xmlns="http://www.w3.org/1999/xhtml">
4 <head>
5 <title></title>
6 <style type="text/css">
7 body, html {font-family:Verdana, Arial; font-size:14px;}
8 .jqmOverlay {background-color:Black;}
9 </style>
10 <script type="text/javascript" src="res/Scripts/jquery.min.js"></script>
11 <script type="text/javascript" src="res/Scripts/jqModal.js"></script>
12 </head>
13 <body>
14 <div scope="GridArea" style="margin:20px;">
15 <div style="padding: 10px; display:block; font-weight:bold; background-color: #800000; height: 20px; color: #FFFFFF;" >
16 GRID DISPLAYING STUDENTS
17 </div>
18 <div scope="StudentRepeater">
19 <table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin- top:10px;">
20 -->
21 <tr>
22 <td colspan="3" style="height:2px; background-color: #808080;"></td>
23 </tr>
24 <tr>
25 <td valign="top" align="left" style="width:300px;">
26 <div scope="Profile">
27 <div style="display:block;height:120px;">
28 <table cellpadding="2px" border="0px" cellspacing="2px" width="100%" bgcolor="White">
29 <tr><td style="font-weight: bold; background-color: #808080" colspan="2">Student Profile</td></tr>
30 <tr>
31 <td align="right" style="background-color: #808080; font-weight: bold;">Name:</td>
32 <td style="background-color: #CCCCCC"> {StudentName}</td>
33 </tr>
34 <tr>
35 <td align="right" style="background-color: #808080; font-weight: bold;">SSN:</td>
36 <td style="background-color: #CCCCCC"> {StudentSSN}</td>
37 </tr>
38 <tr>
39 <td align="right" style="background-color: #808080; font-weight: bold;">Major:</td>
40 <td style="background-color: #CCCCCC"> {Major}</td>
41 </tr>
42 <tr>
43 <td align="right" style="background-color: #808080; font-weight: bold;">Phone:</td>
44 <td style="background-color: #CCCCCC"> {PhoneNumber}</td>
45 </tr>
46 </table>
47 </div>
48 <div style="display:block; margin-top: 2px;">Student enrolled in {CourseCount} course(s)</div>
49 <div style="display:block; margin-top: 2px;">
50 <input type="button" value="Popup 1" onclick="AspNetScopes.Action('OpenPopup1', '{StudentSSN}')" />
51 <input type="button" value="Popup 2" class="show-modal-popup2" studentSSN="{StudentSSN}" />
52 </div>
53 </div>
54 </td>
55 <td style="width:2px;" valign="top">
56 </td>
57 <td valign="top" align="left" style="background-color: #CCCCCC">
58 <div scope="Schedule" style="height:100%;">
59 <div scope="CourseRepeater" style="height: 120px; ">
60 <table cellpadding="2px" border="0px" cellspacing="2px" width="100%" bgcolor="White">
61 <tr>
62 <td style="font-weight: bold; background-color: #808080">Course ID</td>
63 <td style="font-weight: bold; background-color: #808080">Full Name</td>
64 <td style="font-weight: bold; background-color: #808080">Time</td>
65 </tr>
66 -->
67 <tr>
68 <td style="background-color: #CCCCCC">{CourseID}</td>
69 <td style="background-color: #CCCCCC">{CourseFullName}</td>
70 <td style="background-color: #CCCCCC">{StartTime} - {EndTime}</td>
71 </tr>
72 -->
73 </table>
74 </div>
75 <div style="margin-top: 2px;display:block;">
76
77 </div>
78 <div style="margin-top: 2px;display:block;">
79 <div scope="Pager" style="display:inline;margin-right:20px;margin-left:5px;background-color:#CCCCCC;" />
80 <div scope="Summary" style="font-style: italic;display:inline;">
81 Displayed courses from {FromCourse} to {ToCourse}
82 </div>
83 </div>
84 </div>
85 </td>
86 </tr>
87 <tr>
88 <td colspan="3" style="height:2px; background-color: #808080;"></td>
89 </tr>
90 <tr>
91 <td colspan="3"> </td>
92 </tr>
93 -->
94 </table>
95 </div>
96 <div scope="Pager" style="display:block;background-color:#CCCCCC;padding:5px;" />
97 <div style="display:block; background-color: #800000; height:10px;">
98 </div>
99 </div>
100 Popup window invoked by "Popup 1" button is implemented <br />
101 using pure ASP.NET Scopes approach. Dialog invoked by <br />
102 "Popup 2" button demonstrates well known common approach <br />
103 to modal windows using 3rd party jQuery plugin. <br />
104 <div scope="PopupPlaceholder" />
105 <div scope="Popup2Placeholder" />
106 </body>
107 </html>
If you went through this listing carefully, you'd notice that content of both Pager scopes is disappeared. This is because instead of duplicating the content for both similarly looking pagers, I factored the markup out into the partial HTML template associated with the child controller, analog of user controls in ASP.NET Forms and partial views in MVC. I'll talk about child controllers and pager implementation in details later.
In the browser window our HTML template looks like the following:
Fig. 10: HTML template StudentsPage.htm in IE
After rendering, this template will give us a desired output depicted on Fig. 1. We also see that working on the HTML template, the graphics designer gets 100% WYSIWYG in the browser window! This is another great thing about SF, because, although integrated Visual Studio designer is a great tool, we often get quite a different output in an actual browser when developing with Forms or MVC. Moreover it's quite common that integrated VS designer fails to render nested controls properly. Using the target browser to work on the template solves this problem completely.
Finally, the only thing we need in order to render this beautiful HTML template is data. We're now coming to the main part of SF programming which is a controller class whose responsibility is to generate list of values to replace placeholders. But before we move to this long discussion, we need to learn about scope paths used by the developer to navigate through the model and rendered scope trees.
4.6. Scope Paths
The controller class is used to generate data, which is bound to each scope individually by the developer. In order to manipulate some specific scope inside the controller, the developer has to select it first. Recall that in rendered scope trees the scopes can be repeated; therefore, we need a way to distinguish, for example, CourseRepeater of the 1st, 2nd, and 3rd student (see Fig. 8). This is exactly what we need scope paths for.
Scope path is simply a unique path to a scope within a scope tree. It is represented by a set of nodes visited in the scope tree walking from its root to the desired scope. Each visited node is represented by a segment consisting of a repeating axis (the index of a repeated scope) and a name of the scope. In rendered scope trees the repeating axis is 0 or any positive integer. In model scope trees the repeating axis is not used, because scopes are not repeated. Note that we don't have to specify the NULL scope anywhere, because every path starts from it anyway. For example, to select CourseRepeater in the model scope tree on Fig. 8, the expression would be:
Select("GridArea", "StudentRepeater", "Schedule", "CourseRepeater")
So, we simply list the nodes in the path by names. In model scope tree it works, because scopes are not repeated and using just a chain of names, we uniquely identify the location of the data scope within the tree. In the rendered scope tree this expression would always select the CourseRepeater of the 1st student. What if we want to select it for the 2nd or 3rd student? The expressions to select CourseRepeater scopes in the rendered scope tree for all 3 students are the following:
Select("GridArea", "StudentRepeater", 0, "Schedule", "CourseRepeater")
Select("GridArea", "StudentRepeater", 1, "Schedule", "CourseRepeater")
Select("GridArea", "StudentRepeater", 2, "Schedule", "CourseRepeater")
What happens here is that we have to specify the repeating axis for Schedule scope in order to stick to the right path. It is not necessary to specify 0
repeating axis in the rendered scope tree paths, because axis is 0 by default. This allows us to have shorter and more readable scope path selection expressions in the controller.
Since scope paths are unique within the rendered scope tree, it is simple and quite an expected solution to use scope paths as client-side identifiers for the corresponding scope DIV
tags. I.e. in addition to the attributes that scope DIV
has inside the HTML template, the id
attribute equal to the client representation of the scope path is inserted when scope is being rendered. The client representation of the scope path consists of segments separated by "$" delimiter. Each segment has a format of "<axis>-<name>". In the client scope paths the axis is always specified even if it is 0. This is needed to provide a consistent way of accessing scope DIV
tags on the client side using document.getElementById()
or jQuery
selectors.
4.7. Practice: Scope Paths in Students Application
The system adds unique ID
attributes to scope DIV
tags, because client-side of the SF needs to know which scope DIV
tags have to be updated on async postbacks. The ID
attribute of the scope DIV
is the only link connecting rendered page on the client side to the controller class on the server-side.
Let's examine some ID
attributes of rendered scope DIV
tags in our Students Application. Take a look at the resulting output of the Default.aspx page in the browser and compare it to the rendered scope tree on Fig. 8. You can make sure that nesting structure of the rendered scope DIV
tags is exactly the one depicted by the rendered scope tree. Don't forget that we're talking about scope DIV
tags only, not any other DIV
tags on the page that just used for markup. The scope
attributes in all rendered scope DIV
tags are replaced by the ID
attributes equal to the corresponding paths to these scopes within the rendered scope tree.
The DIV
corresponding to GridArea scope is rendered as:
<div id="0-GridArea" style="margin:20px;">
This DIV
has two child DIV
tags for StudentRepeater and Pager scopes rendered as:
<div id="0-GridArea$0-StudentRepeater">
<div id="0-GridArea$0-Pager" style="display:block;background-color:#CCCCCC;padding:5px; ">
Now, recall the example in section 4.6 where we used 3 different Select()
expressions passing the scope paths to access the CourseRepeater scope for 1st, 2nd, and 3rd students inside the controller class. We can see that these repeated CourseRepeater scopes are rendered as 3 DIV
tags, one for each student, as following:
<div id="0-GridArea$0-StudentRepeater$0-Schedule$0-CourseRepeater" style="height: 120px; ">
<div id="0-GridArea$0-StudentRepeater$1-Schedule$0-CourseRepeater" style="height: 120px; ">
<div id="0-GridArea$0-StudentRepeater$2-Schedule$0-CourseRepeater" style="height: 120px; ">
So, the mechanism of scope paths is quite trivial. The ID
attributes of the scope DIV
tags reflect the paths used to select them inside the controller classes. I suggest you always spend a bit of time with a pen and a paper to carefully plan your model scope tree, because this helps to better see the whole picture and write the correct Select()
expressions in the controller class without any difficulties.
5. Rendering Process and Controller Classes in SF
5.1. Controller class overview
A controller class is located in a file with .cs extension. Every controller in SF has to be inherited from ScopeControl
abstract class. The diagram of ScopeControl
abstract class is depicted on Fig. 11:
Fig. 11: ScopeControl class diagram
Typical controller class inherited from ScopeControl
contains a number of methods. On the top-level these methods can be subdivided into 4 primary groups depending on their responsibilities:
SetTemplate()
method – used to associate the specific HTML template with the controller.
SetupModel()
method – used to add child controllers, attach action handlers, and attach data binding handlers.
- Action handlers – used to process actions on async postbacks.
- Data binding handlers – used to provide data for scopes in the scope tree.
5.2. Practice: PageStudents.cs controller listing
In section 3.4 on listing 2 we passed the instance of PageStudents
controller class to SF core telling the system that all further processing and rendering must be done by PageStudents
controller. Besides DataFacade.cs class emulating data layer (not discussed in this article), all other back-end code of Students Application is located in the controller classes. Let's start discussing the PageStudents
controller from giving the complete code listing of this class. Don't try to understand the details of controller implementation – in further discussion I'll explain every single line of code in this class. The following is a complete listing of PageStudents.cs file:
Listing 5: ../App_Code/Controls/PageStudents.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Web;
5 using System.IO;
6 using System.Web.Hosting;
7
8 using AspNetScopes.Framework;
9
10 11 12 13 public class PageStudents : ScopeControl
14 {
15 public override void SetTemplate(ControlTemplate template)
16 {
17 template.Markup = File.ReadAllText(HostingEnvironment.MapPath("~/App_Data/Templates/StudentsPage.htm"));
18 }
19
20 public override void SetupModel(ControlModel model)
21 {
22 model.Select("GridArea", "Pager").SetControl(new PagerControl());
23 model.Select("GridArea", "StudentRepeater", "Schedule", "Pager").SetControl(new PagerControl());
24 model.Select("PopupPlaceholder").SetControl(new PopupControl());
25 model.Select("Popup2Placeholder").SetControl(new Popup2Control());
26
27 model.Select("GridArea").SetDataBind(new DataBindHandler(GridArea_DataBind));
28 model.Select("GridArea", "StudentRepeater").SetDataBind(new DataBindHandler(StudentRepeater_DataBind));
29 model.Select("GridArea", "StudentRepeater", "Profile").SetDataBind(new DataBindHandler(Profile_DataBind));
30 model.Select("GridArea", "StudentRepeater", "Schedule", "Summary").SetDataBind(new DataBindHandler(Summary_DataBind));
31 model.Select("GridArea", "StudentRepeater", "Schedule", "CourseRepeater").SetDataBind(new DataBindHandler(CourseRepeater_DataBind));
32
33 model.Select("GridArea", "Pager").HandleAction("NextPage", new ActionHandler(Pager1_NextPage));
34 model.Select("GridArea", "Pager").HandleAction("PrevPage", new ActionHandler(Pager1_PrevPage));
35
36 model.Select("GridArea", "StudentRepeater", "Schedule", "Pager").HandleAction("NextPage", new ActionHandler(Pager2_NextPage));
37 model.Select("GridArea", "StudentRepeater", "Schedule", "Pager").HandleAction("PrevPage", new ActionHandler(Pager2_PrevPage));
38
39 model.HandleAction("OpenPopup1", new ActionHandler(Action_OpenPopup1));
40 }
41
42
43
44 private void Pager1_NextPage(ActionArgs args)
45 {
46 Scopes.ActionPath.Rew(1).Fwd("StudentRepeater").Context.Refresh();
47 Scopes.ActionPath.Context.Refresh();
48 }
49
50 private void Pager1_PrevPage(ActionArgs args)
51 {
52 Scopes.ActionPath.Rew(1).Fwd("StudentRepeater").Context.Refresh();
53 Scopes.ActionPath.Context.Refresh();
54 }
55
56 private void Pager2_NextPage(ActionArgs args)
57 {
58 Scopes.ActionPath.Rew(1).Fwd("CourseRepeater").Context.Refresh();
49 Scopes.ActionPath.Rew(1).Fwd("Summary").Context.Refresh();
60 Scopes.ActionPath.Context.Refresh();
61 }
62
63 private void Pager2_PrevPage(ActionArgs args)
64 {
65 Scopes.ActionPath.Rew(1).Fwd("CourseRepeater").Context.Refresh();
66 Scopes.ActionPath.Rew(1).Fwd("Summary").Context.Refresh();
67 Scopes.ActionPath.Context.Refresh();
68 }
69
70 private void Action_OpenPopup1(ActionArgs args)
71 {
72 Scopes.CurrentPath.Fwd("PopupPlaceholder").Context.Params["StudentSSN"] = (string)args.ActionData;
73
74 Scopes.CurrentPath.Fwd("PopupPlaceholder").Context.Params["ShowDialog"] = "1";
75 Scopes.CurrentPath.Fwd("PopupPlaceholder").Context.Refresh();
76 }
77
78
79
80 private void GridArea_DataBind(DataBindArgs args)
81 {
82 int studentCount = DataFacade.GetStudentCount();
83
84 Scopes.CurrentPath.Fwd("Pager").Context.Params["StartItemIdx"] = "0";
85 Scopes.CurrentPath.Fwd("Pager").Context.Params["PageSize"] = "3";
86 Scopes.CurrentPath.Fwd("Pager").Context.Params["ItemTotalCount"] = studentCount.ToString();
87 }
89
89 private void StudentRepeater_DataBind(DataBindArgs args)
90 {
91 int startItemIdx = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("StartItemIdx");
92 int pageSize = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("PageSize");
93 int itemTotalCount = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("ItemTotalCount");
94
95 object[] students = DataFacade.GetStudents(startItemIdx, pageSize);
96 for (int i = 0; i < students.Length; i++)
97 {
98 args.NewItemBinding();
99 Scopes.CurrentPath.Fwd(i, "Profile").Context.Params.AddRange(students[i], "StudentName", "StudentSSN", "Major", "PhoneNumber");
100 }
101
102 103 Scopes.CurrentPath.Rew(2).Fwd("Popup2Placeholder").Context.Params["StudentRepeaterID"] =
104 Scopes.CurrentPath.Context.ScopeClientID;
105 }
106
107 private void Profile_DataBind(DataBindArgs args)
108 {
109 int courseCount = DataFacade.GetCourseCount(Scopes.CurrentPath.Context.Params["StudentSSN"]);
110
111 Scopes.CurrentPath.Rew(1).Fwd("Schedule", "Pager").Context.Params["StartItemIdx"] = "0";
112 Scopes.CurrentPath.Rew(1).Fwd("Schedule", "Pager").Context.Params["PageSize"] = "3";
113 Scopes.CurrentPath.Rew(1).Fwd("Schedule", "Pager").Context.Params["ItemTotalCount"] = courseCount.ToString();
114
115 args.NewItemBinding();
116 args.CurrBinder.Replace(Scopes.CurrentPath.Context.Params, "StudentName", "StudentSSN", "Major", "PhoneNumber");
117 args.CurrBinder.Replace("{CourseCount}", courseCount);
118 }
119
120 private void CourseRepeater_DataBind(DataBindArgs args)
121 {
122 string studentSSN = Scopes.CurrentPath.Rew(2).Fwd("Profile").Context.Params["StudentSSN"];
123
124 int startItemIdx = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("StartItemIdx");
125 int pageSize = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("PageSize");
126 int courseCount = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("ItemTotalCount");
127
128 object[] courses = DataFacade.GetCourses(studentSSN, startItemIdx, pageSize);
129 for (int i = 0; i < courses.Length; i++)
130 {
131 args.NewItemBinding();
132 args.CurrBinder.Replace(courses[i]);
133 }
134 }
135
136 private void Summary_DataBind(DataBindArgs args)
137 {
138 int startItemIdx = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("StartItemIdx");
139 int pageSize = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("PageSize");
140 int courseCount = Scopes.CurrentPath.Rew(1).Fwd("Pager").Context.Params.GetInt("ItemTotalCount");
141
142 int fromNum = startItemIdx + 1;
143 int toNum = startItemIdx + pageSize < courseCount ? startItemIdx + pageSize : courseCount;
144
145 args.NewItemBinding();
146 args.CurrBinder.Replace("{FromCourse}", fromNum);
147 args.CurrBinder.Replace("{ToCourse}", toNum);
148 }
149 }
Although the code on listing 5 is difficult to understand with our knowledge so far, you should clearly recognize the top-level groups of methods that we discussed in section 5.1. SetTemplate()
is implemented on line 15, SetupModel()
is implemented on line 20, action handlers are on lines 44-70, and the rest are data binding handlers on lines 80-136.
5.3. Practice: Associating StudentsPage.htm with StudentsPage controller
To associate an HTML template with the controller, we have to implement the SetTemplate()
method of the ScopeControl
class (ref Fig. 11). This is done on line 15 of listing 5. SetTemplate() method has an argument template
of type ControlTemplate
whose diagram is shown on Fig. 12:
Fig. 12: ControlTemplate class diagram
On line 17 we simply use the template.Markup
property to pass the raw content of the HTML template to the SF core. We read the StudentsPage.htm file (see section 4.5) from its physical location on the disk and assign the result to the Markup
property. This is it! Imagine now how flexible you can be choosing your presentation templates on the fly.
5.4. Data binding design in SF
The process of generating data in the controller is called data binding to keep consistency with Forms development. Data binding is the most complicated part of SF site development. Data is provided individually for each node in the scope tree. For this purpose, the developer implements a set of data binding handlers inside the controller class. One handler is responsible for binding a single scope node in a scope tree. SF invokes binding handlers for each data scope as scope tree is being traversed during page rendering process. Handlers are called using post-order walk with top-to-bottom in-order traversal. Recalling graphs from high school, it's actually called right-to-left in order traversal, but because I draw scope trees horizontally (like on Fig. 8), I call it top-to-bottom. This traversal order is obviously chosen because the actual scope DIV
tags in HTML template are encountered exactly in this order. The rendering logic with data binding handlers is extremely simple and transparent and this is all rendering process takes in SF!
The SF must be told explicitly which data binding handlers should be called for which data scopes. For this purpose all handlers inside the controller class have to be attached to the corresponding scope tree nodes inside SetupModel()
method implemented by the developer. As Fig. 11 states, the SetupModel()
method is passed model
variable of type ControlModel
whose diagram is shown on Fig. 13:
Fig. 13: ControlModel class diagram
So variable model
of type ControlModel
is used by the developer to select specific data scopes and attach data binding handlers to them. The typical actions that we should do to data bind the scope are the following:
- Select the specific data scope in the scope tree passing scope path to
Select()
method. Note that inside SetupModel()
method scopes are selected within the model scope tree. An attempt to specify a repeating axis will result in an error.
- Call
SetDataBind()
method of SelecteNodeSetup
class (ref Fig. 13) passing the appropriate delegate to it. The instance of SelectedNodeSetup
class is returned by the previously called Select()
method – this design is just needed to allow single line binding expressions.
The typical data binding expression inside SetupModel()
method looks like the following:
model.Select(<some_scope_path>).SetDataBind(new DataBindHandler(<some_delegate>));
Root scope just like any other scope in the scope tree can have a data binding handler. There is no need to select the root scope in data binding expressions, and SetDataBind()
method can be called directly:
model.SetDataBind(new DataBindHandler(<some_delegate>));
5.5. Practice: Attaching data binding handlers in PageStudents controller
Lines 27-31 in PageStudents
controller or listing 5 attach data binding handlers to the data scopes of the model tree on Fig. 8. Line 27 adds GridArea_DataBind()
function as a data binding handler for GridArea scope. Line 30 adds Summary_DataBind()
function as a data binding handler for Summary scope. And so on. As discussed in the previous section, we have to point at the specific scope using Select()
function and then call SetDataBind()
passing the delegate to it. We do not databind the root scope, because there is no need for this in PageStudents
controller.
As I already mentioned, we have to remember that inside SetupModel()
method we are working with the model scope tree only, not the rendered scope tree, because rendering process is not started yet. So all scope paths passed to Select()
functions do not have repeating axes in them.
Finally, we don't have to attach data binding handlers to all data scopes. Add handlers only for those scopes that need data to replace placeholders or repeat contents. For example, Schedule scope is just a container for 3 other scopes and do not have any placeholders, so we don't really need to bind any data to it.
5.6. Rendering and controller execution steps
Fig. 3 provided us a top-level overview of SF page execution steps. The biggest interest for us is in steps 3 and 4, because this is where all magic happens i.e. the controller is executed and the page is rendered. It's time now to delve into the SF core and take a closer look at these processes. Although this discussion requires knowledge of some SF features that we did not learn yet, I think it's more appropriate to talk about the rendering process right now so you can have a whole rendering process design picture as we go through the SF architecture in the further sections. You don't have to understand every detail in this discussion – they will become cleaner as we learn more in the sections that follow, but you should get a feeling of a rendering process and the order of activities that SF takes to render the final output on initial page load and on an async postback.
So, let's take a typical use case. The end user comes to a Student Application site with a purpose to get information about a certain student and his schedule. User navigates to the Default.aspx page and the page is rendered on initial load. Then he wishes to get the next page of students and clicks the pager button to paginate student records. This action initiates an async postback to the server. The certain part of the page is re-rendered and the updated fragment is refreshed in the browser using partial update approach. Then user repeats actions resulting in async postback an unlimited number of times until he finds the desired information and closes the browser.
The following are the detailed activities taken by SF while executing the controllers and rendering the results for our use case:
- The initial request comes to the Default.aspx page (see section 3.4).
- The SF recognizes that the page have to be served by the controller/template pair (see section 3.1) and the controller object (see section 5.1) is retrieved by invoking the
ProvideRootControl
event (see section 3.3).
- System invokes
SetTemplate()
method on the controller object (see section 5.1) to get the HTML template associated with the controller class.
- The HTML template markup is parsed by the system and the model scope tree data structure is build internally based on the structure of scope
DIV
tags inside the HTML template markup. If scope DIV
tags are not well formed, the exception is thrown.
- System invokes
SetupModel()
method on the controller object (ref section 5.1) to populate the model scope tree built on the previous step with data binding handlers (see section 5.4), action handlers (see section 6.3), and child controllers (see section 5.7).
- If there are any child controllers populated in the model scope tree on the current step, then steps 3 to 5 are repeated for each child controller recursively, so when this process finishes, we have a complete model scope tree populated with all child controllers and data binding and action handlers.
- System starts the rendering process on the initial page load. The rendering process consists only of the data binding process plus output of the resulted HTML. During data binding process, the SF core invokes the data binding handlers one by one for each of the data scopes in the scope tree (see section 5.4) starting from a NULL scope. After the binding handler for the certain data scope in the tree finishes, the placeholders in this scope markup are replaced using the data generated in the handler and the resulted HTML fragment is added to the final page HTML output immediately. As a result, when the whole data binding process is finished, we have a completely rendered HTML page.
- When rendering process starts the model scope tree becomes treated as a rendered scope tree. Depending on data values generated inside the binding handlers, the data scopes can be repeated multiple times. This means that in order to manipulate the scopes in the rendered scope tree inside data binding handlers, we have to specify repeating axes in
Select()
expressions (see section 4.6).
- After rendering is finished and before sending the output back to the end user, the system uses the
ViewState
mechanism to persist all scope contexts and parameters (see section 5.9) for data scopes of the rendered scope tree.
- The page is output to the end user for the first time.
- User wants to see the next set of student records and clicks the pager button to switch to the next page of students. As a result, the client-side action is raised (see section 6.3) and an async postback to the Default.aspx page is initiated.
- Steps 2 to 5 are executed on the async postback exactly the same way they were executed on the initial page load so the complete model scope tree is build again.
- Scope contexts saved on step 7 in
ViewState
are retrieved the most recent rendered scope tree is restored. Obviously, this step is executed only on the postback.
- System processes the action, which initiated the async postback to the server. The action is identified by its name and the path to the action source scope where the action is raised (see section 6.1). SF finds the controller responsible for the action source scope. If the controller has an action handler for the current action (see section 6.3), this handler is invoked. Inside the action handler, the developer can access the rendered scope tree restored on the previous step and modify data scope contexts and parameters. If needed, the current controller can raise the controller action to let the parent controller handle it (see section 6.4). In the end of the action handler functions, the developer has to explicitly specify which data scopes of the rendered scope tree need to be refreshed (see section 6.5).
- System starts the rendering process on an async postback. The rendering process consists of a data binding process again which invokes the data binding handlers one after another, but, as opposed to the initial page load, the rendering process on async postback does not start from the NULL scope. Instead, the SF re-renders scope tree branches starting from data scopes refreshed inside the action handler implementations on the previous step. This means that only those binding handlers are invoked, that correspond to the data scopes in these re-rendered scope tree branches. When scope tree branch is re-rendered starting from the certain refreshed scope, then contexts and parameters of all scopes on this branch are discarded, because the branch of the rendered scope tree is rebuilt from the beginning. When data binding process finishes, the output resulted from re-rendered sub-trees is collected and added to the standard AJAX response.
- After rendering is finished and before sending the AJAX response back to the user, the system uses the
ViewState
mechanism again to persist the updated scope contexts and parameters for data scopes of the rendered scope tree. The rendered scope tree is updated on the previous step and its structure can be different from the tree restored on step 11, since one or more of its branches are rebuilt.
- The response comes back to the browser. Partial contents are picked up by the client-side of the framework and are inserted in the
DIV
tags corresponding to the refreshed data scopes.
- We are done. The partial update is beautifully finished and the user gets the updated page in the browser window. Further, steps 9 to 16 are repeated multiple times for different actions until the user gets all the desired student information.
The overall activity flow should be quite simple and transparent. Note that I intentionally use term "execution steps" instead of "page lifecycle" how we used to call it in standard ASP.NET Forms. This is because I want to emphasize that there is no page lifecycle in SF. Recall that working with the numerous controls in standard ASP.NET Forms we always had to watch where and when our code gets executed which severely impacted the design of the codebehind classes. In SF, while the system itself goes through quite a few activities before the page is rendered as we just have learned, from the developer point of view the entire "page lifecycle" boils down to the simple and strictly defined data binding process! And, as long as model scope tree is known, the order in which binding handlers are executed and the whole data binding process are defined with mathematical exactness without even a small chance for any type of ambiguity.
5.7. Child controllers
The controller that we pass to SF by handling ProvideRootControl
event of ScopesManagerControl
(see listing 2 in section 3.4) is called a root controller. The root controller is responsible for rendering the entire model tree by default. If a controller is responsible for rendering the subtree starting from some scope in a model tree, I say that the controller is assigned to this scope. So, the root controller is always assigned to a NULL scope and renders the entire page by default. It's time to redefine the term root scope respectively to the controller. A root scope of the controller is the scope, to which this controller is assigned. A root scope of a root controller is always a NULL scope in a model tree. A root scope of a child controller in general can be any scope in a model tree.
Under certain circumstances we want some parts of the page to be rendered by the child controllers. From functional point of view, child controller in SF is the same as user control in ASP.NET or partial view in MVC. In Students Application the most obvious candidate to become a child controller is a Pager scope, because we don't want to duplicate data binding logic for two absolutely identical pagers.
Assigning child controllers to scopes in the model tree is similar to attaching data binding handlers (ref section 5.5). The controllers are assigned in two steps inside SetupModel()
method using variable model
of type ControlModel
passed to the function as argument:
- Select the specific data scope in the scope tree passing scope path to
Select()
method.
- Call
SetControl()
method of SelecteNodeSetup
class passing the controller instance to it.
The typical expression to assign child controller looks like the following:
model.Select(<some_scope_path>).SetControl(<controller_instance>);
Unlike ASP.NET Forms where pages and user controls are inherited from different parent classes, all controllers in SF are inherited from ScopeControl
(ref Fig. 11) and implemented the same way no matter if this is a root controller or a child controller.
NOTE: There is one additional requirement for the root controller in the current implementation. The HTML template associated with the root controller has to contain a complete HTML page that includes <html>
, <head>
, and <body>
tags. This requirement is set due to a quick and dirty implementation of the Alpha version of the framework and will likely go away in the future. Also note that in the current implementation, in addition to associating HTML template with the controller by overriding SetTemplate()
method, the template for child controller can also be provided by the parent controller. I did not decide yet if this feature is needed or not, but it adds some interesting flexibility allowing to override child controller HTML template by providing markup in the HTML template of a parent controller.
Finally, it is extremely important to understand that the single instance of the controller assigned to some data scope with SetControl()
method is used by SF to render this scope and its child scopes as necessary on all repeated tree branches. Recall that using repeaters or data grids in ASP.NET Forms, we could access the actual control instances inside item templates during data binding process. I.e. standard ASP.NET always creates physical instances of the repeated controls that we can access. In SF controllers are not created by the system. The developer is responsible for instantiating the controller and passing its instance to the SetControl()
method. Ok, but how would we distinguish controllers on the repeated scope tree branches? This is achieved by the mechanism of data scope contexts that we will discuss soon in the next theoretical section.
5.8. Practice: Child controllers in Students Application
The controllers assigned to the scopes of the model scope tree in Students Application are depicted on Fig. 8 as white labels on red backgrounds wrapping the corresponding data scopes. White label contains a name of the controller class assigned to the data scope. On listing 2 we already assigned PageStudents
controller to the root data scope, so NULL scope in the model tree on Fig. 8 has a red wrapper around it labeled "PageStudents". Besides PageStudents
controller, there are 3 more controllers used in the application. There is a PagerControl
controller used to page both students and student courses i.e. it is assigned to both Pager scopes on Fig. 8. Also, there are PopupControl
and Popup2Control
controllers assigned to PopupPlaceholder and Popup2Placeholder scopes respectively that are needed to demonstrate some advanced functionalities which I'll talk about later.
Look at SetupModel()
function of PageStudents
controller on listing 5. Lines 22-25 assign child controllers to the specific scopes. First, the scopes are selected and then SetControl()
is called on the selected scope. To page student records, line 22 assigns PagerControl
controller to Pager scope located under StudentRepeater (see Fig. 8). Line 23 assigns new instance of the same controller to Pager scope under CourseRepeater to page student courses. And so on.
5.9. Scope contexts
Before we jump to the main part of the practice which is action and data binding handlers in Students Application, we need to learn about scope contexts and parameters. Each scope in the rendered scope tree like the one on Fig. 8 has a context associated with it. Scope context is used to persist various parameters of the data scope through async postbacks. Scope context object can be retrieved by specifying scope path to the desired scope. Note that contexts are available only on rendering stage i.e. you can use them only inside action or data binding handlers.
Look at ScopeControl
diagram on Fig. 11. Each controller inherited from ScopeControl
has the Context
and the Scopes
properties. Context
property is needed to access the context of the data scope which current controller is assigned to. If the controller is assigned to the certain scope, then the context of this scope becomes a context of the assigned controller. I.e. ScopeControl.Context
is just a quick reference to the context of the root scope of the current controller. To access contexts of any scopes beyond the controller root scope, the developer uses Scopes
property inside action and binding handlers. Inside data binding handler the Scopes.CurrentPath
property always points to the current scope i.e. the scope for which data binding handler is invoked. For example, if Profile_DataBind()
binding handler of PageStudents
controller (ref listing 5, line 107) is invoked to render Profile scope of the 3rd student record, then Scopes.CurrentPath
points to "0-GridArea$0-StudentRepeater$2-Profile" scope. Scopes.CurrentPath
property is of type ScopePathNavigator
whose diagram is given on Fig. 14:
Fig. 14: ScopePathNavigator class diagram
The members of this class are the following:
Scopes.CurrentPath.Fwd()
advances the pointer forward from current scope. To this function you pass a scope path relative to the current scope node. Exception is thrown if scope is not found on the specified path. Note that since we are on rendering stage now, the scope paths must contain repeating axes. If repeating axis is 0, it still can be omitted to allow shorter scope path specifications. You are allowed to move the pointer around the rendered scope tree that is served by the current controller only. You're also allowed to access the context of a root scope of a child controller, but not beyond it.
Scopes.CurrentPath.Rew()
takes integer number of segments as a parameter and moves the pointer backward on the current path by specified number of segments. Exception is thrown if number of steps specified is 0 or it exceeds the number of segments (scopes) in the path. The axis of the most recently rolled back segment is always preserved and restored if Fwd()
is called right after Rew()
. This is an important behaviour of ScopePathNavigator
ensuring that we always stick to the right path navigating back and forth in the rendered scope tree. In the practice example that follows I demonstrate how this behaviour works.
Scopes.CurrentPath.Context
simply gets the context object of the current scope. If current scope is a controller root scope then Scopes.CurrentPath.Context
is the same as ScopeControl.Context
.
Context
property is of type RenderScopeContext
whose diagram is shown on Fig. 15:
Fig. 15: RendeScopeContext class diagram
Let's briefly explain the members of this class:
Refresh()
function instructs SF to re-render the scope tree starting from the current scope. This function is used on async postback to achieve partial page updates.
ScopeClientID
is the value of ID attribute inserted into the scope DIV
on rendering stage.
Params
returns the ScopeParams
object used to manipulate scope parameters.
IsVisible
can be set to FALSE
to instruct SF not to render scope contents.
The Params
property is of type ScopeParams
and it is used very frequently throughout our implementation. This property allows us to add and retrieve parameters to each of the scopes in the rendered scope tree. The diagram of ScopeParams
class is shown on Fig. 16:
Fig. 16: ScopeParams class diagram
The members of this class do the following:
ContainsKey()
function checks if param with a certain name exists in the collection.
Get()
returns the param by name (or exception if not found). Overloaded Get()
function has an option to specify the default value.
GetInt()
returns the param by name converted to integer (or exception if not found or cannot convert) with an option of specifying the default value.
SetInit()
add the param if only this param is not added to the collection yet.
AddRange()
takes an object and property names. If property names are specified, then these properties are retrieved from the object using reflection and added to the parameters collection. If propertyNames
are not specified, then all public properties of the object are retrieved and added to the collection. As an option, AddRange()
can take another ScopeParams
object instead of a generic object. This is very useful when we want to quickly transfer all or specific params of one scope to another.
NOTE: In Alpha version only string scope parameters are allowed, because I was lazy to implement the serialization. If you wish to save some generic object, you need to serialize it to the string using XML or JSON format. In the future versions we will be able to use any [Serializable]
or [DataContract]
objects as scope parameters.
Finally, let's add more details to the discussion on controller instances that we started in the end of section 5.7. Now we know how scope contexts work and it becomes easy to distinguish, let's say, courses pager for the 1st student from the one for the 2nd student. Although, the controller instance stays the same, the context of the current scope changes, so if we saved any params for 2nd student, we can now retrieve these params using scope context. This means that we should not use member variables in the controller class to hold any parameters, because there is only one instance of the controller per corresponding scope in the model scope tree. The context mechanism must be used instead to hold all the parameters used in back-end logic. Also recall that to pass parameters to some user control in ASP.NET Forms we had to obtain the instance of a user control and directly call its properties or methods. In SF we pass parameters to controllers using scope contexts. In the next section I'll provide a couple of examples so you can understand the contexts better.
5.10. Practice: Scope contexts
First, let's give an example of moving the pointer around the rendered scope tree on Fig. 8. Assume that currently being rendered scope is the 3rd student's Profile and we are inside the data binding handler for this scope. Scopes.CurrentPath
points at "0-GridArea$0-StudentRepeater$2-Profile" and we wish to access the context of Pager scope used to page courses in the same student record. Looking at Fig. 8, we see that to get to courses Pager scope, we need to go 1 step back and then forward to Schedule and Pager. The resulting call would be:
Scopes.CurrentPath.Rew(1).Fwd("Schedule", "Pager")
This would set the pointer to Pager data scope located at "0-GridArea$0-StudentRepeater$2-Schedule$0-Pager" scope path. We rolled one step back and then went 2 steps forward without specifying any axis, but how did it know that axis of Schedule scope should be 2
? Recall from section 5.9 that axis of Profile scope is preserved and added to the path automatically, unless we explicitly override it with an integer number. This is good, because we don't necessarily know and actually we don't want to know which student record is currently being rendered, we just want to get the Pager in the same student record so segment axis preservation allows us to do exactly this.
Another example. Assume that currently rendered scope is the 3rd student's Profile again, but now for whatever reason I wish to access the context of Pager scope inside the 2nd student record. The expression would be:
Scopes.CurrentPath.Rew(1).Fwd(1, "Schedule", "Pager")
Since the current path is "0-GridArea$0-StudentRepeater$2-Profile", axis 2
will be preserved if we don't explicitly specify the desired axis. So, we just override it by axis 1
and get the desired Pager. The resulting scope path is "0-GridArea$0-StudentRepeater$1-Schedule$0-Pager".
Finally, let's see how context changes for repeated scopes. In rendered scope tree on Fig. 8 the pager for student courses is repeated 3 times, because we have 3 different student records. On listing 5 at line 23 we assigned the instance of PagerControl
controller to the Pager scope. This single instance of the controller is used to render all 3 scope tree branches starting from Pager scopes for all 3 students. When 1st branch is rendered, PagerControl.Context
is the context of the Pager scope with "0-GridArea$0-StudentRepeater$0-Schedule$0-Pager" scope path. When 2nd branch is rendered, PagerControl.Context
is the context of the Pager scope with "0-GridArea$0-StudentRepeater$1-Schedule$0-Pager" scope path. And so on. So controller instance stays the same, but context changes. Obviously, the contexts of all scopes beyond the control root scope also change. In ASP.NET Forms we often saved some data inside codebehind class in order to use this data on later lifecycle stages of the page. Avoid doing this in SF and store all your values and parameters inside scope contexts.
5.11. Data binding handler arguments
Every data binding handler in the controller has an argument of type DataBindArgs
. Fig. 17 shows the diagram of this class:
Fig. 17: DataBindArgs class diagram
NewItemBinding()
method is used to repeat scope child content. To repeat the content N times, the NewItemBinding()
has to be called N times. If method is not called inside the binding handler, then by default system assumes that content is repeated once.
CurrBinder
property has to be used after each NewItemBinding()
call to tell the system which data to use to replace placeholders.
CurrBinder
property is of type ItemBinder
and its diagram is shown on Fig. 18:
Fig. 18: ItemBinder class diagram
Replace()
method is used to replace the actual placeholders. Basic version of this method gets placeholder name and a replacement. Simple find-and-replace operation will be executed to do the replacement. Another option is to pass generic object and a list of property names. In this case properties are retrieved from object by names using reflection and the placeholders are assumed to be property names enclosed in curly brackets. Note that this assumption is an Alpha version limitation and should go away in the future. If property names are not specified, then all public properties of the object are retrieved. Last option is to pass ScopeParams
object. This works as the one before, but instead reflected properties of the object we retrieve named parameters from the collection.
Ok, enough. I think you got a basic idea how to use the function arguments inside data binding handlers to bind data values to placeholder. You'll see more examples as we go. Now it's time to look at the actual data binding handlers. To understand how these guys are implemented, we will need all the SF knowledge that we accumulated so far.
5.12. Practice: Data binding handlers of PageStudents controller
Look at the rendered scope tree on Fig. 8 and on a set of binding handlers on listing 5 at lines 80-136. In what order are they going to be called and what happens when scopes are repeated? In the previous sections I mentioned that handlers are called using post-order walk with top-to-bottom in-order traversal. I visualized this process on the rendered scope tree on Fig. 8: blue labels on the tree nodes represent the order in which handlers corresponding to the data scopes are invoked. Let's examine all handlers in the order they are invoked in PageStudents
controller on listing 5.
GridArea_DataBind()
on line 80 is called first, because we did not attach any handler to the root scope. When this handler is called, Scopes.CurrentPath
is "0-GridArea". On line 82 we call data layer to get the total number of the students. On lines 84-86 you can see that we point to the Pager scope navigating forward and retrieve its context to add parameters to it. What we do here is simply the initialization of the pager. "StartItemIdx"
param is initially set to 0
, "PageSize"
to 3
, and "ItemTotalCount"
to the number of students that we retrieved on line 82.
Ok, next handler to be called is StudentRepeater_DataBind()
on line 89. This one is more complicated. Scopes.CurrentPath
is now equal to "0-GridArea$0-StudentRepeater". On lines 91-93 we again access the Pager scope to get its context, but now we need to go one segment back to GridArea, and then one segment forward to Pager. Lines 91-93 are needed to get current pager values to use them for retrieval of students when calling data layer. You might notice that we use a shortcut GetInt()
function (ref Fig. 16) instead of getting string parameter and converting it to a string. Next, on line 95 we retrieve the actual list of student objects using our pager values. We loop through them on lines 96-100 calling NewItemBinding()
on line 98 for each iteration (ref section 5.11). This method is called to tell the system to repeat the scope content i.e. the number of times the content is repeated is equal to the number of students and this is exactly what we need. Next, on line 99 we navigate to Profile scope and add a range of parameters to it passing the current student object as a source of data. We list the properties that need to be retrieved from the object and that will become the parameter names in parameter collection. Note that we must specify the repeating axis for the Profile which corresponds to the i-th student. Please ignore lines 103-104 – they are the part of more advanced discussion covered in the end of the current article. Note that we did not do any placeholder replacement in this scope. Why? We simply don't need it, because StudentRepeater
scope does not have any placeholders.
Next scope to render is Profile, so Profile_DataBind()
is called on line 107. We will need a total number of courses the student is enrolled in and in data layer we have a function to retrieve this number by student SSN. Now, where do we get the SSN? Recall that on line 99 i-th iteration of a loop adds a range of parameters to the Profile scope for i-th student. Assuming that there were 3 students in the array, the parameters were added to scopes with the following paths "0-GridArea$0-StudentRepeater$0-Profile", "0-GridArea$0-StudentRepeater$1-Profile", and "0-GridArea$0-StudentRepeater$2-Profile". Current path inside the handler is "0-GridArea$0-StudentRepeater$0-Profile" i.e. in the parameters of current scope we can find those values that were set for the 1st student (on the 1st iteration)! Therefore, on line 109 we just retrieve StudentSSN param set on line 99 and use it to get a count of courses. Next 3 lines 111-113 are needed for course pager for the same reason lines 84-86 were needed for student pager i.e. the pager is assigned initial values. Next, on line 115 we call the method to specify that Profile content is repeated once. Line 116 uses shortcut function to replace all placeholders from current scope params which were set on line 99. Line 117 adds extra replacement for total number of courses.
Next scope is Schedule, but since we did not attach handler to it, next scope becomes CourseRepeater and CourseRepeater_DataBind()
is invoked. Current path is "0-GridArea$0-StudentRepeater$0-Schedule$0-CourseRepeater". On line 122 we get back to Profile whose parameters contain all student info and retrieve student SSN from it. Note that again we don't specify any repeating axes, because having been rolled 2 segments back to StudentRepeater, the scope navigator preserves the axis of the pre-last node which was Schedule and then uses it when we jump forward to Profile. On lines 124-126 we access course Pager to get the pager values. Next, on line 128 we use data layer to retrieve array of courses for the current page. On lines 129-133 we loop through all course objects. On line 131 we tell the system to repeat CourseRepeater content for each course and on line 132 we bind data to placeholder just passing the entire course object which means that placeholders with names equal to the reflected public properties of the object are be replaced by the corresponding values.
After CourseRepeater scope there is a Pager scope, but Pager scope has its own PagerControl
controller assigned to it (line 23) meaning that data binding for this scope as well as its child scopes are handled by PagerControl
controller instead of PageStudents
controller. In the next section I'll explain PagerControl
in details; for now just assume that rendering goes to the PagerControl
and once branch starting from Pager is rendered, the rendering comes back to our PageStudents
controller.
Final scope is Summary i.e. Summary_DataBound()
on line 136 is invoked. Current path is "0-GridArea$0-StudentRepeater$0-Schedule$0-Summary". On lines 138-140 we again get pager values. Then on lines 142-143 calculate numbers of first and last displayed courses. And on lines 145-147 we bind the placeholders to display the desired data. Did I just say that Summary was the last scope? Oops, I'm terribly wrong, because on lines 96-100 we repeated the students 3 times meaning that the corresponding subtree representing student record is repeated as well! And this, in its turn, means that our handlers for Profile, CourseRepeater, and Summary scopes are called again in exactly the same order for 2nd and 3rd student!
So, next handler to be invoked is Profile_DataBind()
on line 107 again, but this time current path inside it is "0-GridArea$0-StudentRepeater$1-Profile" which means that we have a different context corresponding to the 2nd student now! As before, total number of courses is retrieved using StudentSSN, but this SSN is now for the 2nd student corresponding to the 2nd iteration of the loop on lines 96-100 where we populated the parameters of Profile scope, i.e. the call to the data layer returns a course count for the 2nd student this time. And so on. I hope you got an idea :)
After student records are rendered and corresponding handlers are called, we are still not finished. The next scope to render is Pager under StudentRepeater, but this Pager also has a controller assigned to it (line 22) so this controller is responsible for calling its own handlers. In the next section we will investigate PagerControl
controller in details.
After strudents Pager and all its child scopes are rendered, the post-order walk comes to the PopupPlaceholder scope and later on to the Popup2Placeholder. I'll not go deeply into the details here – all source code is available, so you can check it yourself. Just like Pager scope, these two scopes have controllers assigned to them and all rendering logic is the same as in PagerControl
child controller which we are just about to examine.
5.13. Practice: PagerControl controller
In this section we will take a closer look at PagerControl
controller. We used this controller inside PageStudents
controller for both Pager scopes (lines 22, 23) which means that PagerControl
is responsible for paging students and student courses. Below I provide the complete listings of HTML template and the controller class. The HTML template is represented by the Pager.htm file whose listing is below:
Listing 6: ../App_Data/Templates/Pager.htm
1 <div scope="PrevDisabled" style="display:inline"><< Prev</div>
2 <div scope="PrevEnabled" style="display:inline">
3 <a href="javascript:AspNetScopes.Action ('PrevPage', {PrevPageIdx})"><< Prev</a>
4 </div>
5
6 <span style="font-weight:bold;">Page {CurrPageNum} of {TotalPageCount}</span>
7
8 <div scope="NextEnabled" style="display:inline">
9 <a href="javascript:AspNetScopes.Action('NextPage', {NextPageIdx})">Next >></a>
10 </div>
11 <div scope="NextDisabled" style="display:inline">Next >></div>
This quite simple markup looks like the following in the browser window:
The template contains 4 scopes on the same nesting level: PrevDisabled, PrevEnabled, NextEnabled, and NextDisabled. The idea underlying this scope structure is simple. If "<< prev" button should be enabled, then show PrevEnabled and hide PrevDisabled; otherwise, show PrevDisabled and hide PrevEnabled. Same logic for "next >>" button.
You might notice interesting JavaScript calls to AspNetScopes.Action()
. This API is used on a client side to raise async actions processed inside the controller class. In the next sections we will have a detailed discussion on actions in SF.
The controller for the template above, as we already know, is PagerControl
class. The class is in the PagerControl.cs file and the complete listing of this file is below:
Listing 7: ../App_Code/Controls/PagerControl.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Web;
5 using System.IO;
6 using System.Web.Hosting;
7
8 using AspNetScopes.Framework;
9
10 11 12 13 public class PagerControl : ScopeControl
14 {
15 public override void SetTemplate(ControlTemplate template)
16 {
17 template.Markup = File.ReadAllText(HostingEnvironment.MapPath("~/App_Data/Templates/Pager.htm"));
18 }
19
20 public override void SetupModel(ControlModel model)
21 {
22 model.SetDataBind(new DataBindHandler(ROOT_DataBind));
23 model.Select("PrevEnabled").SetDataBind(new DataBindHandler(PrevEnabled_DataBind));
24 model.Select("NextEnabled").SetDataBind(new DataBindHandler(NextEnabled_DataBind));
25
26 model.HandleAction("NextPage", new ActionHandler(Action_NextPage));
27 model.HandleAction("PrevPage", new ActionHandler(Action_PrevPage));
28 }
29
30
31
32 private void Action_NextPage(ActionArgs args)
33 {
34 int startItemIdx = Context.Params.GetInt("StartItemIdx");
35 int pageSize = Context.Params.GetInt("PageSize");
36 int itemTotalCount = Context.Params.GetInt("ItemTotalCount");
37
38 if (startItemIdx + pageSize <= itemTotalCount)
39 {
40 startItemIdx = startItemIdx + pageSize;
41 Context.Params["StartItemIdx"] = startItemIdx.ToString();
42
43 Scopes.NotifyAction("NextPage", null);
44 }
45 }
46
47 private void Action_PrevPage(ActionArgs args)
48 {
49 int startItemIdx = Context.Params.GetInt("StartItemIdx");
50 int pageSize = Context.Params.GetInt("PageSize");
51 int itemTotalCount = Context.Params.GetInt("ItemTotalCount");
52
53 if (startItemIdx > 0)
54 {
55 startItemIdx = startItemIdx - pageSize;
56 Context.Params["StartItemIdx"] = startItemIdx.ToString();
57
58 Scopes.NotifyAction("PrevPage", null);
59 }
60 }
61
62
63
64 private void ROOT_DataBind(DataBindArgs args)
65 {
66 int startItemIdx = Scopes.CurrentPath.Context.Params.GetInt("StartItemIdx");
67 int pageSize = Scopes.CurrentPath.Context.Params.GetInt("PageSize");
68 int itemCount = Scopes.CurrentPath.Context.Params.GetInt("ItemTotalCount");
69
70 int currPage = startItemIdx / pageSize + (startItemIdx % pageSize == 0 ? 0 : 1); ;
71 int pageCount = itemCount / pageSize + (itemCount % pageSize == 0 ? 0 : 1);
72
73 args.NewItemBinding();
74 args.CurrBinder.Replace("{PrevPageIdx}", currPage > 0 ? currPage - 1 : 0);
75 args.CurrBinder.Replace("{NextPageIdx}", currPage + 1);
76 args.CurrBinder.Replace("{CurrPageNum}", currPage + 1);
77 args.CurrBinder.Replace("{TotalPageCount}", pageCount);
78
79 Scopes.CurrentPath.Fwd("PrevEnabled").Context.Params["PrevPageIdx"] = (currPage > 0 ? currPage - 1 : 0).ToString();
80 Scopes.CurrentPath.Fwd("NextEnabled").Context.Params["NextPageIdx"] = (currPage + 1).ToString();
81
82
83 if (currPage > 0)
84 {
85 Scopes.CurrentPath.Fwd("PrevEnabled").Context.IsVisible = true;
86 Scopes.CurrentPath.Fwd("PrevDisabled").Context.IsVisible = false;
87 }
88 else
89 {
90 Scopes.CurrentPath.Fwd("PrevEnabled").Context.IsVisible = false;
91 Scopes.CurrentPath.Fwd("PrevDisabled").Context.IsVisible = true;
92 }
93
94 if (currPage < pageCount - 1)
95 {
96 Scopes.CurrentPath.Fwd("NextEnabled").Context.IsVisible = true;
97 Scopes.CurrentPath.Fwd("NextDisabled").Context.IsVisible = false;
98 }
99 else
100 {
101 Scopes.CurrentPath.Fwd("NextEnabled").Context.IsVisible = false;
102 Scopes.CurrentPath.Fwd("NextDisabled").Context.IsVisible = true;
103 }
104 }
105
106 private void PrevEnabled_DataBind(DataBindArgs args)
107 {
108 args.NewItemBinding();
109 args.CurrBinder.Replace(Scopes.CurrentPath.Context.Params);
110 }
111
112 private void NextEnabled_DataBind(DataBindArgs args)
113 {
114 args.NewItemBinding();
115 args.CurrBinder.Replace(Scopes.CurrentPath.Context.Params);
116 }
117 }
Same as PageStudents
, PagerControl
controller implements abstract methods and contains action and data binding handlers inside the body of the class. In SetTemplate()
on line 15 we associate the controller with Pager.htm template. In SetupModel()
on line 20 we attach a number of data binding handlers and add action handlers. Note that this time we attach data binding handler to the controller root scope on line 22.
Let's now look at the binding handlers in PagerControl
. You should recall from the previous discussion on PageStudents
controller that Pager is the scope that should be databound after CourseRepeater, but since Pager scope has a PagerControl
controller assigned to it, all binding handlers are called inside this controller class rather than PageStudents
class. Let's now look at the PagerControl
class to uncover how Pager scope and its children are databound.
So, after CourseRepeater_DataBind()
is invoked in PageStudents
controller (see listing 5, line 120), the control is transferred to PagerControl
controller, and the first handler invoked here is, obviously, a data binding handler for a controller root scope which is the second Pager scope in the tree on Fig. 8. The root scope in PageControl
has ROOT_DataBind()
handler attached to it on line 22 of listing 7. So, ROOT_DataBind()
gets called (line 64). Let's assume that we're already on a rendering iteration for the 3rd student. Then the current scope path is "0-GridArea$0-StudentRepeater$2-Schedule$0-Pager". On lines 66-68 we get pager values from root scope context. How did these parameters get in there? Recall lines 111-113 of Profile_DataBind()
method in PageStudents
class where the pager has been initialized and some parameters were added to the Pager scope. These are the values that we retrieve on lines 66-68 in PagerControl
class! So, what we did in PageStudents
controller, we actually passed the parameters to its child PagerControl
controller by adding parameters to Pager scope to which PagerControl
is assigned. Also recall that each controller has a context which is the same as context of its root scope. This means that we also could retrieve pager values using Context
property instead of Scopes.CurrentPath.Context
. Next, on lines 70-71 we calculate more pager values. On line 73 method is called to repeat pager content once and lines 74-77 bind actual values to placeholders in the template. On line 79 we add "PrevPageIdx" param to PrevEnabled scope in order to use it later when PrevEnabled scope is databound. Similar thing on line 79 for NextEnabled scope. Lines 83-92 are needed to enable or disable "<< prev" button. As mentioned before, when page is not the first one, then PrevEnabled scope is displayed and PrevDisabled scope is hidden; otherwise, vice versa, PrevEnabled is hidden, and PrevDisabled is displayed. To hide or display a scope Context.IsVisible
property is used.
Since PrevDisabled scope just contains some static text and is not databound, the next binding handler to be invoked is PrevEnabled_DataBind()
on line 106. Current path is "0-GridArea$0-StudentRepeater$2-Schedule$0-Pager$0-PrevEnabled". Everything is quite simple here. Repeat content once (line 108) and replace placeholder from params of the current scope which were populated on line 79 during execution of previous ROOT_DataBind()
handler. And the last hander to be invoked in PagerControl
class is NextEnabled_DataBind()
on line 112 which is totally analogical to the previous PrevEnabled_DataBind()
hander.
Now when all handlers of PagerControl
controller are finished, the scope sub-tree starting from second Pager node on Fig. 8 is completely rendered, and the control is transferred back to the parent PageStudents
controller which proceeds by calling data binding hander for Summary scope in PageStudents
controller.
We're done with data binding handlers and we are ready to proceed to one of the most exiting features of SF which is the ability to process asynchronous actions and do partial page updates.
6. SF Actions
6.1. Actions design
Just like pages and user controls in ASP.NET Forms can raise and handle events, the client page in SF can raise actions handled on the server side inside the corresponding controller class. In the current implementation of the framework it is required to have ScriptManager
control on the page and all of the postbacks initiated by actions are async postbacks. I was lazy to implement full postback for an Alpha version, because it's trivial; however, I implemented the most exciting part – the async actions with partial AJAX-like pager updates!
First, to help you better understand actions in SF, let's recall how events work in ASP.NET Forms. Here is a top-level activity flow occurring each time the event is raised and handed in standard ASP.NET site:
- End user acts on the page by, for example, clicking some button.
- Button click starts form postback or invokes
__doPostBack()
JavaScript function. If postback is initiated from update panel, then __doPostBack()
is overridden by Ajax Library version of this function that simulates form postback using XMLHttpRequest
instead of doing actual postback.
__doPostBack()
takes the corresponding control client ID as a parameter which is transferred to the server-side in __EVENTTARGET
hidden field.
- Using client ID, the control is found on the page, and if the control has a handler for our event, then it is invoked.
- Other page logic is executed.
- Page is rendered and sent back to the client. If postback is async, then resulting page is not sent as whole. Instead, special segmented response is constructed containing only parts of the page to refresh, view state, scripts to register, etc. etc.
- This special async response is parsed by MS Ajax Library and updated content is inserted into the appropriate update panels on the page which are simply
DIV
tags with known IDs. Done!
The design to rise and process actions in SF is simple and overall flow closely resembles the one for ASP.NET Forms. The following is the top-level activity flow occurring each time the action is raised and handled:
- End user acts on the page by, for example, clicking some button.
- Button click invokes a special JavaScript function
AspNetScopes.Action()
which is a part of a client-side SF implementation.
- Params of this function are the path of the scope where the function is located, action name, and action argument. An async postback is initiated and function params are transferred to the server-side.
- SF core intercepts the postback. Using scope path it locates the scope that was a source of the action in the rendered scope tree.
- The controller responsible for rendering action source scope is used to process the action. The system checks if the controller has an action handler added for the action name passed.
- If action hander is found, it is invoked. Inside action hander implementation, the developer manipulates scope parameters and explicitly specifies which scopes of a rendered scope tree have to be refreshed to reflect changes caused by processed action.
- Scope tree is rendered starting from refreshed scopes, not from the
NULL
scope. The result, a set of rendered tree branches, is sent back to the client.
- On the client, the updated branches are applied to the appropriate scopes which are also
DIV
tags with known IDs. Done! Our action is processed and our page is updated using async postback!
You see obvious similarities of how events and actions and the rendering results are processed on the client side. In fact, client-side of SF is totally based on a standard MS Ajax Library, allowing us to have additional functionalities of SF keeping all of the beautiful features of the standard MS Ajax Library in the same time.
6.2. SF client side
SF client side library implementation in Alpha version is quick-and-dirty, but it defines the implementation path I will follow in the future for the release version of the SF. The fact that I was able to elegantly build some new logic on the existing MS Ajax implementation actually saved me tons of time!
If you look at the source of any SF page in the browser you'll see a ScriptResourse.axd that loads the AspNetScopes.js file from the AspNetScopes.dll assembly. This file contains a client-side logic that allows SF to raise actions and do partial page updates. The developer only needs to know that there is AspNetScopes
class with a couple of static methods that do all work on the client-side. There is AspNetScopes.Install()
function which is called by the system to do necessary client-side preparations. It is invoked together with Sys.Application.initialize()
function from MS Ajax Library on start-up and is not intended to be used directly by the developer. The developer needs to know about the following 2 functions:
AspNetScopes.Action(scopePath, actionName, actionArgument)
This function is needed for the same purpose the __doPostBack()
is needed in a standard Ajax Library i.e. it initiates an async postback to the server. If you look at the code which is really just a couple of strings of JavaScript code, you'll actually see that our function calls __doPostBack()
after saving scope path, action name and argument to the hidden fields. The developer needs to use this function to raise any action.
AspNetScopes.AddRefreshHander(scopePath, callback)
This is a helper function that you don't have to use. Sometimes it is necessary to execute some javascirpt when the specific scope is refreshed. This is exactly what this function is needed for. It takes scope path and a callback. When scope is refreshed, the callback is executed on the client side. I'll use it in the example with the popup window in the end of this article to perform the delayed loading effect.
6.3. Rising and handling HTML template actions
To rise an action on the client side, the developer has to use AspNetScopes.Action()
function inside the HTML template. This function has 3 parameters, but inside template, the developer must specify only two: actionName
and actionArg
. While scope tree is rendered, the scopePath
parameter is inserted in the resulting markup automatically depending on which scope contains the AspNetScopes.Action()
call.
When action occurs and appropriate controller is selected by the system for processing, SF searches action handler to be invoked for specified action name. Action handlers are added, as you might have already guessed, in the controller class inside SetupModel()
method. The developer has has to use the following expression to handle an action:
model.HandleAction(<action_name>, <delegate>)
Inside action handlers we can manipulate scopes and their parameters just the same way we did inside data binding handlers. Recall that inside binding handler the Scopes.CurrentPath
property always pointed to the scope currently being databound. Inside action handler the Scopes.CurrentPath
property has a different meaning and always points to the root scope of the current controller. Another property becomes available inside action handlers, the Scopes.ActionPathPath
, which always points to the scope from which the action is originated. Note that this property cannot be used inside data binding handlers.
NOTE: In the future implementation I plan to change the design a little bit. We will have Scopes.ControlPath
always pointing to the root scope of the controller and Scopes.CurrentPath
pointing to the currently bound scope inside data binding handler, or to the action source scope inside action handler. This is a more consistent design then it is right now.
6.4. Rising and handling child controller actions
Besides actions raised on the client side using AspNetScopes.Action()
, actions can also be raised by child controller classes and handled inside parent controllers. It is a very typical situation when, for example, pager controller handles "NextPage" action and updates its values, but it also needs to notify the parent controller about the action occurred, so that parent controller can switch to the next page of data.
To rise action inside the controller class, the developer can call Scopes.NotifyAction()
method passing the name of the action and the argument. Note that this time argument can be any object, not just the string, as it is for actions raised from the client side.
Action handlers are added in the parent controller class inside SetupModel()
method. To add the handler, the developer must use the following expression:
model.Select(<path>).HandleAction(<delegate>)
The difference between handling the controller and HTML template actions is that for controller actions before calling HandleAction()
with action name and handler function pointer, the actual child controller has to be selected by pointing at the scope which this controller is assigned to.
6.5. Refreshing scopes and AJAX
One of the most exciting features of the SF is how AJAX-like partial updates are implemented. Every typical Web 2.0 page works the same way – page is rendered for the first time, every consequent user action on this page results in update of the smaller part of the page. In ASP.NET Forms this behaviour was achieved by update panels whose main disadvantage is that async postback is actually a postback and the page still goes through the entire lifecycle executing tons of unnecessary code even though only a tiny part of page has to be updated.
In SF this is implemented differently. You might have already guessed that every scope in the rendered scope tree is an update panel! No, technically it's not an update panel, of course, but logically yes, every scope in the rendered scope tree can be refreshed independently on async postback. If the scope is refreshed, only the tree branch starting from this scope is rendered meaning that system calls data binding handlers only for scopes in the rendering branch! Is not this beautiful? Not a single line of code is executed on a postback without your explicit instruction. This process is very lightweight from performance point of view comparing to rendering of the entire page and extracting updated part from it in ASP.NET Forms.
First of all, why do I always talk about rendered scope tree on a postback? Should not I talk only about model scope tree until the rendering process starts? Just recall the rendering process execution steps from section 5.6. The entire page is rendered only on initial request, after which our model tree becomes a rendered scope tree and all our scope contexts are persisted. On a subsequent request to this page, the persisted contexts are retrieved. These contexts strictly define the structure of the previous rendered scope tree. So, logically we can always restore the rendered scope tree resulted from the previous output.
Ok, back to partial updates. Obviously, partial update makes sense only on a postback i.e. as a result of processing some action. So, action occurs on the page, postback starts, action handler called, some code is executed, and some scope DIV
tags are updated.
The system must be explicitly told which scope to refresh. By default none of the scopes is refreshed on a postback. To tell the SF to refresh the scope, the developer has to point at the specific scope using tree navigation and call Refresh()
function on its context (see Fig. 15). It is important to understand that scopes can be refreshed only inside action handlers. It's not valid to call Context.Refresh()
inside data binding handlers, because the system obviously has to know about refreshed scopes before rendering process is started.
When scope is refreshed, the rendered scope sub-tree starting at this scope is discarded together with all scope contexts from this sub-tree. Then during rendering process, the rendered scope sub-tree is rebuilt starting from the refreshed scope i.e. all data binding handlers are called in appropriate controllers for all of the scopes in a scope tree branch starting from the refreshed scope. Binding handlers for scopes outside of the refreshed subtrees are not invoked. Such approach simplifies the back-end logic dramatically. I'll not even mention design benefits here – they are obvious for every ASP.NET developer who tried to build more or less sophisticated Web 2.0 UI based on UpdatePanel controls in ASP.NET Forms.
6.6. Practice: PagerControl actions on client side
Back to our PagerControl
. The PagerControl
supports two actions: "NextPage" and "PrevPage". When end user clicks "next >>" button (see Fig. 1), "NextPage" action is raised. When user clicks "<< prev" button, "PrevPage" action is raised. Further in this example I'll talk about "NextPage" action only, because "PrevPage" action implementation is totally analogical.
Look at Pager.htm on listing 6. Let's investigate NextEnabled scope and "next >>" button inside it. The "next >>" hyperlink has its href
property set to AspNetScopes.Action()
javascript meaning that this function is called when "next >>" button is clicked. Note that we have to pass only 2, not 3 parameters: action name and any action argument. On rendering one more param representing the scope path is inserted as first argument (ref section 6.3). For example, for the 3rd student the resulting call in rendered HTML would be the following:
<a href="javascript:AspNetScopes.Action('0-GridArea$0-StudentRepeater$2-Schedule$0-Pager$0-NextEnabled', 'NextPage', 1)">Next >></a>
Client representation of the full scope path to NextEnabled scope is inserted as a parameter. The system uses it to find the appropriate handler for the "NextPage" event.
Look how we use a {NextPageIdx} placeholder for action argument. For NextEnabled scope this placeholder is bound on line 115 of listing 7 using the params of the NextEnabled scope, previously populated on line 80 of listing 7 by NextPageIdx
value. This means that {NextPageIdx} placehoder is always replaced with the appropriate next page index which is later transferred to server side as action argument when action is raised.
6.7. Practice: PagerControl actions on server side
In order to handle actions inside PagerControl
on listing 7, action handlers need to be added inside SetupModel()
method. On line 26 we attach handler for "NextPage" action , and on line 27 we do the same for "PrevPage" action.
When "NextPage" action is raised, the async postback carries all action info to the server and the attached to "NextPage" action Action_NextPage()
handler is called. On lines 34-36 we, first of all, retrieve all current pager values. Note that we use controller context for this purpose, but we could also use Scopes.CurrentPath.Context
which returns a context of the controller root scope when called inside action handler. On line 38 we make sure that next page actually exists. This check is dummy and can be omitted, because we have some logic in data binding handler for the controller root scope (lines 94-103) that hides the clickable "next >>" button if there is no next page. Starting from line 40 we do actual modification of the pager parameter. On line 40 we calculate the starting index of the item on the next page. Then on line 41 we update the StartItemIdx
parameter by newly calculated value. Finally, on line 43 we trigger "NextPage" controller action passing NULL
argument. Note that we use "NextPage" name for both client side and controller actions, and this is just my preference, not a requirement. Also note that we do not refresh any scope here, although the parameters were updated. I decided to leave the refreshing task for the parent PageStudents
controller that intercepts the action. When "PrevPage" action is raised, then Action_PrevPage()
handler is called. All logic here is similar to Action_NextPage()
except that all our calculations are made for previous page instead. After parameters are updated, "PrevPage" controller action is raised with NULL
argument.
In reality instead of rising two different controller actions "NextPage" and "PrevPage", you'd probably want to raise one "PageChanged" action passing some parameter indicating direction of paging. We used to this behaviour working with numerous ASP.NET Forms controls. But for our current example I decided to have two separate controller actions.
6.8. Practice: Handling PagerControl actions inside PageStudents
The last step to complete main part of our Students Application is to handle controller actions raised by PagerControl
inside parent PageStudents
controller. To achieve this, the first step is to add action handlers inside SetupModel()
method of PageStudents
class on listing 5. Recall that we have two different Pager scopes in the model scope tree: one is for paging students and second one is for paging student courses (see Fig. 8). Line 33 handles "NextPage" action of the PagerControl
assigned to the first Pager scope. To point at this scope, model scope path is passed to the Scopes.Select()
function. Line 34 handles "PrevPage" action of the same controller. Lines 36-37 analogically handle "NextPage" and "PrevPage" actions of the PagerControl
assigned to the second Pager.
So, when "next >>" button is clicked under students grid, "NextPage" action is raised on a client side, and then "NextPage" action is re-raised by PagerControl
. The Pager1_NextPage()
handler added on line 33 of listing 5 is executed. You see that this handler just have two lines of code. On line 46 we navigate to StudentRepeater scope and call Refresh()
on it. This causes the system to re-render the rendered scope tree starting from StudentRepeater node and update the corresponding scope DIV
on a page in the client browser. During the rendering process, new Pager values are retrieved, as they were updated in Action_NextPage()
handler of PagerControl
on line 41 of listing 7. So the new set of student objects is retrieved in StudentRepeater_DataBind()
binding handler of PageStudents
controller (line 95 of listing 5) and our output lists different students. But we're not done yet. Recall that we did not refresh anything inside the PagerControl
itself, so we should do this here. Line 47 refreshes the Pager scope causing another rendered scope sub-tree starting from Pager scope to re-render and update its corresponding DIV
on the page.
When "<< prev" button is clicked under students grid, then Pager1_PrevPage()
handler on line 50 is executed. The code inside the handler is identical to the one inside Pager1_NextPage()
, because we need to do exactly the same thing – refresh StudentRepeater and Pager scopes.
When "next >>" or "<< prev" button is clicked under student course grid, then Pager2_NextPage()
or Pager2_PrevPage()
(lines 56 or 63 in listing 7) is executed. Everything here is the same, except that Summary scope is also impacted by paging and needs to be refreshed, so we call additional Refresh()
on Summary scope to re-render it and update its content on the page.
7. Dialog Windows
Now it's time to go over the popup windows and how they are done in Students Application. We are not going to learn anything new here; I just want to demonstrate how easy it is to achieve some advanced features of Web 2.0 UI using SF. One of the most popular elements of Web 2.0 nowadays is displaying info in the popup windows with delayed loading effect. To do popups we usually have to use 3rd party scripts which are available in hundreds of variations on the WWW. I always preferred jQuery and its plugins such as jqModal where we simply have to create a dialog container and call the jqModal initialization method on the jQuery wrapper object for the selected container. Then dialog can be displayed or hidden using show()
and hide()
methods provided by the plugin.
The delayed loading effect is useful from user experience point of view, because the dialog UI does not have to wait till the data is fully loaded. The dialog window can be displayed immediately showing some beautiful banner or AJAX loader icon with "please wait ..." message. Then the data gets loaded and the dialog is updated using async postback, so the whole process is totally seamless for the end user.
As I already mentioned in section 1, Students Application displays two dialog windows which look pretty much the same, but their underlying implementation is different. Buttons "Popup 1" and "Popup 2" are located under each student profile (see Fig. 1) and clicking these buttons invokes the dialog with all information for the current student similar to the information displayed in a student record (see Fig. 2). I'll not go deeply into the code, because this article is getting too long, so let me just give a brief design overview. All source code is available, so with your SF knowledge so far you'll be able to browse the project and understand how these popups are implemented.
7.1. The "Popup 1" dialog window
Both popup windows are implemented as child controllers. First controller is PopupControl
located in ../App_Code/Controls/PopupControl.cs file. The HTML template associated with it is in ../App_Data/Templates/Popup.htm file. In PageStudents
controller on listing 5 we assigned the PopupControl
controller to the PopupPlaceholder scope in our model tree on Fig. 8.
Now, look at the Popup.htm markup. There is a container DialogWindow scope having two child scopes: ContentView and LoaderView. LoaderView has some markup to show "please wait ..." message. ContentView consists of 4 child scopes: Profile, Summary, CourseRepeater and Pager scopes. The idea is to show LoaderView immediately after the dialog is displayed. And when data is loaded from data layer, hide LoaderView and show ContentView scope.
Look at the ROOT_DataBind()
binding handler in PopupControl
controller at line 73. On lines 75 and 76 we assign showDialog
and loadedState
variables from controller root scope context. On the initial load these values are not set, so the default values are returned and both variables are set to "0"
. When loadedState is "0"
, the ContentView is made invisible on line 78. I.e. the ContentView scope is always invisible on the initial load. When showDialog
is "0"
, the container DialogWindow scope is made invisible on line 85. I.e. dialog is invisible on the initial load. Does the rendering proceed further to call binding handlers for ContentView, Profile, etc.? No, it does not! Recall that if the scope is made invisible, there is no need to render this scope and its contents i.e. binding handlers for this scope and for its child scopes are ignored by the system. Is this good? Of course, it is, because unnecessary code does not get executed, since rendering results would be hidden anyway!
Now look at StudentsPage.htm template on listing 4 at line 50. When user clicks "Popup 1" button under the student profile, the "OpenPopup1" action is raised in PageStudents
controller with an argument equal to current student SSN. We use a {StudentSSN} placeholder to insert the correct SSN into the AspNetScopes.Action()
JavaScript call. In PageStudents
controller on listing 5 at line 39 we attached Action_OpenPopup1()
handler to "OpenPopup1" action, so this handler gets executed on line 70. Now look what we do in this handler. We take the student SSN passed in args.ActionData
and add this value to the context of PopupPlaceholder scope on line 72. Remember that this scope has our PopupControl
controller assigned to it meaning that we actually pass "StudentSSN"
parameter to the controller. On line 74 we set another parameter "ShowDialog"
to "1"
, so we basically tell the controller to display the dialog. And finally, we must refresh the PopupPlaceholder to make the SF re-render the sub-tree starting from this scope.
Next, the tree is re-rendered starting from PopupPlaceholder scope. This means, that ROOT_DataBind()
in PopupController
at 73 is called again. But this time showDialog
is assigned "1"
instead of a "0"
, because we just set this value in PageStudents
controller at line 74. This means that this time the container DialogWindow scope is made visible on line 86. Variable loadedState
is still "0"
so the ContentView scope is still invisible and LoaderView scope is visible. Fine, so postback returns and what do we see on the screen? Well, since our DialogWindow is visible now and LoaderView is visible inside it, we see the popup window with an AJAX loader icon and "please wait ..." message! How does this work? Look at the Popup.htm template. Inside DialogWindow container scope it has a centered DIV
with fixed positioning and z-index
set to 3000 so that DIV
is displayed on the top of all other layers. We also have a DIV
for screen overlay taking the entire size of the current browser window providing the grayed out background for the modal window effect. So, when DialogWindow is made visible, its content is displayed so the centered DIV
styled to look like a modal dialog window.
Next, the content is not loaded yet. All you see is a "please wait ..." message for now. The beauty of the delayed loading is that you did not wait to get the popup at all and the meaningful content is going to be loaded in a second. Now look at the tiny JavaScript in the end of Popup.htm template file. Remember I said that "Popup 1" dialog is written without a single line of JavaScript? I meant that its popping up functionality does not require JavaScript, but we actually need a bit of JavaScript here just to accomplish delayed loading. If there were no delayed loading, there would be no need for any client script. So, what that tiny script does, it uses AspNetScopes.AddRefreshHander()
(ref section 6.2) to set the callback invoked when scope specified by {CurrScopeID} placeholder is refreshed. Now look at the PopupControl
at line 89 to see that this {CurrScopeID} is actually replaced by the ID of the root scope of PopupControl
controller which is a PopupPlaceholder scope. So, since our PopupPlaceholder is refreshed, the client callback is invoked. It checks, if there is a LoaderView scope available, and if it's there, it sets the timeout to raise the "DelayedLoad" action. The LoaderView is there, so the action is raised and is handled in PopupControl
by Action_DelayedLoad()
handler attached at line 38.
Inside Action_DelayedLoad()
handler we simply set "LoadedState"
param of the root scope to "1"
and refresh controller root PopupPlaceholder scope once again. So the ROOT_DataBind()
is called again, but this time both showDialog
and loadedState
are set to "1"
, meaning that DialogWindow container scope is visible together with the ContentView, but LoaderView is hidden. Note that Context.IsVisible
is persisted together with all scope parameters and is cleared when the scope or its ancestor scopes get refreshed, so we don't have to set Context.IsVisible
to TRUE
explicitly. Now, since our ContentView is visible, its data binding handler and data binding handlers for its child scopes are executed in the regular order one after another on lines 92-132. I'll not go through any details here. The data binding handlers are similar to the ones that we had in PageStudents
controller on listing 5. The only thing to mention is the Thread.Sleep()
call on line 95 inside ContentView_DataBind() handler of PopupControl
controller. This Sleep()
simulates the delay that you would normally get requesting data from the database. The delay is 2 seconds meaning that the LoaderView scope with "please wait ..." message is displayed for 2 seconds before it is replaced with the ContentView scope. Note that LoaderView is not displayed now, so our callback in the script at the end of Popup.htm template will not find the LoaderView and, therefore, will not raise the "DelayedLoad" event anymore until the LoaderView is displayed again.
Now everything is loaded and we can see our beautiful dialog. The only thing left is how to close this dialog window. Look at the Popup.htm again and see that when close link is clicked, the "ClosePopup" action is rased causing the system to execute Action_ClosePopup()
handler attached on line 37 of PopupControl
controller. Inside this handler we simply reset the "ShowDialog"
parameter to "0"
and refresh the root scope. So, the ROOT_DataBind() is called again. The showDialog
is "0"
, because we just set it to "0"
, so the DialogWindow container scope is hidden again. The loadedState
is also "0"
, because it was reset on line 81, so next time the dialog is displayed, it will start from the LoaderView screen. Since DialogWindow is invisible again, then when the postback finishes and scope DIV
tags are updated, the dialog disappears from the screen and we see our students grid again. So we came to our initial state meaning that we are done!
I personally think that it's pretty cool to have a dialog window implemented on a back-end without using any 3rd party scripts, because the structure of the code is extremely simple. Of course, the back-end approach lacks some fancy dialog window effects like transitions or fading, because the scope can be ether hidden or visible and nothing else. In most of the cases I'd give a preference to the back-end approach with delayed loader that we've just seen in action, but sometimes I still would use the standard approach with the 3rd party plugins. The "Popup 2" dialog is the example of using the 3rd partly approach mixed with SF.
7.2. The "Popup 2" dialog window
Second "Popup 2" window is also implemented as controller. The controller class Popup2Control
is located in ../App_Code/Controls/Popup2Control.cs file and has the HTML template in ../App_Data/Templates/Popup2.htm file associated with it. On listing 5 we assigned the Popup2Control
controller to the Popup2Placeholder scope in model tree on Fig. 8.
This controller is implemented in a quick-and-dirty way just to demonstrate the ability to mix SF with the 3rd party JavaScript libraries same way we did on a regular ASP.NET pages. I'll not discuss the controller in details, and will highlight some important moments only. "Popup 2" still uses delayed loading approach. Data binding handlers for its ContentView, Profile, Summary, and CourseRepeater scopes are identical to the ones of the previous PopupControl
. The difference is that we don't have the DialogWindow container scope anymore – we simply don't need it for the current dialog implementation. Note that "Popup 2" button in PageStudents.htm on listing 4 at line 82 has the "show-modal-popup2"
CSS class and if you look at the JavaScript in the end of Popup2.htm template, you'll see that this class is used to attach the client-side click callback using jQuery selector. The "Popup 2" button also has a studentSSN
attribute whose value is a placeholder substituted by the actual student SSN. This SSN is then retrieved by the script in Popup2.htm on line 66 when user clicks the "Popup 2" button.
Everything else is trivial. Button clicked, client callback script is executed, SSN retrieved, dialog window is displayed using jqModal on lines 71-73 of Popup2.htm, and the "DelayedLoad" action is raised on line 75. Recall that in the server-side popup implementation we used an additional postback to display the dialog, here we don't need a postback and display the dialog right away using the script. Next, the "DelayedLoad" is handled, "DisplayStudentInfo"
value is set to "1"
, root scope is refreshed, and ContentView is set visible in the ROOT_DataBind()
handler. Note that we cannot hide the LoaderView, because if we do, then we don't have a loader screen to display next time the dialog is invoked. This was not a problem with the "Popup 1" dialog, because, as I already said, we had an additional postback there to show the dialog, but for "Popup 2" we don't do any postback before displaying the dialog. One more thing to mention is that the client click event must be rebound to the "Popup 2" button every time the main student grid switches to another page because DOM content is changed and client-side jQuery event bindings are dismissed. This is the reason why we have that {StudentRepeaterID} placeholder in AspNetScope.AddRefreshHandler()
function call. Also note that this placeholder is databound on line 79 of Popup2Control
, but the value for it is passed to the Poupu2Control
controller from PageStudents
controller on listing 5 at lines 103-104.
Ok, I think this is all about the Students Application. Now we know how each and every line of code in this application works. In the next final section I will sum up our discussion and will try to outline the future development plans for SF.
8. Resume
In the end of this article I'll like to sum up pros and cons of building the web applications on ASP.NET SF, share my thoughts about the future development for the framework, and answer some potential questions that you might have.
8.1. SF cons
Let's start from cons – there are not so many that I can think of really:
- You are probably going to miss your favourite Forms controls like
Calendar
, TreeView
, etc. To be able to use these in SF, the controls have to be re-implemented as SF controllers with templates. As an example, I'm going to implement the calendar controller using SF and talk about it in the next article and on the framework development site. Watch for SF updates.
- In the beginning it might be a little unusual to view pages as scope trees and use scope contexts to transfer parameters between data scopes and controllers.
I'm totally open to criticism. If you see any design flows or issues in SF, please let me know. We're also setting up the SF development site with public blog for all the discussions.
8.2. SF pros
For me, as for an architect working with ASP.NET applications and developers on everyday basis, the benefits of SF are obvious. There are many of them – I'll list only the main ones:
- Maximum possible separation between presentation and the back-end.
- W3C compliant HTML templates containing nothing but a valid HTML allowing unlimited GUI customization of the applications.
- Simple and transparent data binding logic allowing beautiful back-end code design. No page lifecycle. No need for conditional code execution on async postbacks.
- Scope tree based AJAX allowing unlimited partial update capabilities without any additional coding.
- Full control of the presentation. There are no black-box controls anymore. No more questions like "how can I make this label render as
DIV
instead of a SPAN
or vice versa". You control it now from the beginning to the end and no single byte is output to the end user without your explicit instruction.
- Full compatibility with the standard ASP.NET. You can have SF pages and Forms-based pages working together in one application.
8.3. Future development
As I mentioned already a couple of times, the current version of SF is Alpha 1 and it is only intended to prove the concept of server pages based on HTML templates composed from databound hierarchical scopes. This means that Alpha 1 implementation is missing many basic functionalities that must definitely be added in the future. The existing functionalities must be cleaned up from all quick-and-dirty code and must be finalized. Let's quickly discuss some parts that I'm missing and what else needs to be done. The following list summarized the future development plans:
- One thing which is definitely missing is the ability to access the
Page
object from the controller. The Page
contains lots of useful methods and properties that we might need during the controller operation such as methods for registering client scripts, resolving URLs, query string and form parameters, etc. I did not decide yet if it is better to give the access to the Page
directly or through some bridge class making only the certain methods available.
- Another missing part is ability to pass params to controllers from the markup. Just as we did in .aspx/.ascx pages specifying the control properties right in the markup. My current idea is to allow specifying any generic attributes except for
scope
attribute for data scope DIV
tags in HTML templates. These attributes will be converted into the scope parameters with the corresponding names and will be available through the scope context mechanism.
- What about getting rid of need for having the complete HTML page in the HTML template for the root controller? Well, it's actually even a bigger question. There is an idea to provide an additional
ScopeTree
control based on ScopesManagerControl
that can be inserted on a standard ASP.NET page in any number of instances. ScopeTree
will be rendered the same way as ScopesTreeManager
renders its output, but instead of inserting the results into the predefined literals in the <head>
and the <body>
of the page, the ScopeTree
will output the result as its own content. This is an extremely flexible solution that will allow the developer to decide if he wishes to use SF to render the entire page, or only the part of it.
- What about master pages? I did not test these guys with the current implementation, but I don't see any problem of putting the head and body content literals into the <head> and <body> content placeholders bound to a master page. Of course, with the current Alpha 1 version the master page must be implemented with standard ASP.NET Forms and the content page could have the
ScopesManagerControl
inside it. I.e. only the content page could be based on SF. But if we had a standalone ScopeTree
control that I just mentioned about, we could use this one directly on a master page, so that master page could be based on SF as well. So, there are several alternatives and at this point I'm still deciding about the final design.
- If you look at the HTML source of the SF page in the browser you'll notice that
__VIEWSTATE
field is huge. This is because scope contexts of the rendered scope tree are persisted using ViewState
and, obviously, I was lazy to implement context serialization for Alpha version meaning that the system just serializes the classes in a regular way saving all fully qualified class names wasting about 90% view state data. In the future version I'll come up with some smart context serializer. Also, I think it's a good idea to store all contexts in some other new hidden field instead of using ViewState
mechanism.
- In Alpha version the scope context object accepts only strings as parameters. In the future versions contexts will accept generic parameters marked with
[Serializable]
or [DataContract]
. Also, I think it's useful to separate permanent parameters from temporary ones. Permanent parameters will be persisted as it is now, and temporary ones will exist only during the single rendering process.
- You probably got a question about SF application localization. Well, the most trivial way is just to provide the appropriate content for the placehoders. This is the most flexible way and we can still use ASP.NET .resx files. I don't think we need anything else here, but I'm open for any suggestions.
- Huge improvement would be a VS designer support for model scope trees! Imagine that you build your page hierarchically as scope tree in the Visual Studio designer. Double click on the node generates the data binding handler. Etc. etc. Such add-on would save tons of time eliminating the need to sit with the pen and a paper planning the model tree structure.
- Another step forward is implementing the unit testing engine. Of course, the controller classes are fully testable on their own with the existing unit testing frameworks, no question about it. But in SF due to simplicity of HTML templates we could try to extend the testing engine to the real presentation layer! On some high-level pseudo language our sequence of activities in a single unit test could look like the following: show initial page → assert output → simulate some action from certain scope → assert output → simulate another action → assert output → and so on. I think you got an idea. We could even use skeleton HTML templates to test controllers together with the sample application GUI.
8.4. FAQ
Here are some answers for the questions that I keep getting from my friends about the SF.
- Is ASP.NET SF an open-source? It will definitely become an open-source starting from a first stable version which will be out in a couple of months.
- Where is the best place to discuss the SF? The development site is now set up and running at www.rgubarenko.net. It has a blog where you can post all your questions and criticism.
- Why the development goes so slow? Well, I'm trying to make it as fast as I can, but besides SF I have a small child and a primary full-time job :) So, be patient and wait for the 1st stable release of the framework.
- Will the concept be implemented on other languages? There is already group of volunteers to implement the concept on PHP, so I wish them good luck and we will probably have a PHP SF very soon. I'm not planning to do anything other than ASP.NET SF. But I'll make all codes, designs, docs and logic available for public before the end of this year, so you can go ahead and implement similar frameworks for other platforms.
Conclusion
Ok, I think it's enough for the 1st article. I hope you've enjoyed it as I enjoyed developing the idea of the scope tree based server pages and seeing it in action. This article is mostly not about the SF itself, but about the new development pattern and approach which is now starting to materialize in a form of a development framework for ASP.NET platform. I'm pretty sure the entire concept deserves attention and has a future in numerous implementations on different platforms.
I'm open for any type of suggestions and constructive criticism. I specified my contact e-mail so you can send your questions. I'll try my best to answer all of them, but be patient, because I have many other things to do. I'm currently working on the SF Beta release in which I'll include most of the features listed in a future development section above. I'm also creating a richer Web 2.0 demo app with some advanced functionalities, user input, lots of actions, and a beautiful calendar control. The discussion of some advanced features of this application will become a theme for my next article.
UPDATED (NOV 23, 2010): I opened a public discussion blog devoted to new web-development concepts and ASP.NET Scopes Framework based on them. If you have any questions or suggestions regarding SF, please visit my blog at www.rgubarenko.net