65.9K
CodeProject 正在变化。 阅读更多。
Home

一种开发 Web 窗体的创新架构 - 与 ASP.NET 和 MVC 的比较

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (10投票s)

2010年3月11日

GPL3

13分钟阅读

viewsIcon

52032

本文介绍了一种在企业软件中开发 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 当我们需要支持网格的分页和排序时,我们面临与查询类似的问题。当用户提交查询、分页或排序时,我们需要为每个操作编写不同的处理程序。在处理程序中,我们还需要组合查询表达式。
1.3. 我们很难手动处理和统一 UI 交互行为,如下面的简单代码片段所示。更复杂的情况是,当用户在网格中选择多个记录并点击批量删除按钮时,应该显示一个模态确认面板。在这种情况下,我们必须通过 JS 代码来实现模态面板。这样的代码明显增加了 UI 的复杂性,尤其是在团队协作中,每个成员可能以不同的方式实现行为。
// 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 窗体配置了ProductManagementProductManagement.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 模板在模态对话框中渲染为一个动态生成的网页以进行确认。

因此,RapidWebDev UI 框架提供了一种创新的方法来高效地开发高性能 Web 窗体。

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% 以上的时间来开发高质量、高性能的业务解决方案。 

相关主题

 
© . All rights reserved.