【WebForms王者归来】在 ASP.NET Core 中运行 WebForms 业务代码,99%相似度!

1. 先说 结论

我们为 ASP.NET Core 带来了全新的 WebForms 开发模式,可以让 20 年前的 WebForms 业务代码在最新的 ASP.NET Core 框架中运行,代码相似度99%!

一图胜万言!

2. 为什么要升级到ASP.NET Core?

将十几年依赖于 WebForms 和 .Net Framework 的项目移植到 ASP.NET Core 将是一项艰巨的任务,特别是对于企业管理系统而言,数百个页面可不是闹着玩的。

经典WebForms已经不再更新

为什么要迁移到 ASP.NET Core?

虽然 ASP.NET Core 非常优秀,但最根本的问题却是 WebForms 已经不再更新。

随着时间的推移,WebForms 项目将面临越来越多的安全风险,因此容易受到攻击,维护成本也会越来越高,因为想找到一个熟悉过时技术的开发人员也会越来越难。及时将自己的项目升级到最新的技术是减少系统风险的不二法门。

ASP .NET Core 的性能好是公认的

值得一提的是,ASP.NET Core 性能好是公认的,有报道称 Microsoft Teams 从 .NET Framework 4.6.2 迁移到 .NET Core 3.1,CPU 性能提升 25%。

  1. Microsoft Teams' journey to .NET Core | .NET
  2. OneService Journey to .NET 6 - .NET Blog

另有报道,ASP.NET Core性能已经 10 倍于 Node.js,甚至比 Go, C++, Java都要快。

How fast is ASP.NET Core?

小结

总的来说,ASP.NET Core足够优秀来支撑这次升级:

  1. ASP.NET Core开源免费(MIT),信创产品适用。

  2. ASP.NET Core跨平台,Linux、Windows、Mac都可以开发和运行。

  3. 可以使用最新的 C# 特性,以及最新 VS 带来的效率提升。

  4. 更好的性能,意味着更快的访问速度。

  5. 更好的安全性。

3. 简化 开发工作 ,我们一直在努力!

为了减少大家从 WebForms 升级到最新的 ASP.NET Core 的工作量,我们一直在努力。

ASP.NET Core - MVC开发模式

2017-12-06,我们正式发布了支持跨平台开发和部署的FineUICore,此时只有经典的Model-View-Controller模式,并且前台页面是Razor函数的写法。如果你当时要从FineUIPro升级到FineUICore,工作量还是蛮大的,来看下直观的对比。

由于 ASP.NET Core Razor视图的写法和标签的写法完全不同,所以前台代码的相似度几乎为零!仅有部分后台业务逻辑是一样的。

ASP.NET Core - RazorPages开发模式

2019-06-20,我们推出了支持 Razor Pages 和 Tag Helpers的 FineUICore,可以方便的迁移之前的WebForms应用,这个版本尽量保证 .cshtml 视图文件和 WebForms 的 .aspx 的一致性,可以减轻升级的工作量。

我们专门写了一篇文章详细描述升级过程,可以参考:【FineUICore】全新ASP.NET Core,比WebForms还简单! - 三生石上(FineUI控件) - 博客园

ASP.NET Core - WebForms开发模式

2024年的今天,我们推出支持WebForms开发模式的 FineUICore,不仅可以做到前台页面的高度相似,而且后台业务代码也可以做到99%的相似度。

小结

十几年如一日,我们初心不变,始终恪守如下三个原则,为提升大家的开发体验而不懈努力:

  1. 一切为了简单。

  2. 用心实现 80% 的功能。

  3. 创新所以独一无二。

4. 为什么引入 WebForms 开发模式?

自从 2019年推出支持 RazorPages 的FineUICore以来,我们不断收到用户反馈,吐槽 ASP.NET Core 的使用复杂,没有之前的 WebForms好用。

我简单总结了一下,有人吐糟传递参数麻烦,还要自己写JavaScript代码;有人吐槽后台代码的一致;还有人搞不清楚UIHelper该什么时间使用,以及创建的控件和页面上的控件实例有啥关系。

初始化数据的方式不同

ASP.NET Core 中,我们需要在 OnGet 函数中初始化数据,然后通过 ViewData 传入视图文件:

复制代码
public void OnGet()
{
	LoadData();
}

private void LoadData()
{
	var recordCount = DataSourceUtil.GetTotalCount();
	// 1.设置总项数(特别注意:数据库分页初始化时,一定要设置总记录数RecordCount)
	ViewBag.Grid1RecordCount = recordCount;
	// 2.获取当前分页数据
	ViewBag.Grid1DataSource = DataSourceUtil.GetPagedDataTable(pageIndex: 0, pageSize: 5);
}

而在WebForms的 Page_Load 中,我们可以直接获取表格控件进行数据绑定:

复制代码
protected void Page_Load(object sender, EventArgs e)
{
	if (!IsPostBack)
	{
		BindGrid();
	}
}

private void BindGrid()
{
	// 1.设置总项数(特别注意:数据库分页一定要设置总记录数RecordCount)
	Grid1.RecordCount = GetTotalCount();
	// 2.获取当前分页数据
	DataTable table = GetPagedDataTable(Grid1.PageIndex, Grid1.PageSize);
	// 3.绑定到Grid
	Grid1.DataSource = table;
	Grid1.DataBind();
}

向后台传递数据的方式不同

ASP.NET Core 中,所有后台拿到的数据都需要在视图代码中通过JavaScript的方式获取:

复制代码
<f:Button ID="btnSubmit" CssClass="marginr" ValidateForms="SimpleForm1" Text="登录" 
OnClick="@Url.Handler("btnSubmit_Click")" 
OnClickParameter1="@(new Parameter("userName", "F.ui.tbxUserName.getValue()"))" 
OnClickParameter2="@(new Parameter("password", "F.ui.tbxPassword.getValue()"))">
</f:Button>

比如这个示例向后台传递了两个参数userName和password,后台通过函数参数的方式接受:

复制代码
public IActionResult OnPostBtnSubmit_Click(string userName, string password)
{
	UIHelper.Label("labResult").Text("用户名:" + userName + " 密码:" + password);
	return UIHelper.Result();
}

这个示例有两个难点:

  1. 代码抽象不好理解:通过UIHelper.Label函数拿到的控件是一个在内存中新建的实例(其目的是为了向前台输出一段改变标签控件文本的JavaScript脚本),和页面初始化时的那个Label控件没有任何关系。
  2. 不小心写错参数名称的话,编译不会报错,运行时不能正确获取传入的参数值。

而在WebForms中,可以直接在后台获取控件的属性,无需任何特殊处理:

复制代码
<f:Button ID="btnSubmit" CssClass="marginr" runat="server" OnClick="btnSubmit_Click" ValidateForms="SimpleForm1" Text="登录">
</f:Button>

后台直接通过控件实例的属性获取,可以直接通过智能提示快速输入属性名称,而且有编译时提示:

复制代码
protected void btnSubmit_Click(object sender, EventArgs e)
{
	labResult.Text = "用户名:" + tbxUserName.Text + " 密码:" + tbxPassword.Text;
}

回发时数据处理方式不同

ASP.NET Core 中,后台更新表格数据需要一套单独的代码(因为页面初始化时使用ViewData进行数据传递,所以无法和回发时的数据绑定共用一套代码):

复制代码
public IActionResult OnPostGrid1_PageIndexChanged(string[] Grid1_fields, int Grid1_pageIndex)
{
	var grid1 = UIHelper.Grid("Grid1");
	var recordCount = DataSourceUtil.GetTotalCount();
	// 1.设置总项数(数据库分页回发时,如果总记录数不变,可以不设置RecordCount)
	grid1.RecordCount(recordCount);
	// 2.获取当前分页数据
	var dataSource = DataSourceUtil.GetPagedDataTable(pageIndex: Grid1_pageIndex, pageSize: 5);
	grid1.DataSource(dataSource, Grid1_fields);
	return UIHelper.Result();
}

而在WebForms中,页面回发时重新绑定表格数据和页面初始化时共用一套代码:

复制代码
protected void Grid1_PageIndexChange(object sender, GridPageEventArgs e)
{
	BindGrid();
}

小结

经过前面的对比,我们能明显感觉到WebForms的代码更加直观,更加容易理解,并且WebForms的代码量更少,易于维护。

5. 全新WebForms开发模式 全球首创

全球首创,实至名归

为了解决上述问题,让开发人员在享受 ASP.NET Core 免费开源跨平台速度快的优点同时,还能拥有WebForms比较高的开发效率,我们为 ASP.NET Core 引入了 WebForms 模式。

截止目前,能真正将 WebForms 引入 ASP.NET Core 的控件库厂商仅此一家,别无分店。我们也诚挚的邀请你来试用,相信你一定会喜欢这个全球首创的创新功能。

视图文件+页面模型文件+自动生成的设计时文件

首先从一个最简单的页面入手,我们来看下启用WebForms的 ASP.NET Core 到底是个什么样子?

一个简单的模拟登录页面,用户输入指定的用户名和密码之后,弹出登录成功提示框。

ASP.NET Core RazorPages项目中,我们需要新建一个页面文件以及后台代码文件(或者称之为页面模型):

注意,在登录按钮的点击事件中,可以直接读取输入框的 tbxUserName 的 Text 属性,这个就是 FineUICore 黑魔法,我们会将控件的一些关键属性回发到后台,并自动绑定到相应的控件实例。

而这个控件实例(tbxUserName)是在一个名为 Login.cshtml.designer.cs 文件中声明的,FineUICore会在页面回发时自动初始化这个实例,并绑定关键属性值。

注:我们会提供一个Visual Studio插件自动生成这个文件,无需开发人员手工编写。

小结

如果上述代码让你想起了20年前的WebForms,那就对了。业务代码99%的相似度是实打实的,这也就为经典WebForms的项目迁移到最新的ASP.NET Core奠定了扎实的基础。

让我们用工具对比下实现相同功能的经典WebForms和FineUICore(开启WebForms模式)代码。

6. 哪些所谓的WebForms缺点怎么办?

WebForms的缺点已经不复存在!

20年前大家所诟病的WebForms的缺点之一(网络传输量大)已经不复存在,而WebForms的快速开发特性(Rapid Application Development - RAD)却越来越重要。

有报告显示,今天的主流网站的网页过于臃肿,以至于严重影响浏览性能,而能流畅玩手游《绝地求生》的入门级移动设备甚至难以正常加载。Wix 每个网页需要加载 21MB,Patreon 和 Threads 每个网页需要加载 13MB 的数据。臃肿的网页导致加载时间长达 33 秒,部分情况下甚至无法加载。基本上主流社交平台都存在臃肿的问题。而内容创建平台 Squarespace 和论坛 Discourse 的新版本通常比旧版本性能更差。

How web bloat impacts users with slow devices

WebForms需要在客户端和服务端保持控件状态,所以在页面回发时,需要将页面上所有控件的状态信息一并回发,导致比较大的网络传输。20年后的今天,随着4G、5G移动网络的普及,以及充足的宽带网络,这些流量已经变得不值一提。

WebForms是划时代的技术,也可以看做是微软的低代码解决方案,只不过20年前出来太超前了,受制于网络传输带宽的限制,所以才为大家所诟病。现在回头看看,每次页面回发时多传输10K数据算个事吗?想想你刷一个抖音视频怎么说也要消耗10M(10,240K)流量吧。而WebForms带来的开发效率提升,以及后期节约的维护成本,则是实实在在的好处,真金白金看得见摸得着。

实测WebForms的数据传输量

我猜测大家估计还是心有不甘,虽然多点数据传输能提高开发效率,减少我们写的代码量,提高可维护性。但是成年人的世界既要、又要还要,能少传输点数据岂不是更妙。

带着这个疑问,我们来对比下FineUICore(RazorPages)、FineUIPro(经典WebForms)和FineUICore(WebForms开发模式)下传输的数据量,争取让大家用的心情舒畅。

示例一:表格的数据库分页与排序

示例二:省市县联动

示例三:树控件延迟加载

注:上述表格中数字表示网络数据传输量,单位KB。

经过上述三个页面对比,我们可以看出,经典WebForms不管是页面第一次加载,还是回发时上传和下载的数据量都是最大的。

小结

  1. 相比经典WebForms,不管是页面第一次加载,还是回发时的数据传输量,ASP.NET Core(WebForms开发模式)都是碾压级的,综合数据下载量比经典WebForms减少 50% 左右。

  2. 与数据传输量最少的ASP.NET Core RazorPages相比,启用WebForms时,只有在页面回发时上传数据量有所增加,而页面第一次加载和回发时的下载数据量两者保持一致。

  3. 不管哪种技术,上述三个示例的数据传输都是10KB之内,相比现在动辄10MB(大了1000倍!)的数据传输,你觉得WebForms数据传输量大的缺点还存在吗?

7. 如何开启WebForms开发模式?

首先确保你使用的是ASP.NET Core RazorPages 开发模式,只需要如下两个步骤即可在FineUICore项目中轻松开启 WebForms 模式。

第一步:修改appsettings.json配置文件

复制代码
{
  "FineUI": {
    "EnableWebForms": true,
    "DebugMode": true,
    "Theme": "Pure_Black",
    "EnableAnimation": true,
    "MobileAdaption": true
  }
}

第二步:修改 Startu p.cs 启动文件

在 ConfigureServices 函数中,增加 WebForms过滤器,如下所示。

复制代码
// FineUI 服务
services.AddFineUI(Configuration);

services.AddRazorPages().AddMvcOptions(options =>
{
    // 自定义JSON模型绑定(添加到最开始的位置)
    options.ModelBinderProviders.Insert(0, new FineUICore.JsonModelBinderProvider());

    // 自定义WebForms过滤器(仅在启用EnableWebForms时有效)
    options.Filters.Insert(0, new FineUICore.WebFormsFilter());

}).AddNewtonsoftJson().AddRazorRuntimeCompilation();

搞定!

小结

深度集成到FineUICore中,仅仅通过一个参数来控制是否开启WebForms,可以对比学习RazorPages和WebForms,降低了学习成本,同时也让之前购买FineUICore企业版的客户享受到WebForms带来的便利。

8. Page_Load事件的回归

在经典WebForms页面中,Page_Load事件非常重要,也是大家耳熟能详的,甚至在20年前ASP.NET 1.0 发布的时候,我们就是这么写代码的。

Page_Load事件往往伴随着对IsPostBack属性的判断,因为Page_Load事件不管是页面第一次加载,还是页面回发都会执行。因此对于哪些只需要在页面第一次加载的代码,就需要放到 !IsPostBack的逻辑判断中。

R azorPages中的 复选框列表的初始化

示例:https://pages.fineui.com/#/Form/CheckBoxList

在ASP.NET Core RazorPages开发模式下,我们需要在OnGet中初始化数据,由于此时页面视图尚未初始化,因此我们无法知道页面视图上的任何定义。

复制代码
public void OnGet()
{
	LoadData();
}

private void LoadData()
{
	List<TestClass> myList = new List<TestClass>();
	myList.Add(new TestClass("1", "数据绑定值 1"));
	myList.Add(new TestClass("2", "数据绑定值 2"));
	myList.Add(new TestClass("3", "数据绑定值 3"));
	myList.Add(new TestClass("4", "数据绑定值 4"));

	ViewBag.CheckBoxList2DataSource = myList;
	ViewBag.CheckBoxList2SelectedValueArray = new string[] { "1", "3" };
}

将准备好的数据保存在ViewData(自定义的ViewBag)中,然后传入视图文件,并在页面视图标签中使用这些数据。

复制代码
<f:CheckBoxList ID="CheckBoxList2" Label="列表二(一列)" ColumnNumber="1"
				DataTextField="Name" DataValueField="Id" 
DataSource="@ViewBag.CheckBoxList2DataSource"
				SelectedValueArray="@ViewBag.CheckBoxList2SelectedValueArray">
</f:CheckBoxList>

WebForms 复选框列表的初始化

示例:https://forms.fineui.com/#/Form/CheckBoxList

复制代码
protected void Page_Load(object sender, EventArgs e)
{
	if (!IsPostBack)
	{
		LoadData();
	}
}

private void LoadData()
{
	List<TestClass> myList = new List<TestClass>();
	myList.Add(new TestClass("1", "数据绑定值 1"));
	myList.Add(new TestClass("2", "数据绑定值 2"));
	myList.Add(new TestClass("3", "数据绑定值 3"));
	myList.Add(new TestClass("4", "数据绑定值 4"));

	CheckBoxList2.DataSource = myList;
	CheckBoxList2.DataBind();

	CheckBoxList2.SelectedValueArray = new string[] { "1", "3" };
}

其中,IsPostBack属性定义在页面模型基类BaseModel.cs中:

复制代码
public bool IsPostBack
{
    get
    {
        return FineUICore.PageContext.IsFineUIAjaxPostBack();
    }
}

注意:在Page_Load事件中,页面视图已经初始化完毕,因此我们可以直接调用页面视图上的控件实例,比如这里的CheckBoxList2,对应于页面上的CheckBoxList标签定义。

复制代码
<f:CheckBoxList ID="CheckBoxList2" Label="列表二(一列)" ColumnNumber="1"
				DataTextField="Name" DataValueField="Id">
</f:CheckBoxList>

小结

从上面示例中可以看出,WebForms模式下的页面初始化更加直观,等视图文件初始化完毕后,直接获取控件实例,并设置控件属性。反过来看RazorPages的实现就有点繁琐了,必须通过ViewData进行中转,先赋值,再使用,在页面模型OnGet函数中无法获取视图中定义的变量。

9. 页面回发事件(PostBack)

简化页面回发事件的函数名

首先看下RazorPages中的按钮点击事件,:

复制代码
<f:Button ID="btnChangeEnable" Text="启用后面的按钮" 
		OnClick="@Url.Handler("btnChangeEnable_Click")" />
<f:Button ID="btnEnable" Text="禁用的按钮" OnClick="@Url.Handler("btnEnable_Click")" 
		Enabled="false" />

对应的后台事件处理器:

复制代码
public IActionResult OnPostBtnChangeEnable_Click()
{
	var btnEnable = UIHelper.Button("btnEnable");
	btnEnable.Enabled(true);
	btnEnable.Text("本按钮已经启用(点击弹出对话框)");
	return UIHelper.Result();
}

在视图文件中,定义了按钮的点击事件名为btnChangeEnable_Click,而后台对应的事件处理器名称为OnPostBtnChangeEnable_Click。由于前后台事件名称的不一致,导致很多开发人员将后台事件名称误写为OnPostbtnChangeEnable_Click,导致无法进入事件处理函数。

而WebForms开发模式下,再看下相同的示例

复制代码
<f:Button ID="btnChangeEnable" Text="启用后面的按钮" 
	OnClick="btnChangeEnable_Click" />
<f:Button ID="btnEnable" Text="禁用的按钮" OnClick="btnEnable_Click" 
	Enabled="false" />

对应的后台处理函数名称和前台的定义一模一样:

复制代码
protected void btnChangeEnable_Click(object sender, EventArgs e)
{
	btnEnable.Enabled = true;
	btnEnable.Text = "本按钮已经启用(点击弹出对话框)";
}

除了事件名称保持前后台一致,代码逻辑中已经完全移除UIHelper的调用,我们可以直接调用控件实例,修改实例属性(并非所有属性都可以在页面回发中改变,我们将这些能够在回发中改变的属性为AJAX属性,这个概念和经典FineUIPro保持一致)。

.......

小结

下面简单总结一下WebForms模式下回发事件和RazorPages中的不同之处:

  1. 视图代码中无需将事件名称置于Url.Handler()函数中。
  2. 视图代码中无需编写JavaScript代码来获取控件状态。
  3. 视图中也无需设置OnClickFields来向后台传递控件状态。
  4. 后台事件名称和前台视图定义的事件名称完全一致。
  5. 事件处理函数的返回值是void,因此无需返回UIHelper.Result()。
  6. 事件处理函数参数和经典的WebForms保持一致,第一个参数是触发事件的控件实例,第二个是事件参数(比如表格分页的事件参数类型为GridPageEventArgs)。
  7. 事件处理函数中完全移除对UIHelper的依赖(之前需要重建控件实例,比如UIHelper.Button("btnEnable"))。

如果你是从经典的 ASP.NET WebForms直接学习的FineUICore(WebForms开发模式),忘记上面所有的不同,你只需要记着一点:FineUICore(WebForms模式)的事件处理和经典WebForms的事件处理一模一样

FineUICore(WebForms开发模式)现已开放企业版试用申请!
请填写如下资料发送到邮箱:2877408506@qq.com

  • 产品名称:FineUICore(WebForms开发模式)
  • 单位全称:XXX单位
  • 申请人邮箱:XXX
  • 申请人QQ:XXX
  • 申请人姓名:XXX
  • 申请人地址:XX省XX市
    =================================
    全球首创,让你 20 年前习得的编程技能再用 20 年!

下载完整技术白皮书