Introduction
The series of articles introduces an innovative architecture to develop business web forms in enterprise software development which is better performance, higher productivity, more configurability and easier maintainability than traditional either ASP.NET or MVC development. This is the first article to introduce the facing problems on traditional ASP.NET and MVC development and a new solution to improve them. Then I give a code snippet on developing a product management web form in the new solution.
Problems
When we develop a web form in ASP.NET or MVC, we need work on the following items:
- Create view template and code behind (controller)
- Style adjustment
- Write event handlers for all actions
- Control visibility of web controls for different actions manually
- Permission integration manually
- Hard to write unit tests (not for MVC)
- Something else
With all of this stuff, the source code of developed web forms are very complex usually by mixed logics of webcontrols' visibility control, business logics, style management, permission check, etc. The web forms are less productive, bug-ness and hard maintainable.
New Solution
RapidWebDev solution implements a new UI framework based on ASP.NET which is designed to solve the facing problems. The principle of design is to abstract requirement of web forms for usual business scenarios and define XML schema to configure web forms so that user behavior, style, permission are all managed by the framework. And some interfaces of the framework are required to be implemented which are callback for user behaviors. E.G. there is a query panel with filters configured. When a user clicks query button in query panel, method Query
of the implementation to interface IDynamicPage
will be invoked with query parameters.
With this design,
- web forms are configurable and maintainable
- code complexity is decreased obviously
- implementations are reusable and testable
- consolidate UI style by the framework
So, the roadmap of introduction to RapidWebDev
UI framework in this article is to introduce:
- web form structure/layout
- communication between components in a web form
- what is required to develop a web form
- a sample product management application
- how permission works
This article doesn't introduce the detail implementation of the architecture but focusing on high level innovative points. I will introduce the details in later articles in the series.
Web Form Structure
We abstract generic panel types for usual business scenarios - "query panel" to set query filters; "grid panel" to display queried records; "detail panel" to create/update/view/delete a single record; "button panel" to configure buttons for custom actions; "aggregate panels" for bulk operate multiple records selected in grid, etc.
Let's go through some usual business scenarios in enterprise software.
- in a product management web form, some users can create a draft product, approve products, export or forward them. There should be a "query panel" for users to setup filters, a "grid panel" to display queried products in list, a "detail panel" to create/update/view a single product detail information, an approve panel ("aggregate panel") to approve selected products in the grid and a forward panel ("aggregate panel") to forward selected products to someone else by mail, etc.
- in a stock-in product web form, users can scan product number in a scanning panel ("detail panel" with a product number input
textbox
) and get scanned products displayed in a temporary "grid panel". Users can submit multiple selected scanned products in grid and get a stock-in sheet ("aggregate panel").
- in a news management web form, users can setup new query filters in "query panel" and get them displayed in "grid panel". Users can add a new news or select a exist news in grid to update, delete or view detail information in "detail panel".
Let's preview these panel types as the following screenshot.
Query, Button and Grid panel
Detail panel
Aggregate panel
Communication b/w Web Form Panels
The communication b/w panels is described as the following workflow chart:
Query Panel
When a user clicks Query button in query panel, the query panel grabs all query filters and sends an asynchronous request to web server and pull records rendered into grid panel.
Grid Panel
Display queried records. There are three buttons that can be configured for each row in the grid, they're Edit, View and Delete. When the user clicks Edit/View button in a row, the detail panel is shown up. In Edit mode, the grid refreshes automatically after the user saves the editing record successfully. The grid supports column resizing, show/hide column, sorting, paging, row preview automatically.
Button Panel
Configure custom buttons in web form that each button is assigned with a command argument. When a button is clicked, the aggregate panel with the same command argument will be displayed. But there are three command arguments that are predefined which do not relate to aggregate panels. They're Add
, Print
and DownloadToExcel
. A blank detail panel is displayed when Add
button is clicked. Print
and DownloadToExcel
button is used to print or export all grid records (include in other pagination) to Excel without writing any code.
Detail Panel
Create/update/view a single record. Grid panel refreshes automatically after the user saves in detail panel.
Aggregate Panel
Any custom operations. Grid panel refreshes automatically after the user saves in aggregate panel.
What're Required to Develop a Web Form
XML configuration and implementation of interface IDynamicPage
is required for a web form. In XML configuration, we should configure query filters, grid fields, buttons, detail panel and aggregate panels. Don't worry about dynamic data in static XML configuration. XML configuration allows to configure callback processors.
When we need detail panel in a web form, we need to develop an ascx template without code behind and implements interface IDetailPanel
. The ascx template is used to render the web controls in detail panel as the following screenshot. The IDetailPanel
implementation integrates the ascx template for business logics.
The aggregate panel development is the same to detail panel.
A Sample Product Management Application
We have a requirement of a product management web form as follows:
- Create a new product
- Edit/view an existed product
- Delete a single product
- Bulk delete multiple selected products
- Print queried products
- Export queried product to Excel
- Show the product changing logs when edit/view a product
Step 1
Implement dynamic page interface which is used for querying products and deleting a single product. You see in the method Query, actually we don't assemble query expression. The argument "parameter" passed from the framework can be converted to LinqPredicate directly. The framework not only uses query panel XML configuration to render UI but also assemble query expression. So when we have requirement to change query filters, we only need to change the XML configuration without compilation.
public class ProductDynamicPage : DynamicPage
{
private IAuthenticationContext authenticationContext =
SpringContext.Current.GetObject<IAuthenticationContext>();
public override QueryResults Query(QueryParameter parameter)
{
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
IQueryable<Product> q = from p in ctx.Products
where p.ApplicationId ==
authenticationContext.ApplicationId
select p;
LinqPredicate predicate = parameter.Expressions.Compile();
if (predicate != null && !string.IsNullOrEmpty(predicate.Expression))
q = q.Where(predicate.Expression, predicate.Parameters);
if (parameter.SortExpression != null)
q = q.OrderBy(parameter.SortExpression.Compile());
int recordCount = q.Count();
var results = q.Skip(parameter.PageIndex *
parameter.PageSize).Take(parameter.PageSize).ToList();
return new QueryResults(recordCount, results);
}
}
public override void Delete(string entityId)
{
Guid productId = new Guid(entityId);
using (TransactionScope transactionScope = new TransactionScope())
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
ctx.ProductLogs.Delete(log => log.ProductId == productId);
ctx.Products.Delete(p => p.Id == productId);
ctx.SubmitChanges();
transactionScope.Complete();
}
}
}
Step 2
Create an ascx template for product detail panel. The template is used to render the form to add/update/view a product. The ASP.NET server control will be bound to the member declaration of detail panel implementation class automatically by ID.
<ajax:TabContainer ID="TabContainer" runat="server">
<ajax:TabPanel ID="TabPanelProduct" HeaderText="Product" runat="server">
<ContentTemplate>
<table cellpadding="0" cellspacing="0" class="table6col">
<tr>
<td class="c1" nowrap="nowrap">Category: </td>
<td class="c2">
<My:ComboBox ID="DropDownListCategory" Mode="Local"
Editable="true" ForceSelection="true" runat="server" />
<label for="<%= this.DropDownListCategory.ClientID %>"
class="required">*</label>
</td>
<td class="c1" nowrap="nowrap">Name: </td>
<td class="c2">
<My:TextBox ID="TextBoxName" CssClass="textboxShort"
MaxLength="256" runat="server" />
<label for="<%= this.TextBoxName.ClientID %>"
class="required">*</label>
</td>
<td class="c1" nowrap="nowrap">Number: </td>
<td class="c2">
<My:TextBox ID="TextBoxNumber"
CssClass="textboxShort" MaxLength="32" runat="server" />
<label for="<%= this.TextBoxNumber.ClientID %>"
class="required">*</label>
</td>
</tr>
<tr>
<td class="c1" nowrap="nowrap">Manufactory: </td>
<td class="span" colspan="5">
<My:TextBox ID="TextBoxManufactory" CssClass="textarea"
MaxLength="256" Width="92.7%" runat="server" />
</td>
</tr>
<tr>
<td class="c1" nowrap="nowrap">Description: </td>
<td class="span" colspan="5">
<My:TextBox ID="TextBoxDescription" CssClass="textarea"
Width="92.7%" runat="server" />
</td>
</tr>
<My:ExtensionDataForm ID="ProductExtensionDataForm" runat="server" />
<asp:PlaceHolder ID="PlaceHolderOperateContext" Visible="false"
runat="server">
<tr>
<td colspan="6"><hr /></td>
</tr>
<tr>
<td class="c1" nowrap="true">Created On: </td>
<td class="c2">
<My:TextBox ID="TextBoxCreatedOn"
CssClass="textboxShort readonly"
ReadOnly="true" runat="server" />
</td>
<td class="c1" nowrap="true">Created By: </td>
<td class="span" colspan="3">
<My:UserLink ID="UserLinkCreatedBy" runat="server" />
</td>
</tr>
<tr>
<td class="c1" nowrap="true">Updated On: </td>
<td class="c2">
<My:TextBox ID="TextBoxLastUpdatedOn"
CssClass="textboxShort readonly" ReadOnly="true"
runat="server" />
</td>
<td class="c1" nowrap="true">Updated By: </td>
<td class="span" colspan="3">
<My:UserLink ID="UserLinkLastUpdatedBy" runat="server" />
</td>
</tr>
</tr>
</asp:PlaceHolder>
</table>
</ContentTemplate>
</ajax:TabPanel>
<ajax:TabPanel ID="TabPanelProductLogs" HeaderText="Logs" Visible="false"
runat="server">
<ContentTemplate>
<table cellpadding="2" cellspacing="0" border="1" style="width:100%;
border-collapse:separate">
<asp:Repeater ID="RepeaterProductLogs" runat="server">
<HeaderTemplate>
<tr>
<th style="width: 60px; padding:2px;
background-color: Gray; color: White">
Number
</th>
<th style="padding:2px; background-color: Gray;
color: White">Body</th>
<th style="width: 120px; padding:2px;
background-color: Gray; color: White">
User
</th>
<th style="width: 170px; padding:2px;
background-color: Gray; color: White">
Logged On
</th>
</tr>
</HeaderTemplate>
<ItemTemplate>
<tr>
<td style="text-align:center; padding:2px">
<%# Container.ItemIndex + 1 %>
</td>
<td style="padding:2px">
<%# DataBinder.Eval(Container.DataItem, "Body") %>
</td>
<td style="padding:2px">
<%# UserLink.BuildUserLink(
DataBinder.Eval(Container.DataItem,
"LoggedBy").ToString())%>
</td>
<td style="padding:2px">
<%# DataBinder.Eval(Container.DataItem, "LoggedOn") %>
</td>
</tr>
</ItemTemplate>
</asp:Repeater>
</table>
</ContentTemplate>
</ajax:TabPanel>
</ajax:TabContainer>
Step 3
Implement detail panel interface which is used for add/update/view a single product. The references of data members with custom attribute Binding
are resolved by the framework from ascx template automatically. The data member name should be the same as the server control ID in the template. The argument parentControlPath
of Binding(string parentControlPath)
indicates which ASP.NET ITemplate
control includes the control.
public class ProductDetailPanel : DetailPanelPage
{
#region Binding Controls
[Binding("TabContainer/TabPanelProduct")]
protected DropDownList DropDownListCategory;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxName;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxNumber;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxManufactory;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxDescription;
[Binding("TabContainer/TabPanelProduct")]
protected ExtensionDataForm ProductExtensionDataForm;
[Binding("TabContainer")]
protected AjaxControlToolkit.TabPanel TabPanelProductLogs;
[Binding("TabContainer/TabPanelProduct")]
protected PlaceHolder PlaceHolderOperateContext;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxCreatedOn;
[Binding("TabContainer/TabPanelProduct")]
protected UserLink UserLinkCreatedBy;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxLastUpdatedOn;
[Binding("TabContainer/TabPanelProduct")]
protected UserLink UserLinkLastUpdatedBy;
[Binding("TabContainer/TabPanelProductLogs")]
protected Repeater RepeaterProductLogs;
#endregion
private IConcreteDataApi concreteDataApi =
SpringContext.Current.GetObject<IConcreteDataApi>();
private IMetadataApi metadataApi = SpringContext.Current.GetObject<IMetadataApi>();
private IAuthenticationContext authenticationContext =
SpringContext.Current.GetObject<IAuthenticationContext>();
public override void OnLoad(IRequestHandler sender, DetailPanelPageEventArgs e)
{
if (!sender.IsPostBack)
{
var productCategories = concreteDataApi.FindAllByType("ProductCategory")
.Where(c => c.DeleteStatus == DeleteStatus.NotDeleted)
.OrderBy(c => c.Name);
this.DropDownListCategory.Items.Clear();
this.DropDownListCategory.Items.Add("");
this.DropDownListCategory.SelectedIndex = 0;
foreach (ConcreteDataObject productCategory in productCategories)
this.DropDownListCategory.Items.Add
(new ListItem(productCategory.Name, productCategory.Id.ToString()));
}
IObjectMetadata productMetadata = metadataApi.GetType("Product");
if (ProductExtensionDataForm != null)
this.ProductExtensionDataForm.CreateDataInputForm(productMetadata.Id);
}
public override void LoadWritableEntity(string entityId)
{
Guid productId = new Guid(entityId);
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
Product p = ctx.Products.FirstOrDefault(product =>
product.Id == productId && product.ApplicationId ==
authenticationContext.ApplicationId);
if (p == null)
throw new ValidationException("The product id doesn't exist.");
this.TextBoxName.Text = p.Name;
this.TextBoxNumber.Text = p.Number;
this.DropDownListCategory.SelectedValue = p.CategoryId.ToString();
this.TextBoxDescription.Text = p.Description;
this.TextBoxManufactory.Text = p.Manufactory;
this.ProductExtensionDataForm.SetControlValuesFromObjectProperties(p);
this.PlaceHolderOperateContext.Visible = true;
this.UserLinkCreatedBy.UserId = p.CreatedBy.ToString();
this.TextBoxCreatedOn.Text =
LocalizedDateTime.ToDateTimeString(p.CreatedOn);
if (p.LastUpdatedBy.HasValue)
this.UserLinkLastUpdatedBy.UserId = p.LastUpdatedBy.ToString();
if (p.LastUpdatedOn.HasValue)
this.TextBoxLastUpdatedOn.Text =
LocalizedDateTime.ToDateTimeString(p.LastUpdatedOn.Value);
this.TabPanelProductLogs.Visible = true;
this.RepeaterProductLogs.DataSource =
p.ProductLogs.OrderBy(l => l.LoggedOn).ToList();
this.RepeaterProductLogs.DataBind();
}
}
public override string Create()
{
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
this.Validate(ctx, null);
IObjectMetadata productMetadata = metadataApi.GetType("Product");
Product p = new Product
{
Name = this.TextBoxName.Text,
Number = this.TextBoxNumber.Text,
CategoryId = new Guid(this.DropDownListCategory.SelectedValue),
ApplicationId = authenticationContext.ApplicationId,
Description = this.TextBoxDescription.Text,
Manufactory = this.TextBoxManufactory.Text,
CreatedBy = authenticationContext.User.UserId,
CreatedOn = DateTime.Now,
ExtensionDataTypeId = productMetadata != null ?
productMetadata.Id : Guid.Empty
};
if (ProductExtensionDataForm != null)
this.ProductExtensionDataForm.SetObjectPropertiesFromControlValues(p);
ctx.Products.InsertOnSubmit(p);
ProductLog productLog = new ProductLog
{
Product = p,
Body = "The product is created",
LoggedBy = authenticationContext.User.UserId,
LoggedOn = DateTime.Now
};
ctx.ProductLogs.InsertOnSubmit(productLog);
ctx.SubmitChanges();
return p.Id.ToString();
}
}
public override void Update(string entityId)
{
Guid productId = new Guid(entityId);
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
this.Validate(ctx, productId);
Product p = ctx.Products.FirstOrDefault(product => product.Id == productId);
if (p == null)
throw new ValidationException("The product id doesn't exist.");
p.Name = this.TextBoxName.Text;
p.Number = this.TextBoxNumber.Text;
p.CategoryId = new Guid(this.DropDownListCategory.SelectedValue);
p.Description = this.TextBoxDescription.Text;
p.Manufactory = this.TextBoxManufactory.Text;
p.LastUpdatedBy = authenticationContext.User.UserId;
p.LastUpdatedOn = DateTime.Now;
if (this.ProductExtensionDataForm != null)
this.ProductExtensionDataForm.SetObjectPropertiesFromControlValues(p);
ProductLog productLog = new ProductLog
{
Product = p,
Body = "The product is modified...",
LoggedBy = authenticationContext.User.UserId,
LoggedOn = DateTime.Now
};
ctx.ProductLogs.InsertOnSubmit(productLog);
ctx.SubmitChanges();
}
}
private void Validate(ProductManagementDataContext ctx, Guid? productId)
{
using (ValidationScope validation = new ValidationScope())
{
if (string.IsNullOrEmpty(this.DropDownListCategory.SelectedValue))
validation.Error("The product category should not be empty.");
if (string.IsNullOrEmpty(this.TextBoxName.Text))
validation.Error("The product name should not be empty.");
if (string.IsNullOrEmpty(this.TextBoxNumber.Text))
validation.Error("The product number should not be empty.");
Guid productIdValue = productId.HasValue ? productId.Value : Guid.NewGuid();
if (ctx.Products.Count(p => p.Id != productIdValue &&
p.Name == this.TextBoxName.Text) > 0)
validation.Error(@"The product name ""{0}"" does exist.",
this.TextBoxName.Text);
if (ctx.Products.Count(p => p.Id != productIdValue &&
p.Number == this.TextBoxNumber.Text) > 0)
validation.Error(@"The product number ""{0}"" does exist.",
this.TextBoxNumber.Text);
}
}
}
Step 4
Create another ascx template for bulk deletion. The content of the template is quite simple which is used to display a confirmation message.
Are you sure to bulk delete the selected
<asp:Label ID="LabelProductCount" ForeColor="Red" runat="server" /> products?
Step 5
Implement aggregate panel for bulk deleting multiple products. When the method Save
is invoked, the selected products in grid will be passed in automatically by the framework. So the implementation is only focused on operations against the passing entity IDs.
public class ProductBulkDeleteAggregatePanel : AggregatePanelPage
{
private IAuthenticationContext authenticationContext =
SpringContext.Current.GetObject<IAuthenticationContext>();
[Binding]
protected Label LabelProductCount;
public override void OnLoad(IRequestHandler sender, AggregatePanelPageEventArgs e)
{
this.LabelProductCount.Text = e.EntityIdEnumerable.Count().ToString();
}
public override void Save(string commandArgument,
IEnumerable<string> entityIdEnumerable)
{
if (!string.Equals("BulkDelete", commandArgument,
StringComparison.OrdinalIgnoreCase)) return;
using(TransactionScope transactionScope = new TransactionScope())
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
Guid[] productIdArray = entityIdEnumerable.Select
(entityId => new Guid(entityId)).ToArray();
IEnumerable<Product> products = ctx.Products.Where
(p => productIdArray.Contains(p.Id) &&
p.ApplicationId == authenticationContext.ApplicationId);
ctx.Products.DeleteAllOnSubmit(products);
ctx.SubmitChanges();
transactionScope.Complete();
}
}
}
Step 6
XML configure product management web form. The ObjectId
is an unique key of a web form which is used to access the web form as URI: ~/[ObjectId]/DynamicPage.svc.
="1.0" ="utf-8"
<Page xmlns="http://www.rapidwebdev.org/schemas/dynamicpage"
ObjectId="ProductManagement"
Type="ProductManagement.Web.ProductDynamicPage, ProductManagement">
<Title>Product Management</Title>
<PermissionValue>ProductManagement</PermissionValue>
<Panels>
<QueryPanel HeaderText="Query">
<TextBox FieldName="Name" Label="Name: " />
<TextBox FieldName="Number" Label="Number: " />
<ComboBox FieldName="CategoryId"
Label="Category: "
Editable="false"
ForceSelection="true"
FieldValueType="System.Guid">
<DynamicDataSource TextField="Name"
ValueField="Id"
Url="/Services/ConcreteDataService.svc/json/
FindByKeyword?concreteDataType=ProductCategory&limit=50" />
</ComboBox>
</QueryPanel>
<ButtonPanel ButtonAlignment="Left">
<Button CommandArgument="New" Type="Button" Text="Add" />
<Button CommandArgument="BulkDelete" Type="Button" Text="Bulk Delete">
<GridSelectionRequired WarningMessage=
"Please select the deleting products." />
</Button>
<Button CommandArgument="Print" Type="Button" Text="Print" />
<Button CommandArgument="DownloadExcel" Type="Button" Text="Export Excel" />
</ButtonPanel>
<GridViewPanel HeaderText="Query Results"
EntityName="Product"
EnabledCheckBoxField="true"
PageSize="25"
PrimaryKeyFieldName="Id"
DefaultSortField="LastUpdatedOn"
DefaultSortDirection="DESC">
<ViewButton />
<EditButton />
<DeleteButton />
<Fields>
<Field FieldName="Number" HeaderText="Number" />
<Field FieldName="Name" HeaderText="Name" />
<Field FieldName="CategoryId" HeaderText="Category" Width="80">
<Transform-Callback Type=
"RapidWebDev.Platform.Web.DynamicPage.
GridViewFieldValueTransformCallback.ShowConcreteDataName,
RapidWebDev.Platform"/>
</Field>
<Field FieldName="Manufactory" HeaderText="Manufactory" />
<Field FieldName="LastUpdatedBy" HeaderText="Updated By" Align="Center">
<Transform-Callback Type="RapidWebDev.Platform.Web.DynamicPage.
GridViewFieldValueTransformCallback.ShowUserDisplayName,
RapidWebDev.Platform"/>
</Field>
<Field FieldName="LastUpdatedOn" HeaderText="Updated On" Align="Center"
Width="150" />
<RowView FieldName="Description" />
</Fields>
</GridViewPanel>
<DetailPanel HeaderText="Product Detail"
ShowMessageAfterSavedSuccessfully="false">
<Type>ProductManagement.Web.ProductDetailPanel, ProductManagement</Type>
<SkinPath>~/Templates/ProductManagement/Product.ascx</SkinPath>
<SaveAndAddAnotherButton IsFormDefaultButton="true" />
<SaveAndCloseButton />
<CancelButton />
</DetailPanel>
<AggregatePanel HeaderText="Product Bulk Deletion Confirmation"
CommandArgument="BulkDelete"
ShowMessageAfterSavedSuccessfully="true">
<Type>ProductManagement.Web.ProductBulkDeleteAggregatePanel,
ProductManagement</Type>
<SkinPath>~/Templates/ProductManagement/ProductBulkDeleteAggregatePanel.ascx
</SkinPath>
<SaveButton />
<CancelButton />
</AggregatePanel>
</Panels>
</Page>
Finally, the web form works in the URL: http://[host]:[port]/ProductManagement/DynamicPage.svc. There is an existing article introducing the details on implementing a whole product management system in RapidWebDev.
How Permission Works
As we have seen in XML configuration, there is a segment PermissionValue for the web form. Actually there are many permission values derived from the value. E.G. "ProductManagement
" is configured for the web form. "ProductManagement.Add
" is the permission for adding a new product. "ProductManagement.Update
" is the permission for editing an existed product. "ProductManagement.[CommandArgument]
" is the permission for special button and aggregate panel with that command argument. The permission is intelligently controlled by the framework so that developers don't need to do anything.
There is an interface RapidWebDev.UI.IPermissionBridge
which is used for the UI framework to integrate with external systems. The method HasPermission
is invoked by the framework to check whether the current user having the permission on a behavior. The framework checks permission on both rendering UI interactive elements and callback UI implementation.
bool HasPermission(string permissionValue);
Finally
I will introduce the detail of RapidWebDev
UI framework in later articles of the series. UI framework is only an important component of RapidWebDev
solution, but not all.
What's RapidWebDev
Website: http://www.rapidwebdev.org
RapidWebDev
is an infrastructure helps engineers to develop enterprise software solutions in Microsoft .NET easily and productively. It consists of an extendable and maintainable web system architecture with a suite of generic business model, APIs and services as fundamental functionalities needed in development for almost all business solutions. So when engineers develop solutions in RapidWebDev
, they can have a lot of reusable and ready things then they can more focus on business logics implementation. In practice, we can save more than 50% time on developing a high quality and performance business solution than traditional ASP.NET development.
Related Topics