我们为 ASP.NET Core 带来了全新的 WebForms 开发模式,可以让 20 年前的 WebForms 业务代码在最新的 ASP.NET Core 框架中运行,代码相似度99%!
一图胜万言!
将十几年依赖于 WebForms 和 .Net Framework 的项目移植到 ASP.NET Core 将是一项艰巨的任务,特别是对于企业管理系统而言,数百个页面可不是闹着玩的。
为什么要迁移到 ASP.NET Core?
虽然 ASP.NET Core 非常优秀,但最根本的问题却是 WebForms 已经不再更新。
随着时间的推移,WebForms 项目将面临越来越多的安全风险,因此容易受到攻击,维护成本也会越来越高,因为想找到一个熟悉过时技术的开发人员也会越来越难。及时将自己的项目升级到最新的技术是减少系统风险的不二法门。
值得一提的是,ASP.NET Core 性能好是公认的,有报道称 Microsoft Teams 从 .NET Framework 4.6.2 迁移到 .NET Core 3.1,CPU 性能提升 25%。
另有报道,ASP.NET Core性能已经 10 倍于 Node.js,甚至比 Go, C++, Java都要快。
总的来说,ASP.NET Core足够优秀来支撑这次升级:
1. ASP.NET Core开源免费(MIT),信创产品适用。
2. ASP.NET Core跨平台,Linux、Windows、Mac都可以开发和运行。
3. 可以使用最新的 C# 特性,以及最新 VS 带来的效率提升。
4. 更好的性能,意味着更快的访问速度。
5. 更好的安全性。
为了减少大家从 WebForms 升级到最新的 ASP.NET Core 的工作量,我们一直在努力。
2017-12-06,我们正式发布了支持跨平台开发和部署的FineUICore,此时只有经典的Model-View-Controller模式,并且前台页面是Razor函数的写法。如果你当时要从FineUIPro升级到FineUICore,工作量还是蛮大的,来看下直观的对比。
由于 ASP.NET Core Razor视图的写法和标签的写法完全不同,所以前台代码的相似度几乎为零!仅有部分后台业务逻辑是一样的。
2019-06-20,我们推出了支持 Razor Pages 和 Tag Helpers的 FineUICore,可以方便的迁移之前的WebForms应用,这个版本尽量保证 .cshtml 视图文件和 WebForms 的 .aspx 的一致性,可以减轻升级的工作量。
我们专门写了一篇文章详细描述升级过程,可以参考:【FineUICore】全新ASP.NET Core,比WebForms还简单! - 三生石上(FineUI控件) - 博客园
2024年的今天,我们推出支持WebForms开发模式的 FineUICore,不仅可以做到前台页面的高度相似,而且后台业务代码也可以做到99%的相似度。
十几年如一日,我们初心不变,始终恪守如下三个原则,为提升大家的开发体验而不懈努力:
1. 一切为了简单。
2. 用心实现 80% 的功能。
3. 创新所以独一无二。
自从 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(); }
这个示例有两个难点:
而在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的代码量更少,易于维护。
为了解决上述问题,让开发人员在享受 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模式)代码。
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带来的开发效率提升,以及后期节约的维护成本,则是实实在在的好处,真金白金看得见摸得着。
我猜测大家估计还是心有不甘,虽然多点数据传输能提高开发效率,减少我们写的代码量,提高可维护性。但是成年人的世界既要、又要还要,能少传输点数据岂不是更妙。
带着这个疑问,我们来对比下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数据传输量大的缺点还存在吗?
首先确保你使用的是ASP.NET Core RazorPages 开发模式,只需要如下两个步骤即可在FineUICore项目中轻松开启 WebForms 模式。
{ "FineUI": { "EnableWebForms": true, "DebugMode": true, "Theme": "Pure_Black", "EnableAnimation": true, "MobileAdaption": true } }
在 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带来的便利。
在经典WebForms页面中,Page_Load事件非常重要,也是大家耳熟能详的,甚至在20年前ASP.NET 1.0 发布的时候,我们就是这么写代码的。
Page_Load事件往往伴随着对IsPostBack属性的判断,因为Page_Load事件不管是页面第一次加载,还是页面回发都会执行。因此对于哪些只需要在页面第一次加载的代码,就需要放到 !IsPostBack的逻辑判断中。
示例: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>
示例: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函数中无法获取视图中定义的变量。
首先看下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中的不同之处:
如果你是从经典的 ASP.NET WebForms直接学习的FineUICore(WebForms开发模式),忘记上面所有的不同,你只需要记着一点:FineUICore(WebForms模式)的事件处理和经典WebForms的事件处理一模一样!