
Introduction
Most of database application UI represent data as a combination of lists and
forms. The pages are very often the same : search criteria (form), result table
(list) with add/edit/delete buttons, and data detail (form) with ok/cancel
button. This is especially the case for admin pages (reference table like
countries / cities) without much business rules (just ensuring the relational
integrity).
A standard approach requires to develop specific code for each table and page
(data object + sql requests + aspx + code behind).
The solution described in this article consists in :
writing all that is specific into one or two description files : meta data
(table name, columns, sql commands) + rendering info (labels, edit types,
layout,...)generating on the fly the pages for the list and the detail (search criteria
/ result list / master-detail / detail page / buttons) with all these
informations
In order to develop specific simple pages (list + edit), you just have to write
the description files and that's all. No code needed (no class, no aspx, no code
behind) !
Similar articles on CodeProject :
Universal Database Admin for ASP.NET and SQL Server
Database Viewer and Updater for any Database
Database Navigator
Difference : generates end-user UI, not only for DBA
Go to online demo
Detail.aspx - Sample generated detail page
Features
Generic data objects (table / record), data info (metadata) and rendering
info (how to display a table or a record depending on the context)Interfaces for retrieving and saving the datas : Reader / Writer. The
standard reader and writer access the database but since it's an interface they
can be replaced (for exemple LDAP requests or CSV/XLS files)Generator on the fly for list / record pages and automatic data bindingMulti-language resources, but also multi-target, multi-user role, multi-clientASP.NET 1.1 and 2.0, written in C# for .NET 1.1 and 2.0ADO.NET : Oracle, MySql, Access, SqlServer,...Possible customization of controls : adding edit types for your specific
needs and the generator would create your web controlsLogging with Log4net (encapsulated so it can be replaced easily)WindowsForms : to do !
This is mainly for IT developers, not yet for non-developers. Simples pages
can be generated without effort. It also can be used for more complex pages :
the developer would inherit the default page and assemble user controls
(generated by the tool).
This tool is supposed to remain relatively lightweight, simple and extensible.
I'm not a fan of complex architectures especially for simple functionnalities
like CRUD. But I think it can be used with other frameworks : O/R mapping (like
NHibernate) by reflection, controller (like Maverick or NStruts) by interface
(the controller can be replaced).
Description files
The description files consists in :
a module file : sql commands / table / columns / data types and rules / relationsone or many resource file : rendering properties like labels, visible,
enabled, edit types (text box, combo-box, radio buttons, check-box, ...), access
control (can read / insert / update / delete depending on user role), texts,...
These are simple text files with sections (like .ini with parameters in
sections) because they are more human friendly. In future versions, there should
be XML files (if possible compliant to SOAP or NHibernate DTD) and even a
Windows UI for generating and editing these files.
There can be a resource file for each language. The rendering properties may
depend on the language, the action (view/edit/new/…), the target
(web/windows/pocket), the user role (admin / guest / …) and the client base (if
you want to distribute the same application to different customers with
different parameters).
#---- module file for currency table ------------------------------------
[sql]
select = select * from currency
order by cur_cd
selectOne = select * from currency
where cur_id = ?cur_id
checkBeforeDelete = select count(*) from country where cur_id = ?cur_id
#------------------------------------------------------------------------
[table]
name = currency
primaryKeys = cur_id
#------------------------------------------------------------------------
[column name=cur_id #nullable=0]
[column name=cur_cd type=VA3 nullable=0]
Unique = 1
[column name=cur_name domain=name nullable=0]
[column name=cur_name_2 domain=name nullable=0]
[column name=cur_name_3 domain=name]
[column name=cur_type type=N2 nullable=0]
[column name=cur_status type=B nullable=0]
[column name=cur_rate type=N10,5]
[column name=cur_creation_date type=D]
#------------------------------------------------------------------------
[relation columns=cur_type]
Reference = currency-type
ReferenceColumns = cur_type
DisplayField = cur_type_name
See screens below
#---- resource file for currency table (english version) ----------------
[TableRender]
ListTitle = List of currencies
ColumnOrder = cur_cd, cur_name_2, cur_type, cur_status, cur_rate
EditOrder = cur_cd, cur_name_2, cur_name, cur_name_3, cur_type, cur_status, cur_rate, cur_creation_date
EditTitle = Currency detail : {cur_name_2}
[TableRender action=new]
EditTitle = New currency
#------------------------------------------------------------------------
[ColumnRender name=cur_cd]
Label = Code
Link = 1
[ColumnRender name=cur_name]
Label = Name (french)
[ColumnRender name=cur_name_2]
Label = Name
[ColumnRender name=cur_name_3]
Label = Name (german)
[ColumnRender name=cur_rate]
Label = Exchange rate (USD)
[ColumnRender name=cur_type]
Label = Type
RelationDisplayField = cur_type_name_2
[ColumnRender name=cur_status]
Label = Accepted payment
[ColumnRender name=cur_creation_date type=D]
Label = Creation date
There can be expressions. For exemple :
# the column "cur_name_2" will be retrieved from the record
EditTitle = Currency detail : {cur_name_2}
# record will be updateable only if the IsAdmin property
# in the session is set (either by param/key or by reflection)
EditUpdateable = {session.IsAdmin}
The layout can be more complex :
# adds a sublist below the main form
[TableRender]
DetailLists = currency_country
ActiveDetailList = 0
# the layout can be a complex hierarchy of tables / rows / cells
[EditRender]
Layout = row1, row2, row3, row4, row5, row6, row7
row1 = cur_cd
row1.BackColor = magenta
row2 = cur_name_2
cur_name.width = 100px
row3 = cur_name, cur_name_3
cur_name_2.width = 100px
cur_name_3.width = 100px
row4 = cell41
cell41 = label1
cell41.ColumnSpan = 4
label1.Text = Currency properties
label1.ForeColor = #EE00FF
label1.Width = 200px
label1.Height = 20px
row5 = cur_type
row6 = cur_status, cur_rate
row7 = cur_creation_date
Generates Detail.aspx
Class diagram
data objects : Record and RecordTable which are a kind of enhanced DataRow
and DataTable, contains them so standard DataTable controls can be used
(DataGrid / DataList)data info : RecordInfo with metadata (table / columns properties)relation info : relation between a source and a destination RecordInfo with
column correspondance (id and label)render info : RecordRenderInfo depending on a RenderContext (language,
action, target, user role and client base)
Data access layer : interface for reading and writing records. The default
implementation access the database but there can be other implementations for
LDAP or CSV / XLS files.
public interface IRecordEnumerator : IEnumerator {
Record CurrentRecord { get; }
}
public interface IRecordReader : IRecordEnumerator {
RecordInfo RecordInfo { get; }
int Depth { get; }
bool IsClosed { get; }
int RecordsAffected { get; }
void Close();
DataTable GetSchemaTable();
bool NextResult();
bool Read();
int RowNum { get; }
object Parameters { get; }
}
public interface IRecordWriter {
void ExecuteWriter(IDbConnection dbConnection, IDbTransaction dbTransaction, Record rec);
void ExecuteWriter(IDbConnection dbConnection, IDbTransaction dbTransaction, RecordTable rt);
}
Controller / UserControl / Page
There are UserControls (class + ascx) and Pages (class + aspx) for a record and
a list. They implement interfaces so they can be replaced by other classes and controllers.
The interface can even serve for WindowsForms.
The list page (List.aspx) contains a record list (RecordTable or RecordReader),
eventually a parameter record (search criteria) and other elements (message
label for confirmation / warning / error messages, pagination, buttons).
<!-- #include file="_Top.inc"-->
<asp:Label id="lblMessage" runat="server" Visible="false"></asp:Label>
<form runat="server">
<% if (HasVisibleParameters) { %>
<ucRecord:Record id="paramRecordControl" runat="server"></ucRecord:Record>
<input type="hidden" name="__action" value="search">
<asp:Button id="btSearch" runat="server" Text="Search" onclick="btSearch_Click"></asp:Button>
<% } %>
<ucList:RecordList id="listControl" runat="server"></ucList:RecordList>
<ucPagination:Pagination id="paginationControl" runat="server"></ucPagination:Pagination>
<asp:Button id="btAdd" runat="server" Text="Add" onclick="btAdd_Click"></asp:Button>
</form>
<!-- #include file="_Bottom.inc"-->
The detail page (Detail.aspx) contains the main RecordControl, a message label,
validate / cancel buttons and eventually a sublist (RecordTableControl)
<!-- #include file="_Top.inc"-->
<form runat="server" Id="form1">
<asp:Label id="lblMessage" runat="server" Visible="false"></asp:Label>
<ucRecord:Record id="recordControl" runat="server" RecordName="record"></ucRecord:Record>
<% if (RenderContext.IsViewAction || RenderContext.IsViewSummaryAction) { %>
<asp:Button id="btReturn" causesValidation="false" runat="server" Text="Retour" onclick="btCancel_Click"></asp:Button>
<asp:Button id="btEdit" runat="server" Text="Modifier" onclick="btEdit_Click"></asp:Button>
<% }
else { %>
<asp:Button id="btCancel" causesValidation="false" runat="server" Text="Annuler" onclick="btCancel_Click"></asp:Button>
<asp:Button id="btValidate" runat="server" Text="Valider" onclick="btOK_Click"></asp:Button>
<% } %>
<asp:ValidationSummary id="validationSummary" runat="server"></asp:ValidationSummary>
<br>
<% if (!RenderContext.IsNewAction && RecordRenderInfo.HasDetailList) {
btAdd.Text = GetText("button.AddSublist");
%>
<ucList:RecordList id="subListControl" runat="server"></ucList:RecordList>
<br>
<asp:Button id="btAdd" runat="server" Text="Add" onclick="btAddSublist_Click"></asp:Button>
<% } %>
</form>
<!-- #include file="_Bottom.inc"-->
Controller source (List.aspx.cs)
void Page_Init(object sender, EventArgs e) {
base.Page_Init(sender, e);
if (HasVisibleParameters) {
paramRecordControl.Record = ParametersRecord;
paramRecordControl.BuildControls();
}
}
void Page_Load(object sender, EventArgs e) {
base.Page_Load(sender, e);
if (HasVisibleParameters) {
if (!IsPostBack) paramRecordControl.RecordToControls();
else paramRecordControl.ControlsToRecord();
}
ExecuteReader();
if (IsReaderMode) {
listControl.RecordReader = RecordReader;
paginationControl.PageSize = 0;
}
else {
listControl.RecordTable = RecordTable;
paginationControl.PageSize = RecordRenderInfo.PageSize;
paginationControl.PageNum = PageNum;
paginationControl.RowCount = RecordTable.Count;
}
btAdd.Text = GetText("button.Add");
btSearch.Text = GetText("button.Search");
}
Open source project
The work is not finished yet. I have many improvements in mind. I need help for
continuing the work and also would like to share ideas with the community. So I
propose this as an open source project !
People needed for development, testing, documentation, website administration,
translations,... The open source licence would be simple and commercial use would
be authorized.
Please reply to this thread or write me at the address provided in the online demo.
IT professional in Paris with 10 years of experience in Windows, client/server, databases (mostly Oracle) and J2EE.
I have discovered .NET lately and I like it !