Free, Open-Source ASP.Net 2.0 Framework for Data-Driven Websites
Our philosophy has always been to create the simplest solution possible that will address the business objective at hand. We also focus heavily on human discoverability. These objectives manifest themselves at the page level in many ways.
First, we advocate leaving module-specific business logic in the pages unless that logic is needed in more than one place in the application. Abstracting logic into classes takes time, and it’s one more place a developer will have to go to figure out what the page is doing and what it needs to interact with. Only when you need the same logic in another page or as part of a process should you abstract it. You will find those situations are the exception, not the rule.
If you made it to this page before reviewing how we handle data access, know that business logic and data access logic are not the same things. Please review the Data Access Primer as well since connecting to a database should never happen directly from the page.
Second, we try to keep pages as simple as possible by asking each to only do one thing. This means you won’t see a detail panel share a file with an edit panel very often if ever.
Beyond those objectives, the most important thing for you to understand about pages is the page lifecycle and how our approach fits into it.
When we talk about the page lifecycle, we’re talking about two things: the logical lifecycle and the page-event lifecycle.
The Logical Lifecycle represents what the page needs to do and the order in which it needs to do it. The logical lifecycle is:
Logically, the first thing the page should do is gather any input and validate it. There’s no sense gathering data or building UI output until you know the page has the input required. For example, if a detail page requires an Id for the record to show and does not receive it, there’s no sense querying the database or building the UI. You just need to kick the user off the page and let them start over.
The next step is to apply any module-specific security since sometimes it is based on the input to the page. Once you know the input is valid and the user has the rights to be on the page, then it makes sense to tap the resources the page requires to do its work. Mostly this is in the form of database calls or accessing files, etc. Now that you have the input and resources required, you can move on to the logic of the page, and once the logic is carried out, you can build the UI.
This logical flow for a page hasn’t changed for us in the 15 years we’ve been developing. What has changed are the technologies we use and how this logical lifecycle fits into the technology’s native page-event and control-even lifecycles.
If you’re not familiar with the native page event handlers of .Net, please review this MSDN Page (http://msdn.microsoft.com/en-us/library/ms178472(VS.80).aspx). One of the challenges in .Net is matching the logical steps lifecycle with the native page and control event lifecycle.
This is a breakdown of the five common page patterns, the lifecycles, and some shortcuts we’ve found.
Search Pages
Search pages are generally the entry point to a module and are represented by the Default.aspx file. In our Chamber sample application, there is a Chamber/Default.aspx that allows the user to see a list of Chambers in the system, as well as a AppUser/Default.aspx, AppPermission/Default.aspx and AppGroup/Default.aspx allowing the user to see a list of Users, Permissions and Groups respectively.
The requirements for these pages are generally along the lines of:
The challenge for search pages that require both session storage of filters and paging/sorting in the grid is the page and control events and when each needs to fire. There are three event scenarios for this type of page:
This is the page-event and control-event sequence for the events we will use:
Scenario 1 is basic and is not a challenge.
Scenario 2 (button events) requires the Search or Clear button events to run before data is loaded from the database and bound to the grid. This means we cannot load data in Page_Load because the filter criteria may change.
Scenario 3 (grid events) requires data to be loaded from the database and bound to the grid before the event handlers have a chance to fire. This means we have to load data in Page_Load so the grid is loaded before the events fire.
Luckily, scenarios 2 and 3 cannot happen at the same time, but it makes the page a bit more complicated. We added a property to the UnifiedASP.Base.Page class named IsGridEvent. It looks at the __EVENTTARGET value in the post back, finds the specified control, and returns true if it is a GridView.
Page_Load then loads the data if it is a grid event.
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load If IsGridEvent = True Then DoLoadData() End If End Sub
Page_LoadComplete then loads data for all scenarios that are not grid events.
Protected Sub Page_LoadComplete(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.LoadComplete If IsGridEvent = False Then DoLoadData() End If End Sub
Page_PreRender handles the non-grid rendering of the User Interface.
Protected Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.PreRender
ToggleControl(ddlStateId, myState, "StateId", "StateAbbr", myState.HasRecords, True, False, myStateId, 150)
divTitleLink.Visible = myPermission.HasPermission("Chamber.Create")
Form.DefaultButton = btnSearch.UniqueID
txtCity.Focus()
End Sub
The Button event handlers do what they need to do and let Page_LoadComplete handle reloading data.
Protected Sub btnSearch_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnSearch.Click DoClearSessionValues() End Sub Protected Sub btnClear_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnClear.Click txtCity.Text = "" ddlStateId.ClearSelection() DoClearSessionValues() End Sub
And there are a couple of subs to abstract the reused code.
Private Sub DoClearSessionValues() SetSessionValue(txtCity.ID, Nothing) SetSessionValue(ddlStateId.ID, Nothing) End Sub Private Sub DoLoadData() myCity = GetInput(txtCity, True, Nothing) myStateId = GetInput(ddlStateId, True, Nothing) myState.LoadAll() myChamber.City = myCity myChamber.StateId = myStateId myChamber.LoadAll() grdChamber.DataSource = myChamber grdChamber.AllowPaging = True grdChamber.AllowSorting = True grdChamber.PageSize = 25 grdChamber.DataBind() End Sub
Search pages with sorting or paging and session-aware filters are by far the most complicated you’ll see with the CRUD pages. If you don’t need sorting or paging, just put everything in Page_LoadComplete. If you don’t need session-aware filters, just put everything in Page_Load.
Detail Pages
Detail pages are much simpler than Search Pages. Most require an ID as an input, need to pull a record and possibly some child records, and display them. There are rarely any challenges with page-event or control-event sequences.
We generally leverage Page_Load to gather input and load data, and Page_PreRender to build the user interface as is shown below.
Edit Pages
Like Detail pages, Edit pages are generally much simpler. We leverage Page_Load to gather input and load data, Page_PreRender to build the UI, and the button event handler to gather input and save the record to the data store. We’ll take it in a bit more detail.
Page_Load takes the ID and loads the corresponding record.
Page_PreRender shows controls for the fields of data that can be edited. It is very common that not all data can be edited.
When the user clicks the save button, Page_Load pulls the record again, the button event handler sets new values for those with controls on the page leaving the other Properties unchanged, then calls the Domain object’s Save method.
The button event handler then either redirects the user off the page or the Page_PreRender will show the UI again.
Remove Pages
Remove pages are basically a very scaled down Edit page. Most will cause the Remove method of the domain object instead of Save, and the _Remove stored procedure will either remove the row and its children, or hide them by setting a visible flag to false.
Page Helpers
There are a ton of methods you will find helpful for page work. We will document them in the weblog and tag them Pages.