Generating ASPX Page Layouts Automatically: The Code

Sep 6, 11:00 pm

Article Author: Jim Nesbitt
.NET 3.5 Books

Introduction


In Part 1 of this series I illustrated that most ASPX pages are essentially the same in many respects, apart from the details being displayed on the page. I analyzed an existing page layout and showed how it could be broken down into a series of generic texts that could be constructed in a find/replace operation. I also showed how the parameters for the find/replace process could be identified and how much of this information could be obtained from the database system tables. By supplementing this with some further information such as headings and sequencing, the potential is there to produce an ASP.NET page automatically.


With everything in place, all you need is a generator to bring everything together. That’s what I plan to cover in this article. Here in part 2, I will step through the process of consuming the metadata to generate the website. I will show how I go about automating the process and point out some standards I have used when developing these types of sites.


System Requirements


To work with the solution in the downloadable sample, you need the following:


  • Internet Information Sever

  • The .NET Framework version 1.1

  • VS.NET 2003

  • SQL Server with the Northwind database installed

  • Internet Explorer 5 or later

The Sample Code


The sample download for this article contains two VS.NET solutions:


  • MetaGen – This project contains the Order Info site. MetaGenAPP – This application is used to manage the metadata.

The solution displays details from the NorthWind database using the generated page layout.


There is also a SQL directory for creating database objects and a BCP directory used to load the Order Info metadata used in the sample project.


Full details on setup are contained in the Readme.txt file that is supplied with the download material.


Development Options


I looked at a number of options for the generation process. One option was generating the pages dynamically within VB.NET. My main concern here was potential complexity in tracking down any bugs that might occur in this process, as it’s much harder to debug using dynamically created objects as opposed to objects defined directly in the ASPX page code. I had also previously looked at template generators such as CodeSmith and the process I chose does have a lot of similarities. From my (admittedly) brief inspection of CodeSmith, it appeared that while it had the capability to read the information schema views and a very useful syntax for iterating through this data, reading the metadata tables might be more problematic.


Another possibility was to use XML and XSLT, but I decided on Transact SQL as I was more confident that I could overcome any problems using this approach. Because I can use a lot of the information from the database to start with, I also felt it was simpler to use a common technique.


I may have been unfair (or lacking in knowledge) to discount the other options in favour of Transact SQL. In one way, it’s irrelevant how the generator is built, the most important thing is that it meets its goal of automating the production of the ASP.NET pages. As it turned out, it worked for me, and it allowed me to achieve my goal, and using Transact SQL allowed me to implement some useful generator features that I’ll cover in this article.


If I was producing the pages manually with a text editor, I might copy from a similar page and use find/replace to create the output. The procedure for generating the ASPX code follows exactly the same process, but the copy/find/replace is all automated.


My standard page design makes extensive use of CSS to produce the page layout shown in the example and the page layout effectively consists of several major blocks. The structure of the generator mirrors the structure of the page. It consists of one main stored procedure that calls other procedures to process the major panels on the page. Since both the metadata and the generic code texts are in the database, it can use the two sets of information in order to perform the find/replace to create the actual output.


The full stored procedure code is contained in the file dnx_procs.sql . It occasionally uses some fairly simple views when retrieving the metadata. Views can be identified by the prefix v_ and the definition of the view can be found in dnx_views.sql .


Separation of Generic Texts from Processing


Before covering how the generator works, I ought to explain one point. I’ve already introduced the concept of generic texts that identified parameters to be replaced. The use of these texts and holding them in the database was key to the development of the generator.


When I first started out, I began by building up my output ASP.NET code by concatenating strings (essentially the same as lines of the generic texts) and combining these with attributes in a scripting process similar to building up an ASP page.


This approach was really error prone and difficult because of the double and single quotes required in the ASP.NET page code. The quote character to delimit literals in Transact SQL means that any similar quotes in the text had to appear twice so the compiler could recognize a single occurrence of a quote. Even when this was done correctly, it made the program code difficult to follow.


Continuing with this technique for my find/replace process was going to be very problematic but fortunately I hit upon a simpler alternative fairly quickly. Once the text was in a variable, it was no problem to manipulate, so if I stored the texts in the database, I could retrieve them directly into a variable and really simplify development.


Since I was already had an existing page layout to work from, I could take a specific example of an ASP.NET page code, make it generic using the caret notation, and set up the text directly by pasting it into the table. I hold the texts in a database table ( dnx_pagetext ) with a key field that identifies its purpose. In order to simplify the process of adding and maintaining the generic texts, I extended my maintenance program to maintain these texts.


Using this approach made a significant difference. It simplified the generator process considerably by removing the string building process and also simplified the creation and maintenance of the generic texts. Another big advantage was the ability to change the output without necessarily changing the generator.


Everything was now in place in the database to allow me to combine the metadata and the generic texts together to produce the ASPX page output. I just had to write the generator.


Generator Find/Replace Procedures


A key part of the processing was going to be executing the find/replace using the generic code texts and the replacement values from the page metadata. What I wanted was an easy way to instruct the generator to replace the generic values in the text with actual values from the metadata tables.


I’ve already shown the generic coding for a bound column back in the part 1 article.



    <asp:BoundColumn 
      DataField = "^COL_NAME"         
      HeaderText = "^COL_HEAD" 
      SortExpression = "^COL_NAME" 
      ItemStyle-HorizontalAlign ="^JUSTIFY"        
      DataFormatString ="^FORMAT"                 
      ItemStyle-Wrap="false" 
    />          


To create the actual output, I need to replace the generic values with the values in the column information. In order to get the text for the actual column, I use the following Transact SQL stored procedure call – p_dnx_code_gen :



execute p_dnx_code_gen
‘DataGridBoundCol’
‘^COL_NAME’ , col_name , '^COL_HEAD' , col_prompt
, ‘^JUSTIFY’ , justify_char , '^FORMAT' , format_char


This procedure retrieves the generic text and replaces the generic placeholder text on the left with the values in the parameters on the right hand side. I get the replacement values to use from the metadata.


The Core Find/Replace procedures


The generator process consists of a series of procedures that cursor through the pages metadata, calling a find/replace routine using the parameters from the metadata. The core of this process is the find/replace routine just described – this procedure can take up to 12 sets of find/replace parameters. It uses a helper routine that processes just a single find/replace at a time:



CREATE procedure p_dnx_changer
 text_id varchar (50)
,changer varchar(8000) OUTPUT
,@find1 varchar(500) = NULL
,@replace1 varchar(500) = NULL
AS
————————————————————————————————————
— do the find/replace if required.
————————————————————————————————————
if find1 is not null BEGIN SELECT changer = replace(changer, find1, replace1) END return GO <p></pre><!-- @END —>


This replaces any occurrences of find1</span> with <span class='codeintext'>replace1 in the text variable changer</span> and returns it as an output parameter. If the find string is <span class='codeintext'>null</span> , no replacement is done.</p> <p>The main find/replace routine uses this to do up to 12 find/replaces on the generic text (I've omitted some of these for brevity) and calls the routine <span class='codeintext'>p_dnx_changer</span> a dozen times. If I ever need to do more replacements, I can extend the routines, but a dozen seems to have been enough for everything I have done.</p> <p>The parameters are optional and have a default value of <span class='codeintext'>null</span> , so I only have to specify the actual number of find/replace pairs required when I call the routine - the base stored procedure will simply leave the generic text unchanged if the parameters are <span class='codeintext'>null</span> .</p> <!-- @START —>

CREATE procedure p_dnx_code_gen
  text_id varchar (50)
  ,find1    varchar(500) = NULL
  ,@replace1 varchar(500) = NULL
  ,@find2    varchar(500) = NULL
  ,@replace2 varchar(500) = NULL
  …
  ,@findc    varchar(500) = NULL
  ,@replacec varchar(500) = NULL
AS


The routine retrieves the generic text from the database using the text identifier key supplied by the calling procedure.




GET THE STANDARD CODE FROM THE DATABASE

SELECT changer = convert(varchar(8000),code_text) from dnx_pagetext where text_id = text_id


It then calls the helper routine to do a single find/replace 12 times for each set of find/replace parameters. If the find and replace strings are empty ( null ), then the text is left unchanged.



execute p_dnx_changer text_id ,changer OUTPUT, find1, replace1
execute p_dnx_changer text_id ,changer OUTPUT, find2, replace2

execute p_dnx_changer text_id ,changer OUTPUT, findc, replacec


Finally the procedure checks that there are no carets left in the string variable changer</span> . If there are, this indicates that a generic parameter has not been replaced with the actual parameter value from the metadata. If this occurs, it raises an error identifying that the code text has not been supplied all the required values. This helps to ensure that the generator processing is providing the required values and also helped during the development of the generator.</p> <a id="Heading8"></a><h2 class="redhead0">Returning the Output</h2> <p>I initially returned the output ASP.NET markup by selecting the string variable after the find/replace had been performed, so it appeared in the output from my query in a process very similar to streaming text using scripting. This proved a little inflexible, as it meant that the generator procedures had to mirror exactly the sequence of the output coding, and was a little restrictive. As a result, I decided to put the output text into the database as well. After all, everything else seemed to end up in the database, so why not this.</p> <p>Having made this decision, I needed to ensure that I could get the text back in the correct sequence.</p> <p>I extended the find/replace procedure above to insert the text into my generated text table ( <span class='codeintext'>dnx_generated_code</span> ). I also assigned a sequencing number to my generic texts corresponding to the sequence of the coding on the ASPX page. This also goes into the output table along with the generated text and ensures that I can retrieve, for example, the output for the datagrid header before the output for the datagrid columns. To retrieve the output text for datagrid columns in the required sequence, the output text table also has an identity column, whose sole purpose is to identify which texts were created first. The sequencing number from the code text together with this identity column allows the retrieval of the output code in the correct sequence. One reason for using both is that it allows me to do processing in the most convenient way, without the need for the generator to strictly follow the same sequence as the output coding.</p> <p>One of the benefits of using the standard find/replace routine is that implementing this was straightforward and had no impact on the higher level processing. I extended <span class='codeintext'>p_dnx_code_gen so that </span> once the edit is completed, the procedure outputs the edited text into the generated code table as follows.</p> <!-- @START —>


— Add it into the generated code table. We force the system to truncate
— the sequence value to two decimal places which allows us flexibility to
— get code aggregated together, and takes account of the sequence it was
— generated in, which is significant for ASPX
insert into dnx_generated_code
( text_id ,code_text ,text_seq
)
select text_id ,changer ,round(text_seq -.005 ,2) return GO <p></pre><!-- @END —>


The use of the round statement is a little refinement to give me additional flexibility in combining the code. When I add in validators to columns, I want to be able to retrieve the output in sequence of Column 1 , Validator 1 , Column 2 , Validator 2 .


In order to do this, I assign a sequence of 10.0000 to the column text, and 10.0010 to the validator text. This allows me to view the texts in a logical sequence. If I used the original text_seq field, I would get the code back in the sequence Column 1 , Column 2 , Validator 1 , Validator 2 which isn’t what I want.


The rounding ensures that the output text has the same text_seq value as the column text, so the identity column becomes a tie breaker and ensures that I can retrieve the output text in the right order for the ASP.NET page.


Once all the processing on the metadata is complete, I simply select the code_text column from this table and ensure that it comes back in the correct sequence by ordering it by text_seq and the identity column on this table.


I also have another very similar routine that allows me to combine texts together as a single string and assign an output sequence to it for additional flexibility. It’s a slight refinement on the rounding code described above. There is an 8K limit on the size of strings, but I haven’t found this a problem yet. There’s also some error checking code in the stored procedures, which I haven’t covered here. This code checks that the generic text exists, and that all generic variables (indicated by a caret) have been replaced by an actual replacement value


Processing the Metadata


The procedure to create the full ASPX page output reads the page information to start with and outputs the page and datagrid header information, and then it sets up a cursor to read the column information in the sequence they appear on the datagrid using the grid sequence number. For each column, it calls the stored procedure above with the appropriate code text key and the replacement values. To generate the ASP.NET output for the columns on the tabs, it cursors through the tabs, and then calls another procedure to process the columns using the tab sequence number to create the output for the tabbed pages.


There’s nothing complex about the generator. It just follows the same steps required to complete this process manually. The metadata provides information about the order of processing, and the values to use in the find/replace.


I’ll cover this in detail in the following sections. In the first section, I’ll run through the top level stored procedure that produces the entire page output. This main procedure calls further stored procedures to create the output code for the datagrid and the tabs and I’ll cover each of these in detail in separate sections.


My goal is to go into sufficient detail to give you a good understanding of how the major processes of the generation process work, and I’ll include extracts from the stored procedure code together with an explanation of exactly what’s going on. I’m not going to try to cover every single statement, so I may skip sections that are straightforward or just a variation of something already covered.


The main stored procedure to process the code is p_dnx_gen_grid_and_tabs (all procedure code is in the file dnx_procs.sql ). This creates the ASPX code for the Order Info page ( order_info) . I’m showing sections of the output as I go through the processing, and the full output text can be viewed in the file order_info.aspx provided with the sample project.


Top Level Generator Procedure


The main procedure p_dnx_gen_grid_and_tabs is passed the name of the page as a parameter and starts off by retrieving the page metadata from the database table dnx_page .



————————————————————————————————————
— Get the page metadata
————————————————————————————————————
SELECT
page_type = page_type ,page_title = page_title
,@application_name = application_name
from dnx_page
where page_name = ppage_name <p></pre><!-- @END —>


It then generates the text to begin the ASPX page. A lot of the information in this section is fixed and the same for every ASPX page, except for the page</span> directive and the <span class='codeintext'>page_title</span> .</p> <p>The generic code text for this has a key of <span class='codeintext'>PageStart</span> and besides fixed or common code, it also contains the <span class='codeintext'>page directive. Here’s an extract from the generic text showing the relevant portions.



<!— *********** PAGESTART GENERATED CODE SECTION 
   ************************************** —>
<%@ Page Language="vb"
Inherits="^APPLICATION.^PAGE_NAME"
CodeBehind="^PAGE_NAME.aspx.vb"
AutoEventWireup="true" %> (Fixed text omitted for brevity)
<DIV class="clsTitle">
<ASP:label id="PageTitle"
runat="server"
text="^PAGE_TITLE">
</ASP:label>
</DIV>


Next I need to retrieve the application name, the page name, and then call the find/replace routine to create the output code for the start of the ASPX page.



————————————————————————————————————
GET THE STANDARD CODE TO BEGIN the page
————————————————————————————————————
declare generate_page_name varchar(50) select generate_page_name = ppage_name exec p_dnx_code_gen 'PageStart' ,'^APPLICATION', application_name
,’^PAGE_NAME’ , generate_page_name ,'^PAGE_TITLE' , page_title


Executing the find/replace routine p_dnx_code_gen outputs the text below with the generic parameters replaced by the actual values provided. Here’s an extract from the full output text to show the effect of the find/replace:



<%@ Page Language="vb" 
Inherits="MetaGen.order_info"
CodeBehind="order_info.aspx.vb"
AutoEventWireup="true" %>

<DIV class="clsTitle">
<ASP:label id="PageTitle"
runat="server"
text="Order Details Inquiry">
</ASP:label>
</DIV>


The PageStart code text has a sequence number of 1.1000. This ensures that when I retrieve it from the output table dnx_generated_code , it will come back in the correct sequence at the beginning. As I set up the generic texts in the database, I assign this sequence number, leaving gaps in the sequence, so I can easily split up texts or add additional ones in if required.


Having processed the more or less standard code to start the page off, the generator creates the major panels to hold the datagrid and the tabs. To recap briefly, the standard page layout is created using panels or DIV s that allow areas of screen real estate to be assigned to display the data. One of the major panels holds the datagrid information. The second major panel holds the tabs and the tab links. It occupies the same amount of real estate as the datagrid as only one of them is ever visible at any one time. The user can switch between the datagrid view and zoom in on the details by clicking on the edit button, which sets the datagrid panel visible attribute to false and the tab panel visible attribute to true , so the links and the data on the current tab become visible.


The main procedure calls another procedure to output the ASPX coding for the datagrid, which I’ll cover in detail in a next section. Once again, the page name is passed as a parameter:



————————————————————————————————————
GENERATE THE GRID
————————————————————————————————————
execute p_dnx_gen_grid ppage_name <p></pre><!-- @END —>


The main procedure then starts processing the tabs, and first outputs the ASPX code to start the enclosing panel:




— If we are using a header, we need to give the tabs less height
— to make room

if use_tab0 = 0 BEGIN execute p_dnx_code_gen 'PanelForAllTabsStart' END ELSE BEGIN execute p_dnx_code_gen 'PanelForAllTabsStartHdr' END <p></pre><!-- @END —>


The if statement is for a small refinement I made to allow information common to all records being displayed in the grid to appear in another panel above the datagrid. It’s used to show common columns such as a requisition number when displaying the associated requisition items. The order_info page doesn’t use this feature, so the generator retrieves the generic text PanelForAlltabs and outputs it without any modification.



<ASP:panel id="tab_screen_panel" runat="server" cssclass="tab_screen_panel">


The texts are essentially the same, but use a different cssclass to vary the percentage of the screen height assigned, according to the percentage occupied by any header information.


Generating the Tab Links Panel


The main panel for all the tabs is the tab_screen_panel and it encloses panels for each individual tab ( tab1_panel , tab2_panel , ) and another panel on the right hand side for the links ( tab_links_panel ). The visible attribute for each tab panel is toggled on or off as the user clicks on a link to simulate the effect of a tabbed page.


The procedure first outputs the ASPX coding for the tab links panel (the CSS class ensures that it appears on the right hand side of the page).



exec p_dnx_code_gen ‘TabLinkStart’


which outputs:



<ASP:PANEL id = "tab_links_panel" runat="server" cssClass="tab_links_panel"> 


The main procedure p_dnx_gen_grid_and_tabs has code to declare a cursor (located near the beginning) in order to iterate through each tab link in sequence:



DECLARE v_dnx_pagecols_links_cursor CURSOR LOCAL FOR SELECT
   page_name
  ,tab_num
  ,tab_name
  from v_dnx_pagecols_tabs
  where page_name = ppage_name
  and isnull(tab_num,'') &lt;&gt; ''
  order by tab_num
<p></pre><!-- @END —>


This reads the tab number and the tab name from the page tabs metadata table dnx_page_tabs in tab number sequence. The generator uses this cursor to output the ASPX code for the tab links.



LOOP THROUGH THE TABS AND GENERATE THE CONTENTS OF THE TAB LINK PANEL
WHILE 1 = 1
BEGIN
FETCH NEXT FROM v_dnx_pagecols_links_cursor INTO page_name ,tab_num ,@tab_name IF FETCH_STATUS <> 0 BREAK execute p_dnx_code_gen ‘TabLink’ , ‘^TAB_NUM’ , tab_num , '^TAB_NAME' , tab_name , ‘^ACCESSKEY’ , tab_num , '^VISIBLE' , visible END


The generic text of Tablink is shown below:



<!— Tab Page link  Used to show the panel corresponding to this tab—>
<ASP:linkbutton id="tab^TAB_NUM_link" runat="server" Class="fmLinkButton_Active" text="^TAB_NAME" ommandName="tab^TAB_NUM" nCommand="TabClicked" ccessKey="^TAB_NUM" isible="^VISIBLE">
</asp:linkbutton>
<BR>


The metadata for the tabs on the order_info page is as shown here:
















Tab NumTab Name
1Order Info
2Shipping Info
3Supplier Info



So I get the following output for the first tab:



<!— Tab Page link  Used to show the panel corresponding to this tab—>
    <ASP:linkbutton   
      id="tab1_link" 
      runat="server" 
      Class="fmLinkButton_Active" 
      text="Order Info"          
      CommandName="tab1"  
      OnCommand="TabClicked"
      AccessKey="1"
      Visible="true">
   </asp:linkbutton>
   <BR>


The ASP.NET code is also generated in a similar fashion for tabs 2 and 3 with their required values.


The tab_links_panel tag is then closed off using the following procedure call.



exec p_dnx_code_gen ‘TabLinkEnd’


Top Level Processing for the Tabs


Generating the tab panels to hold the column data follows a similar process. Once the initial metadata has been set up, one of the first steps is to assign a tab number (which tab the column appears on) and tab sequence number to identify the sequence of the column on the tab.


A cursor is used to read through the tabs in sequence and the stored procedure p_dnx_gen_tab is called to generate the ASP.NET code for the columns on the tab. Again I’ll cover this in more detail in a following section.



exec p_dnx_gen_tab   
   page_name
  ,tab_num


The panel for all tabs is then closed off by outputting the generic text unchanged.



exec p_dnx_code_gen ‘PanelForAllTabsEnd’         



</asp:panel> <!— Close of tab_screen_panel (encloses all tabs)—>


And finally, the ASPX code for the page end is created by outputting the generic text unchanged.




GET THE STANDARD CODE TO END the page

exec p_dnx_code_gen ‘PageEnd’


Generating the Datagrid


The procedure p_dnx_gen_grid creates the ASPX page code for the datagrid. It outputs the panel code to enclose the datagrid:



if use_tab0 = 0 
BEGIN
execute p_dnx_code_gen
   'DatagridStart' 
END
ELSE
BEGIN
execute p_dnx_code_gen
   'DatagridStartHdr' 
END
<p></pre><!-- @END —>


This mirrors the coding for the top level tab panel described previously and just allocates a different height percentage to the grid depending on whether I’m using a header panel. The output from this is as follows:



<ASP:panel id="list_screen_panel" runat="server"   
   cssclass="list_screen_panel" wrap="false" 
   onscroll="javascript:setScroll(this);">


It then outputs the start of the asp:Datagrid tag



execute p_dnx_code_gen
   ‘DatagridStartA’ 


giving:



<asp:datagrid 
  id="MyDataGrid" 
  runat="server" 
  CssClass="Grid"
  AutoGenerateColumns="False"
  AllowSorting="true"
  OnSortCommand="sortGrid"
  ShowFooter="false"
  CellPadding=2
  CellSpacing="0"
  wrap="false"
  OnItemCommand="SelectRow"
  EnableViewState="false"


This information is fixed regardless of whether I am setting a page size for the datagrid or not. When potentially retrieving a large number of records, I use paging. If the page is displaying requisition items based on the currently highlighted requisition header line in the datagrid, I turn paging off so the page will display all requisition items. The page metadata identifies if paging is required or not, so the next step is to output the paging code if required:



————————————————————————————————————
— If paging is required, generate the pager ASPX
————————————————————————————————————
if use_paging = 1 begin execute p_dnx_code_gen 'DatagridPager' , '^PAGESIZE' , pagesize_char
END


This uses the following generic code:



AllowPaging="true" 
PageSize="^PAGESIZE"
PagerStyle-Mode="NumericPages"
PagerStyle-cssClass="GridPager"
OnPageIndexChanged="doPaging"
PagerStyle-Position="Top"


This is the output with the number of rows to display. I originally restricted the pagesize to a number of rows that would comfortably fit on a screen without losing the headings, but this is no longer a consideration, so I may expand the metadata to hold a maximum number of rows to display in the future.


Next I close off the datagrid tag, and create the ASP.NET page code for the alternating item style and Selected item style. This text also includes a ButtonColumn to set the selected row. Originally, clicking on this ButtonColumn set the selected row, but I introduced some additional code behind processing to allow a row to be selected by clicking anywhere in the row. It uses this ButtonColumn to do this, and also makes the ButtonColumn invisible.



execute p_dnx_code_gen
   ‘DatagridStartB’
>
<HeaderStyle CssClass="GridHeader" wrap="false"
/>
<AlternatingItemStyle CssClass="GridAltItem" wrap="false"
/>
<SelectedItemStyle CssClass="GridSelectItem" wrap="false"
/>
<Columns>
<asp:ButtonColumn Text=">" HeaderText="" runat="server"
/>


In order to generate the code for the datagrid, I set up a cursor to read the page columns metadata table – page_columns . This selects columns which have been checked as appearing on the datagrid and the cursor reads them in grid sequence order so that they are generated in the correct sequence.


The processing for the datagrid is actually handled by a stored procedure p_dnx_gen_gridrow that’s passed all the metadata values, but I’ll cover the essential processing.


Most of the datagrid columns can be processed using the DataGridBoundCol generic text:



<asp:BoundColumn 
        DataField = "^COL_NAME"         
        HeaderText = "^COL_HEAD" 
        SortExpression = "^COL_NAME" 
        ItemStyle-HorizontalAlign ="^JUSTIFY"        
        DataFormatString ="^FORMAT"                 
        ItemStyle-Wrap="false" 
    />          


I can get the first 3 parameters directly from the metadata. In order to get the format, the procedure ( p_dnx_gen_gridrow ) uses the data type of the column in the metadata to work out the format string.



select dec_places_char =  convert(varchar(1), dec_places) 
select format_char = case when dec_places <> 0 and cssClass = 'CssNumb' then '{0:N' + dec_places_char + ‘}’ when cssclass = 'Cssdate' then '{0:dd-MMM-yy}' -- We put a blank in the format string to prevent an error from the -- replace. else '' END <p></pre><!-- @END —>


There’s some similar processing to get the rest of the parameter values, so it can call the find/replace procedure:



execute p_dnx_code_gen
  ‘DataGridBoundCol’
  , ‘^COL_NAME’  , col_name
  , '^COL_HEAD'  , col_prompt
  , ‘^CSSCLASS’  , CssClass
  , '^JUSTIFY'   , justify_char
  , ‘^FORMAT’    , format_char
<p></pre><!-- @END —>


For the RequiredDate column, this gives the following output:



<asp:BoundColumn 
  DataField = "RequiredDate"         
  HeaderText = "Required Date" 
  SortExpression = "RequiredDate" 
  ItemStyle-HorizontalAlign ="Left"        
  DataFormatString ="{0:dd-MMM-yy}"                 
  ItemStyle-Wrap="false" 
/>


A bound column isn’t always the only option. For large text fields, I use a TemplateColumn to make the column 2 lines high, and allow the user to scroll the details, but not change it. This uses the following call:



execute p_dnx_code_gen
  ‘DataGridTemplateCol’
  ,’^COL_NAME’  , col_name
  ,'^COL_HEAD'  , col_prompt
  ,’^CSSCLASS’  , CssClass
  ,'^JUSTIFY'   , justify_char
  ,’^FORMAT’    , format_char
<p></pre><!-- @END —>


The generic code is shown below. It uses an ItemTemplate to display the details as a textbox with two lines. The readonly attribute is set to true , so the user can’t type into the textbox. This allows the user to see a reasonable amount of text in the column and also be able to scroll to view all the text if necessary. The generic text for this is shown below:



<asp:TemplateColumn 
  HeaderText = "^COL_HEAD" 
  SortExpression = "^COL_NAME" 
  ItemStyle-HorizontalAlign ="^JUSTIFY"        
>
<ItemTemplate> <asp:TextBox TextMode="Multiline" Readonly="True" id="^COL_NAME" runat="server" cssClass="clsGridTextBox" Text = ‘<%# DataBinder.Eval(Container.DataItem, "^COL_NAME") %>’
>
</asp:TextBox>
</ItemTemplate>
</asp:TemplateColumn>


There’s another generic text for checkboxes, and if I need any more, I can just create another one and extend the generator. Once all the columns have been processed, the standard text to close off the columns tag and close the datagrid panel is outputted.


Generating a Tab


The top level stored procedure uses a cursor and calls another procedure to process each tab ( p_dnx_gen_tab ). This outputs the enclosing panel for an individual tab. To generate the label column pairs for the columns appearing on the tab it declares another cursor to read through the page columns metadata according to the sequence number of the column on the tab. The procedure p_dnx_gen_tabrow is passed all the metadata as parameters and creates the tab screen details for a single column. Since creating the ASPX code for a tab mainly requires setting up the ASPX code for the label/column pairs, I’ll cover this in detail here.


Generating the Column Output


The ASP.NET coding for the columns on a tab has more permutations than the datagrid columns. I decided the simplest technique to handle this is to combine all the required generic texts together and apply a single find/replace once to the combined text. This is more straightforward and shorter than applying several find/replaces and using the sequence number rounding technique to combine the texts in the final output. There is currently an 8K limit on the size of a varchar in SQL Server, (7 and up), but this is adequate for a single column.


To apply the find/replace to a string variable instead of directly to a generic text, I use another stored procedure p_dnx_super_code_gen . This operates almost identically to the find/replace procedure p_dnx_code_gen used previously. It has one extra parameter to hold the text for the find/replace and this is used instead of the generic text. A generic text key is still passed, but this is only used to provide the output sequence for the text.


I start off by retrieving the generic text to begin the tab row into a variable:



SELECT changer =  convert(varchar(8000),code_text)
from dnx_pagetext
where text_id =   'TabRowStartandLabel'
<p></pre><!-- @END —>


This generic text is shown below. The id for the label tag uses the column name suffixed with a _t . The CSS class for this label has a parameter of ^RFV to allow for the possibility of indicating mandatory fields with a different font although this is not used at present. The second span tag can hold an asterisk to indicate a mandatory column, or spaces for optional columns.



<!— ************** START OF ^COL_NAME ROW DIV*************** —>       
<div class="row">
<SPAN class="label"> <ASP:label id="^COL_NAME_t" cssclass="clsLbl^RFV" runat="server" text="^COL_PROMPT"> </ASP:label> <SPAN cssclass = "MandSpan"> ^MANDCHAR </SPAN>
</SPAN>
<SPAN class="formw">


I then append the generic text for a column to it:



SELECT changer = changer + convert(varchar(8000),code_text)
from dnx_pagetext
where text_id = CASE WHEN isnull(display_style,'') = '' THEN 'TabRowTextBox' WHEN upper(display_style) = ‘CHECKBOXTHEN ‘TabRowCheckBox’ WHEN upper(display_style) = 'DROPDOWNLIST' THEN 'TabRowDropDownList' END <p></pre><!-- @END —>


This appends the code for a textbox, checkbox or dropdownlist to my column ASPX string depending on the display style assigned in the metadata. Here’s the generic code for a textbox:



<ASP:textbox                          
  id="^COL_NAME"                        
  cssclass="^CSSCLASS"                     
  Maxlength="^MAXLENGTH"                
  ^TEXTMODE
  ^ENABLED                  
  OnTextChanged="Field_Changed"
  ^INPUTCASE
  runat="server"> 
</ASP:textbox>


The id is always the column name. The MaxLength value is obtained from the metadata.


The TEXTMODE replacement value is also set up from the metadata as follows:



select  textmode_char = 
CASE 
  WHEN is_multiline = 1 THEN   ‘TextMode="Multiline " Rows = "3"   ‘ 
  ELSE ‘’
END


As is the enabled attribute



select enabled_char = case 
WHEN protected = 0 
  then ‘enabled = "true"’ 
  else ‘enabled = "false"’ 
END


I use one CSS class for enabled text boxes, and a different one for disabled text boxes to indicate which columns can be changed or not.



select cssclass = 
case 
  when protected = 0 
    then ‘ClsTextBox’ 
  else ‘ClsTextBox_protected’
END


I can also force the input to be upper case, lower case, or allow any case depending on the metadata value:



— Add in javascript to force upper case if required. 
select inputcase_char = case inputcase WHEN ‘U’ then ‘onBlur="javascript:{this.value = this.value.toUpperCase(); }"’ WHEN ‘L’ then ‘onBlur="javascript:{this.value = this.value.toLowerCase(); }"’ ELSE ‘’
END


This javascript will replace the INPUTCASE parameter in the TabRowTextBox generic script to ensure the correct case (usually upper) is input when required. The default value allows any case, so the metadata only needs to be modified where a specific case is required.


For a date field, the date always has to be valid, so I append a compare validator to my variable:



— Add in Date Checking 
IF lower(cssCLass) = lower ('CssDate') BEGIN SELECT changer = changer + convert(varchar(8000),code_text) from dnx_pagetext where text_id = 'TabRowDTC' END <p></pre><!-- @END —>


The generic text TabRowDTC holds a generic CompareValidator



<asp:CompareValidator 
  id="^COL_NAME_dtc" 
  runat="server" 
  ErrorMessage="Please Enter a valid date in ^COL_PROMPT" 
  Type="Date" 
  Operator="DataTypeCheck" 
  ControlToValidate="^COL_NAME"?
</asp:CompareValidator>


The id of the validator is the same as the column name with a suffix of _dtc . I use the label text for the column to create the error message, and the control to validate is the current column.


If the field is decimal, I can use a generic regular expression validator.



IF isnull(display_style,'') = '' 
and lower(cssCLass) = lower (‘CssNumb’)
and dec_places &gt; 0 BEGIN SELECT changer = changer + convert(varchar(8000),code_text) from dnx_pagetext where text_id = 'TabRowDecimal' END <p></pre><!-- @END —>


This adds the following text:



<asp:RegularExpressionValidator 
  id="^COL_NAME_vdec" 
  runat="server" 
  ControlToValidate="^COL_NAME"
  ValidationExpression="d{0,^INT_PLACES}(.d{1,^DEC_PLACES})?$"
  runat="server"
  ErrorMessage="Please Enter a valid number in ^COL_PROMPT" 
> *
</asp:RegularExpressionValidator>


I obtain the number of integer places and decimal places from the metadata.



select dec_places_char = convert(varchar(5), dec_places  )
select int_places_char = convert(varchar(5), int_places )
select dec_places_char = isnull(dec_places_char,’‘)
select int_places_char = isnull(int_places_char,’‘)


For non-decimal columns, this will return null , so I replace nulls with spaces. As I mentioned previously, this shouldn’t cause the find/replace any problems, as it simply won’t find anything to replace for these parameters if the column isn’t decimal.


Finally, I close off the DIV tag that encloses the label/column pair:



SELECT changer = changer + convert(varchar(8000),code_text)
from dnx_pagetext
where text_id = ‘TabRowEnd’


The find/replace routine is called to modify the entire string and create the output.



exec p_dnx_super_code_gen  ‘TabRowTextBox’, changer
  ,'^COL_NAME', col_name          
  ,’^COL_PROMPT’, col_prompt
  ,'^CSSCLASS', CssClass          
  ,’^MAXLENGTH’, maxlength_char   
  ,'^ENABLED', enabled_char       
  ,’^TEXTMODE’, textmode_char     
  ,'^RFV', rfv                    
  ,’^DEC_PLACES’, dec_places_char
  ,'^INT_PLACES', int_places_char 
  ,’^INPUTCASE’, inputcase_char 
  ,'^MANDCHAR', mandchar


Although I’m changing the text in the variable changer</span> , I need to provide the key of a generic text for the output sequence, so I just use <span class='codeintext'>TabRowTextBox</span> even if the column is a decimal or a date field.</p> <p>There are few permutations here, but basically, this process just involves combining the ASPX for the label with the generic ASPX for the column and appending a validator when required. The find/replace routine is then used to modify this for the column being processed.</p> <a id="Heading16"></a><h2 class="redhead0">Column Output Examples</h2> <p>As an example of the generated code, here's some extracts showing the main details output for a multiline textbox.</p> <!-- @START —>

<ASP:textbox                          
  id="ProductName"                        
  cssclass="ClsTextBox_protected"                     
  Maxlength="80"                
  TextMode="Multiline " Rows = "3"   
  enabled = "false"                  
  OnTextChanged="Field_Changed"


This is the generated ASPX code for a decimal field with a regular expression validator:



<asp:textbox                          
  id="UnitPrice"                        
  cssclass="ClsTextBox"                     
  Maxlength="0"                
  enabled = "true"                  
  OnTextChanged="Field_Changed"
  runat="server"                        
>
</asp:textbox>
<asp:RegularExpressionValidator id="UnitPrice_vdec" runat="server" ControlToValidate="UnitPrice" ValidationExpression="d{0,15}(.d{1,4})?$" runat="server" ErrorMessage="Please Enter a valid number in Unit Price"
> *
</asp:RegularExpressionValidator>


Finally, this is an example of the output for a date field with the date validator:



<asp:textbox                          
  id="RequiredDate"                        
  cssclass="ClsTextBox_protected"                     
  Maxlength="20"                
  enabled = "false"                  
  OnTextChanged="Field_Changed"
  runat="server"                        
>
</asp:textbox>
<asp:CompareValidator id="RequiredDate_dtc" runat="server" ErrorMessage="Please Enter a valid date in Required Date" Type="Date" Operator="DataTypeCheck" ControlToValidate="RequiredDate"
> *
</asp:CompareValidator>


The process above is repeated for all the columns and finally, the ASP.NET code to close off the panel for the tab is outputted.


Next Steps


Holding information separately as metadata has lots of advantages. Initially, my goal was just to automate the production of the ASP.NET page code, as the layout I use is fairly complex to create. Along the course of the development, I realized that there’s a whole area of processing that’s really more or less the same, but varies depending on the columns being processed and this also includes VB.NET. There’s a lot of coding dependent on the columns on the page.


Now, with some further development, I generate the major portion of the VB.NET code behind page as well. In conjunction with OOP, this is a powerful tool. I’m sure you can see lots of places where the VB.NET code behind only varies based on the columns being processed, just like the ASPX page. Complex business logic can’t be automated, but I modified the generator so this can be hooked in so I can regenerate the VB.NET code as often as required without any impact. I’m working on a way to hold some of the simple rules in metadata to see if this can cut out more manual coding.


The metadata is a valuable resource and I’ve managed to extend it to hold dropdown list information as well. Processing for all dropdown lists, including dependent dropdowns is also automated now. I only discovered how to achieve this through using the metadata approach and originally thought that this would definitely have to be handled by custom coding. All of this is a bit of a work in progress, and I’m still discovering areas that offer scope for automation, allowing me to concentrate on the interesting parts and do as little as possible of the boring bits.


Since the metadata contains all the information about the details on the page, a printer friendly page can be created by creating a new generator stored procedure to process the metadata and create the ASPX for this.


I can generate search pages from the column information. Basically, I use another variant of the generator that outputs the tab pages with an additional dropdown list between the column prompt and the column. The dropdownlist allows entry of selection criteria such as equals, less than, greater than and like. The code behind simply displays the tab view with all columns empty, so the user can input selection criteria and values. The values and criteria can then be passed to a maintenance page and used to amend the select statement to return rows matching the criteria.


At the moment, I don’t have a fully editable (as opposed to single record at a time) datagrid, but the potential is there to generate one once I work out how or if I decide to use a third party control.


I considered a number of options before deciding to indicate mandatory fields with an asterisk. To add in this feature, I modified the generator processing, and regenerated my pages, which was a lot easier than changing the ASPX code manually. Also, there’s far less chance of missing something out.


It’s valuable for adding in enhancements/new features as well. These can be added into the generator and they’ll automatically be available following re-generation. It’s also a very useful maintenance resource tool as I can run queries against the metadata to find out where columns are used.


The executable to maintain the metadata is a big help. It also makes it possible for someone with no knowledge of VB.NET or ASP.NET to set up the screen layouts and leave the custom coding to a developer. With this approach, the prototype doesn’t get dumped once real development gets underway. It becomes the real development with the addition of business rules logic.


Conclusion


In this the second and final article in this series I highlighted the behind the scenes use of the metadata that was built in part 1. This was achieved by basically automating the process of creating a page step-by-step. In this case I chose to leverage the strengths of use a database, SQL Server, to not only store the metadata but to also generate the ASPX pages themselves.


I started with an overview of why I chose to use MS SQL Server to create the generation process. Next I detailed out a generic find/replace procedure that is crucial to the process. I then described the top level process and how the tabs on the website are created from the metadata. Lastly I stepped through the creation of the datagrid and how I developed it so that it is generic and can house practically any table.


One of the really strong points of this design is the use of the metadata. By creating the metadata and having the ability add more metadata you have endless opportunity for expansion of the generation process. The Next Steps section also provided many ideas for using the metadata outside of the generation process.

Founders at Work

Commenting is closed for this article.