一种开发 Web 窗体的创新架构 - 与 ASP.NET 和 MVC 的比较
本文介绍了一种在企业软件中开发 Web 窗体的创新方法,而非采用 ASP.NET 或 MVC,通过逐步比较开发复杂性、可重用性、性能和可维护性来进行。该方法被实现为 RapidWebDev 的重要 UI 组件。
引言
本文介绍了一种在企业软件中开发 Web 窗体的创新架构,而非采用 ASP.NET 或 MVC,通过逐步比较开发复杂性、可重用性、性能和可维护性来进行。该架构被实现为 RapidWebDev 的一个重要 UI 组件,RapidWebDev 是一个开源的企业软件开发基础设施。
RapidWebDev Uri: http://www.rapidwebdev.org
文章目录
1. 开发复杂性比较
ASP.NET 和 MVC 开发的问题
ASP.NET 和 MVC 开发的复杂性在于,尽管我们可以将 Web 窗体的核心功能概括为查询、列表、详细信息和任何其他自定义操作,但我们仍需要为不同的 Web 窗体编写新的 ASP.NET 模板。然而,在大多数情况下,我们对于查询过滤器、网格列、详细信息面板或其他操作面板的界面要求在 Web 窗体之间并不相同。因此,主页面在这种场景下不起作用。随着为不同 Web 窗体编写的代码越来越多,1.1. 我们很难在系统中统一 Web 窗体的样式,尤其是在团队协作中,例如缩进、标签样式、操作通知、用户交互行为等。
1.2. 我们必须手动处理每一次回发或提交。例如:
- 1.2.1 在提交查询时,我们必须逐个检查每个查询过滤器,并在查询过滤器不为空时将其与查询表达式组合,如下面的代码片段所示。硬编码的查询表达式对于更改不够灵活。即使我们对查询过滤器进行了很小的更改,也需要重新编译解决方案并进行部署。
// for Linq2SQL or ADO.NET EF case
if (!string.IsNullOrEmpty(this.TextBoxQueryByName.Text.Trim()))
q += q.Where(user => user.Name.Contains(this.TextBoxQueryByName.Text.Trim()));
// for nature T-SQL case
if (!string.IsNullOrEmpty(this.TextBoxQueryByName.Text.Trim()))
{
string nameParameter = this.TextBoxQueryByName.Text.Trim().Replace("'", "''");
q += "AND Name LIKE '%" + nameParameter + "%'";
}
- 1.2.2 当我们需要支持网格的分页和排序时,我们面临与查询类似的问题。当用户提交查询、分页或排序时,我们需要为每个操作编写不同的处理程序。在处理程序中,我们还需要组合查询表达式。
// when an user clicks query button, the panel to display detail information for a record should be hidden
protected void OnQueryButtonClick(sender object, EventArgs e)
{
// TODO: execute query
Grid.Visible = true;
DetailPanel.Visible = false;
ApprovePanel.Visible = false;
}
// when an user clicks a edit button in a row of grid,
// the panel to display detail information for a record should be displayed
protected void GridView_RowCommand(object sender, GridViewCommandEventArgs e)
{
if (string.Equals("Edit", e.CommandName, StringComparison.OrdinalIgnoreCase))
{
DetailPanel.Visible = true;
ApprovePanel.Visible = false;
// TODO: Load an entity by e.CommandArgument to controls inner of DetailPanel.
}
}
1.4. 当我们将操作与 Web 窗体中的权限集成时(例如只读、创建、更新、删除和批准权限等),UI 代码会变得更加复杂。我们必须在 Web 窗体的某个地方硬编码,根据当前用户的授权来重定向或隐藏某些操作。我们可以从 HttpModule 或基页面类中抽象出重定向,但这种权限仅适用于页面级别。除了下面的示例代码片段外,我们别无他法在行为级别进行授权。
protected void Page_Load(object sender, EventArgs e)
{
if (user is not authorized)
redirect to Unauthorized page;
if (user has no readonly permission)
redirect to Unauthorized page;
if (user has permission on create)
display create button;
if (user has permission on update/delete)
display update/delete button in each row of grid;
if (user has permission on approve)
display approve button somewhere;
if (user has permission on OperationX)
display OperationX button somewhere;
}
1.5. 几乎不可能为 ASP.NET 页面和控件编写单元测试,这使得我们对 UI 的任何即将到来的更改都感到不自由。这不是 MVC 的问题。
RapidWebDev UI 框架的解决方案
RapidWebDev UI 框架在 ASP.NET 或 MVC 开发方面有不同的思路。该框架预定义了 Web 窗体的通用行为,包括 UI 面板可见性控制和用户操作,这些行为可概括如下:
- 当用户单击查询按钮时,从服务器异步返回的数据将呈现到网格中。
- 网格的每一行都有三个可配置按钮:查看、编辑和删除。当用户单击查看或编辑按钮时,记录将显示在模态详细信息面板中。当用户保存记录的更改时,模态详细信息面板将关闭,网格将自动刷新。当用户单击删除按钮时,在用户在弹出对话框中确认删除后,记录将被删除。
- 当用户在按钮面板中单击添加按钮时,将弹出一个空白的模态详细信息面板用于输入。当用户保存记录的更改时,模态详细信息面板将关闭,网格将自动刷新。
- 当用户单击按钮面板中的自定义按钮时,与按钮相关的聚合面板将以模态对话框的形式弹出。用户可以在聚合面板中确认针对网格中选定记录的自定义操作。在用户确认选定记录的自定义操作后,模态聚合面板将关闭,网格将自动刷新。
当用户在 UI 中执行操作时,框架会调用接口 IDynamicPage、IDetailPanelPage 或 IAggregatePanelPage 已配置实现的与操作相关的回调方法。架构如下:

因此,让我们看看 RapidWebDev UI 框架如何改进 ASP.NET 和 MVC 面临的问题:
1.1. Web 窗体的 UI 通过 XML 配置和模板由框架呈现。因此,软件中 Web 窗体的 UI 样式(如标签、缩进、布局)易于统一,因为框架通过 XML 呈现 UI。并且用户交互由框架预定义和管理,因此 Web 窗体的用户交互行为毫无疑问地得到了统一。
1.2. 查询过滤器由 XML 配置。当用户单击查询按钮时,框架会自动调用带有由点击提交收集的QueryParameter的回调方法。QueryParameter包括查询过滤器、排序和分页信息,这些信息可以智能地转换为查询表达式。因此,我们无需像下面的代码片段那样手动组合查询表达式。通过此设计,我们对于查询过滤器的更改非常灵活,只需修改 XML 配置即可,无需更改任何 CLR 代码。
/// <summary>
/// Query products by parameters.
/// </summary>
/// <param name="parameter"></param>
/// <returns></returns>
public override QueryResults Query(QueryParameter parameter)
{
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
// Initialize a query from LINQ data context
IQueryable<Product> q = from p in ctx.Products
where p.ApplicationId == authenticationContext.ApplicationId
select p;
// QueryParameter is converted to LINQ lambda Where expression
// by a convertion strategy configured in Spring.NET IoC
LinqPredicate predicate = parameter.Expressions.Compile();
if (predicate != null && !string.IsNullOrEmpty(predicate.Expression))
q = q.Where(predicate.Expression, predicate.Parameters);
// QueryParameter is converted to LINQ lambda OrderBy expression
// by a convertion strategy configured in Spring.NET IoC
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, );
}
}
1.3. 我们只需在回调中编写业务逻辑以进行预定义或自定义操作,而无需关心框架何时以及如何调用回调。因此,我们不会像 ASP.NET 或 MVC 那样遇到手动控制面板可见性的麻烦。举个例子,我们为创建实现了一个回调,如下面的示例代码片段所示。我们无需关心记录创建后如何切换面板。通常,Web 窗体开发的可视性是有限的,因此复杂性降低了。最终,开发速度和产品质量得到了提高。
/// <summary>
/// Create a new concrete data from detail panel and return its id.
/// The method needs to create a new entity and set control values to its properties then persist it.
/// </summary>
/// <returns>returns the id of new created concrete data.</returns>
public override string Create()
{
ConcreteDataObject concreteDataObject = new ConcreteDataObject();
concreteDataObject.Name = this.TextBoxName.Text;
concreteDataObject.Value = this.TextBoxValue.Text;
concreteDataObject.Description = this.TextBoxDescription.Text;
concreteDataApi.Save(concreteDataObject);
return concreteDataObject.Id.ToString();
}
1.4. 通过接口 IPermissionBridge 在框架中集成了授权,该接口在 Spring.NET IoC 中配置。例如,为 Web 窗体配置了ProductManagement。ProductManagement.Add是添加新产品的权限。ProductManagement.Update是编辑现有产品的权限。ProductManagement.[CommandArgument]是具有该命令参数的特殊按钮和聚合面板的权限。如果用户没有ProductManagement.Add权限,则Add按钮对用户不可见,并且接口 IDetailPanelPage 实现中的相关方法Create受到保护。因此,我们不再需要编写显式的权限检查代码。
1.5. RapidWebDev UI 框架开发的 Web 窗体是单元测试的。让我们看看上面Create方法的测试用例。
[Test, Description("Test the Create Method in ConcreteDataDetailPanel")]
public void TestCreate()
{
ConcreteDataDetailPanel page = new ConcreteDataDetailPanel();
DetailPanelPageProxy proxy = new DetailPanelPageProxy(page);
using (var httpEnv = new HttpEnvironment())
{
httpEnv.SetRequestUrl(@"/ConcreteDataDetailPanel/DynamicPage.svc?ConcreteDataType=Department");
#region Set binding controls of DetailPanel
TextBox TextBoxName = new TextBox();
TextBoxName.Text = "name";
proxy.Set("TextBoxName", TextBoxName);
TextBox TextBoxValue = new TextBox();
TextBoxValue.Text = "value";
proxy.Set("TextBoxValue", TextBoxValue);
TextBox TextBoxDescription = new TextBox();
TextBoxDescription.Text = "description";
proxy.Set("TextBoxDescription", TextBoxDescription);
#endregion
string entityId = proxy.Create();
// Verify the added entity
IConcreteDataApi concreteDataApi = SpringContext.Current.GetObject<IConcreteDataApi>();
ConcreteDataObject obj = concreteDataApi.GetById(new Guid(entityId));
Assert.AreEqual(obj.Name, "name");
}
}
2. 可重用性比较
ASP.NET 和 MVC 开发的问题
ASP.NET 开发的网页很难重用。即使有大量可重用的东西,我们也无法从一个网页继承另一个网页。MVC 将控制器和视图解耦,但视图内的面板混合在一起。一旦我们为带有查询、列表、编辑、查看或任何其他自定义操作面板的 Web 窗体进行了开发,即使我们想只显示单个记录,也难以重用该窗体。举个例子,我们有一个用户管理页面。如果我们想查看系统中的用户配置文件,我们就不能在其他地方重用该页面来仅显示用户配置文件,因为它有查询面板、网格等。
RapidWebDev UI 框架的解决方案
使用 RapidWebDev UI 框架,我们实现接口 IDynamicPage 并为查询和列表编写 XML 配置;实现接口 IDetailPanelPage 并为详细信息面板编写 ascx 模板;实现接口 IAggregatePanelPage 并为聚合面板编写 ascx 模板。Web 窗体的开发被分解为多个单元。这使得我们有可能在其他地方重用 Web 窗体的小单元。RapidWebDev UI 框架中的“可重用性”可概括为:
2.1. 重用详细信息面板以查看单个记录
如果我们已在 Uri:~/ProductManagement/DynamicPage.svc开发了一个产品管理页面,
我们可以通过 Uri:~/ProductManagement/DetailPanel.svc?entityid=792db4cc-3a89-4f9d-94e8-a9293cd27c56&rendermode=View重用该页面以查看详细信息。这实际上重用了整个 Web 窗体的详细信息面板,以直接查看单个记录。
2.2. 在其他 Web 窗体中重用接口 IDynamicPage、IDetailPanelPage 或 IAggregatePanelPage 的实现。如果两个 Web 窗体具有相似的功能需求但布局不同,我们可以仅配置不同的 XML 或 ascx 模板,但使用相同的接口 IDynamicPage、IDetailPanelPage 或 IAggregatePanelPage 的实现。
2.3. 通过继承扩展接口 IDynamicPage、IDetailPanelPage 或 IAggregatePanelPage 的实现。这在企业软件中非常有用,因为业务流程通常由围绕实体的多个过程组成。例如,在装运订单工作流中,我们可能有创建、批准、分发、装运和反馈等过程。但所有这些都围绕着一个订单。使用 RapidWebDev UI 框架,您可能只需要为创建、编辑和查看实现一个订单管理页面。并且只需通过微小更改扩展实现即可用于其他业务流程。
例如,我们有一个管理产品的网页(如上截图所示)。现在我们需要开发另一个审计产品的网页。审计详细信息面板包含单个产品的批准决定和评论,以及只读的产品所有信息。在这种情况下,我们可以重用产品管理的 IDynamicPage 实现来进行查询,并为继承自产品管理实现的 IDetailPanelPage 创建一个新的实现。然后,我们可以重用 Load 方法来加载产品详细信息。
/// <summary>
/// Detail panel page to audit products.
/// When we configure an implementation of IDetailPanelPage or IAggregatePanelPage to a web form,
/// the members with Binding attribute of its base class are resolved from template as well.
/// </summary>
public class ProductAuditDetailPanel : ProductDetailPanel
{
#region 2 new controls only used in audit detail panel
[Binding]
protected DropDownList DropDownListAuditDecision;
[Binding]
protected TextBox TextBoxAuditComment;
#endregion
/// <summary>
/// Load a product into detail panel for audit.
/// </summary>
/// <param name="entityId"></param>
public override void LoadWritableEntity(string entityId)
{
// load the existed audit decision and comment
Guid productId = new Guid(entityId);
using (ProductManagementDataContext ctx = DataContextFactory.Create<ProductManagementDataContext>())
{
Product p = ctx.Products.FirstOrDefault(product => product.Id == productId);
if (p == null)
throw new ValidationException("The product id doesn't exist.");
}
this.DropDownListAuditDecision.SelectedValue = p.AuditDecision;
this.TextBoxAuditComment.Text = p.AuditComment;
// Reuse base LoadReadOnlyEntity to load readonly detail of the product
base.LoadReadOnlyEntity(entityId);
}
/// <summary>
/// Audit a product.
/// </summary>
/// <param name="entityId"></param>
public override void Update(string entityId)
{
Guid productId = new Guid(entityId);
using (ProductManagementDataContext ctx = DataContextFactory.Create<ProductManagementDataContext>())
{
Product p = ctx.Products.FirstOrDefault(product => product.Id == productId);
if (p == null)
throw new ValidationException("The product id doesn't exist.");
p.AuditDecision = this.DropDownListAuditDecision.SelectedValue;
p.AuditComment = this.TextBoxAuditComment.Text;
ctx.SubmitChanges();
}
}
}
3. 性能比较
ASP.NET 和 MVC 开发的问题
如果您有 ASP.NET 和 MVC 网页方面的经验,您应该知道 MVC 网页比同等 ASP.NET 网页快得多。ASP.NET Web 窗体的性能受视图状态、ASP.NET JS 库和回发的限制。Microsoft 为 ASP.NET 提供了一个伪 AJAX UpdatePanel,但它只是开发了类似 AJAX 的 Web 窗体,并没有真正解决性能问题。UpdatePanel 通过 JS 异步将所有表单元素发布到服务器,并用返回的 HTML 内容替换 UpdatePanel 内部的部分。ASP.NET 的局限性仍然存在。
MVC 开发的网页速度很快,没有大的视图状态和缓慢的 ASP.NET JS 库。但是 MVC 的回发遵循原始的 HTTP 提交(GET 或 POST),在频繁的用户交互中效率不高。因此,它是开发网站的完美解决方案,但不适合企业业务软件。
理想情况下,企业软件中 Web 窗体与服务器之间的通信应该是 JSON 数据驱动的。当用户在 Web 窗体中执行任何操作时,Web 窗体仅异步将 JSON 数据发送到服务器,并通过 JS 将返回的 JSON 数据渲染到 Web 窗体中。当然,有大量的 JS 库可以完成这项工作,例如 jquery、ExtJS、YUI 和 Microsoft Atlas。但是我们必须在 ASP.NET Web 窗体/MVC 视图模板中编写大量的 JS 代码,这些代码很难维护且生产力低下。当软件公司在合同压力下时,首选的解决方案是牺牲用户体验来提高生产力。
RapidWebDev UI 框架的解决方案
RapidWebDev UI 框架创建了一种平衡的方法来开发高性能 AJAX Web 窗体,而无需开发人员关心 JS 库。Web 窗体中的用户交互被抽象为框架中的查询、分页、排序、创建、更新、删除和任何其他自定义操作。以下段落通过每次用户交互来介绍 AJAX 在开发的 Web 窗体中如何工作。
查询
当用户单击查询按钮时,Web 窗体仅将查询过滤器异步发送到服务器,并将返回的 JSON 数据渲染到网格中。网格中的分页和排序与查询不同。交互的 Web 服务器响应大小非常小,仅包含 JSON 数据(请参见下面的截图)。
开发人员所做的就是实现一个查询方法并返回记录集合,如下面的代码片段所示。AJAX 请求和渲染完全由框架控制。
/// <summary>
/// Query products by parameters.
/// </summary>
/// <param name="parameter"></param>
/// <returns></returns>
public override QueryResults Query(QueryParameter parameter)
{
// TODO: get the total record count and results
int recordCount = q.Count();
var results = q.Skip(parameter.PageIndex * parameter.PageSize)
.Take(parameter.PageSize).ToList();
return new QueryResults(recordCount, );
}
删除
用户确认删除某条记录后,Web 窗体异步发送 GET 请求到 Web 服务器,并在成功收到通知后刷新网格。如下面的代码片段所示,开发人员只需通过传入的实体 ID 删除记录。所有其他工作都由框架完成,例如请求、网格刷新等。
/// <summary>
/// Delete a concrete data by id.
/// </summary>
/// <param name="entityId"></param>
public override void Delete(string entityId)
{
ConcreteDataObject concreteDataObject = concreteDataApi.GetById(new Guid(entityId));
if (concreteDataObject != null)
{
concreteDataObject.DeleteStatus = DeleteStatus.Deleted;
concreteDataApi.Save(concreteDataObject);
}
}
创建、更新、查看
当用户在详细信息面板中创建、更新或查看记录(如下图所示)时,详细信息面板 ascx 模板实际上是在一个动态生成的独立网页中渲染的,但通过 iframe 集成到主页面中。详细信息面板 ascx 模板自动包装在一个 ASP.NET UpdatePanel 中。当用户“保存”时,详细信息面板包装页面以伪 AJAX 方式进行回发。但该页面仅包含一个用于详细信息的小型 ascx 模板,因此性能也可以接受。详细信息面板保存后,弹出模态对话框将关闭,网格将自动刷新。
任何自定义操作
聚合面板用于自定义操作,其工作方式与详细信息面板相同。下图显示了当用户单击批量删除按钮时,聚合 ascx 模板在模态对话框中渲染为一个动态生成的网页以进行确认。
4. 可维护性比较
由于前面提到的开发复杂性低和可重用性高,RapidWebDev UI 框架开发的 Web 窗体易于维护。特别是,由于用户行为被抽象并按框架的架构进行分类,我们可以轻松地找到要修改的代码块。而且,如果我们更喜欢在 ASP.NET 或 MVC 中开发高效的 AJAX Web 窗体,我们必须编写大量的 JS 代码,这无疑很难维护。
什么是 RapidWebDev
RapidWebDev Uri: http://www.rapidwebdev.org
RapidWebDev 是一个基础设施,可帮助工程师轻松高效地开发 Microsoft .NET 中的企业软件解决方案。它包含一个可扩展且可维护的 Web 系统架构,以及一套通用的业务模型、API 和服务,这些是几乎所有业务解决方案开发所需的基础功能。因此,当工程师在 RapidWebDev 中开发解决方案时,他们会发现许多可重用且现成的功能,从而可以更专注于业务逻辑的实现。实际上,与传统的 ASP.NET 开发相比,我们可以节省 50% 以上的时间来开发高质量、高性能的业务解决方案。