整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

TinyMCE 编辑邮件模板 FineUICore

TinyMCE 编辑邮件模板 FineUICore

天设计发送邮件,需要设计邮件模板,将动态的字段放入模板里,使用了TinyMCE今天来分享一下;

测试环境:

FineUICore 8.1 (前端的应该不区别FineUI版本)

TinyMCE版本 5.10.0 (下面会写升级方法)

先上图


升级TinyMCE

FineUI自带的好像是4.X,升级我之前发过帖子,需要下载最新的包,覆盖就行了(\wwwroot\res\third-party\tinymce\)

不升级的话会有BUG,插入的字段会跑偏;

升级之后的话会有BUG,高度不会自适应,需要指定高度,这个倒是无所谓,等我修复了再放出来;

TinyMCE的参数

TinyMCE的参数通过 .Options( 去设置,参考 http://tinymce.ax-z.cn/ 就可以了,

比如设置中文

F.OptionItem().Key("language").Value("zh_CN").PersistOriginal(false),

PersistOriginal 表示是否原样输出,如果为false会当作字符串,比如输出 false ,就为true就好了,

完整配置和注释如下

.Options(

//语言

F.OptionItem().Key("language").Value("zh_CN").PersistOriginal(false),

//自定义css路径

F.OptionItem().Key("content_css").Value(Url.Content("~/res/css/FineUIOvereide.css")).PersistOriginal(false),

//不要上面的菜单

F.OptionItem().Key("menubar").Value("false").PersistOriginal(true),

//最小高度

F.OptionItem().Key("min_height").Value("500").PersistOriginal(true),

//插件选择

F.OptionItem().Key("plugins").Value("['link','code','textcolor colorpicker ','noneditable importcss autoresize']").PersistOriginal(true),

//工具条

F.OptionItem().Key("toolbar1").Value("bold italic underline strikethrough | numlist bullist | alignleft aligncenter alignright | link unlink | code"),

//作为标签的class类

F.OptionItem().Key("noneditable_noneditable_class").Value("tag").PersistOriginal(false)

)

TinyMCE的插件

要实现标签功能,需要用到 两个插件 [noneditable不可编辑插件](http://tinymce.ax-z.cn/plugins/noneditable.php) 和 [importcss引用css插件](http://tinymce.ax-z.cn/plugins/importcss.php),放到plugins 最后就行了;这里我设置了 noneditable_noneditable_class ,就是这个css 将作为 不可编辑的标记;

如果没升级 TinyMCE 出现以下情况

插入焦点会很奇怪;

核心实现 当然是JS

直接上代码,看注释

//一个字段的合集 [{id,value},{id,value}]
const fields=@Html.Raw(ViewBag.myFields);

F.ready(function () {
    fields.forEach(item=> {
        //Label 
        let tag=new F.Label({
            cls: "tag",
            hideLabel: true,
            value: item.value,
            renderTo: F.ui.field.bodyEl,
        });
        //点击事件
        tag.el.on("click", ()=> {
            //拼一个标签 把值塞进去
            let span=`<span class="tag" id="${item.id}" style="height:20px;line-height:20px;margin:0px 5px;">${item.value}</span>`;
            //注意获取编辑控件的用法
            F.ui.HtmlEditor1.getEditorInstance().insertContent(span);
        });
    })
});

tag 是我自己写的样式,不放出来了,30RMB

到这就能插入了;

后台

后台需要保存,发送邮件时需要替换标签,这里用的正则替换,也放出来

myFields.ForEach(item=>
{
    var v="";
    //如果有数据
    if (data.ContainsKey(item.id))
    {
        v=data[item.id].ToString();
    }
    //正则替换模板
    var regEx_style=$"<span[^>]*id=\"{item.id}\"[^>]*?>[\\s\\S]*?<\\/span>";
    temp=Regex.Replace(temp, regEx_style, v);
});

这里的span 要和前台的 span标签一致,否则找不到,属于约定,也可以用一个参数控制;

源码

6.1 前台代码

@{
    ViewBag.Title="没想好";
    var F=Html.F();
}

@section head {
}

@section body {
    @(
    F.Panel().NoBorderAndHeader().Layout(LayoutType.HBox).IsViewPort(true)
    .Items(
        F.Panel().ID("field").BoxFlex(1).Title("字段").Toolbars(F.Toolbar().Items(
            F.Button().Text("测试").OnClick(Url.Action("test"), new Parameter("temp", "F.ui.HtmlEditor1.getValue()"))
            )),
        F.HtmlEditor().BoxFlex(3).ID("HtmlEditor1").Editor(Editor.TinyMCE).LabelAlign(LabelAlign.Top)
                .BasePath(Url.Content("~/res/third-party/tinymce/")).ToolbarSet(EditorToolbarSet.Full)
                //.Height(600)
                .Options(
                    F.OptionItem().Key("language").Value("zh_CN").PersistOriginal(false),
                    F.OptionItem().Key("content_css").Value(Url.Content("~/res/css/FineUIOvereide.css")).PersistOriginal(false),
                    F.OptionItem().Key("menubar").Value("false").PersistOriginal(true),
                    F.OptionItem().Key("min_height").Value("500").PersistOriginal(true),
                    F.OptionItem().Key("plugins").Value("['link','code','textcolor colorpicker ','noneditable importcss autoresize']").PersistOriginal(true),
                    F.OptionItem().Key("toolbar1").Value("bold italic underline strikethrough | numlist bullist | alignleft aligncenter alignright | link unlink | code"),
                    F.OptionItem().Key("noneditable_noneditable_class").Value("tag").PersistOriginal(false)
                )
        )
  )
}

@section script {
    <script>
        const fields=@Html.Raw(ViewBag.myFields);
        F.ready(function () {
            fields.forEach(item=> {
                let tag=new F.Label({
                    cls: "tag",
                    hideLabel: true,
                    value: item.value,
                    renderTo: F.ui.field.bodyEl,
                });
                
                tag.el.on("click", ()=> {
                    let span=`<span class="tag" id="${item.id}" style="height:20px;line-height:20px;margin:0px 5px;">${item.value}</span>`;
                    F.ui.HtmlEditor1.getEditorInstance().insertContent(span);
                });
            })
        });
    </script>
}

6.2 后台代码

using FineUICore.EmptyProject.Controllers;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace FineUICoreEx.Demo.Areas.Other.Controllers
{
    [Area("Other")]
    public class TinyMCETestController : BaseController
    {
        public IActionResult Index()
        {
            List<myField> myFields=GetmyFields();
            ViewBag.myFields=JArray.FromObject(myFields).ToString(Formatting.None);
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> test(string temp)
        {
            JObject data=new JObject();
            GetData().ForEach(item=>
            {
                data.Add(item.id, item.value);
            });
            List<myField> myFields=GetmyFields();
            myFields.ForEach(item=>
            {
                var v="";
                if (data.ContainsKey(item.id))
                {
                    v=data[item.id].ToString();
                }
                var regEx_style=$"<span[^>]*id=\"{item.id}\"[^>]*?>[\\s\\S]*?<\\/span>";
                temp=Regex.Replace(temp, regEx_style, v);
            });

            ShowAlert(temp);
            return FineUICore.UIHelper.Result();
        }

        private List<myField> GetData()
        {
            return new List<myField>() {
                new myField() { id="date",value=DateTime.Now.ToString("yyyy-MM-dd") },
                new myField() { id="name",value="我不是标题" },
                new myField() { id="content",value="内容22223423<br/>内容22223423" },
            };
        }

        public dynamic GetmyFields()
        {
            return new List<myField>() {
                new myField() { id="date",value="日期" },
                new myField() { id="name",value="姓名" },
                new myField() { id="title",value="标题" },
                new myField() { id="content",value="内容" },
            };
        }
    }

    public class myField
    {
        public string id { get; set; }
        public string value { get; set; }
    }
}

效果图

---

后记和问题

问题1

目前遇到的问题是

F.ui.HtmlEditor2.getEditorInstance() 页面初始时会获取不到,需要延迟

问题2

另外页面初始时回发,会有一个

n.beforeAjax(function () {
    t.oIlO0.initialized && t.oIlO0.save()
})

这个会报错,不要回发或者延迟回发就好了 `绝对是BUG`

代码断

. 先说结论

我们为 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 传入视图文件:

1

2

3

4

5

6

7

8

9

10

11

12

13

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 中,我们可以直接获取表格控件进行数据绑定:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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的方式获取:

1

2

3

4

5

<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,后台通过函数参数的方式接受:

1

2

3

4

5

public IActionResult OnPostBtnSubmit_Click(string userName, string password)

{

UIHelper.Label("labResult").Text("用户名:" + userName + " 密码:" + password);

return UIHelper.Result();

}

这个示例有两个难点:

  1. 代码抽象不好理解:通过UIHelper.Label函数拿到的控件是一个在内存中新建的实例(其目的是为了向前台输出一段改变标签控件文本的JavaScript脚本),和页面初始化时的那个Label控件没有任何关系。

  2. 不小心写错参数名称的话,编译不会报错,运行时不能正确获取传入的参数值。

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

1

2

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

</f:Button>

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

1

2

3

4

protected void btnSubmit_Click(object sender, EventArgs e)

{

labResult.Text="用户名:" + tbxUserName.Text + " 密码:" + tbxPassword.Text;

}

回发时数据处理方式不同

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

1

2

3

4

5

6

7

8

9

10

11

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中,页面回发时重新绑定表格数据和页面初始化时共用一套代码:

1

2

3

4

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 CoreWebForms开发模式)都是碾压级的,综合数据下载量比经典WebForms减少 50% 左右。

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

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

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

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

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

1

2

3

4

5

6

7

8

9

{

"FineUI": {

"EnableWebForms": true,

"DebugMode": true,

"Theme": "Pure_Black",

"EnableAnimation": true,

"MobileAdaption": true

}

}

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

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

1

2

3

4

5

6

7

8

9

10

11

12

// 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,可以对比学习RazorPagesWebForms,降低了学习成本,同时也让之前购买FineUICore企业版的客户享受到WebForms带来的便利。

8. Page_Load事件的回归

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

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

RazorPages中的复选框列表的初始化

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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)中,然后传入视图文件,并在页面视图标签中使用这些数据。

1

2

3

4

5

<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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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中:

1

2

3

4

5

6

7

public bool IsPostBack

{

get

{

return FineUICore.PageContext.IsFineUIAjaxPostBack();

}

}

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

1

2

3

<f:CheckBoxList ID="CheckBoxList2" Label="列表二(一列)" ColumnNumber="1"

DataTextField="Name" DataValueField="Id">

</f:CheckBoxList>

小结

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

9. 页面回发事件(PostBack

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

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

1

2

3

4

<f:Button ID="btnChangeEnable" Text="启用后面的按钮"

OnClick="@Url.Handler("btnChangeEnable_Click")" />

<f:Button ID="btnEnable" Text="禁用的按钮" OnClick="@Url.Handler("btnEnable_Click")"

Enabled="false" />

对应的后台事件处理器:

1

2

3

4

5

6

7

public IActionResult OnPostBtnChangeEnable_Click()

{

var btnEnable=UIHelper.Button("btnEnable");

btnEnable.Enabled(true);

btnEnable.Text("本按钮已经启用(点击弹出对话框)");

return UIHelper.Result();

}

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

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

1

2

3

4

<f:Button ID="btnChangeEnable" Text="启用后面的按钮"

OnClick="btnChangeEnable_Click" />

<f:Button ID="btnEnable" Text="禁用的按钮" OnClick="btnEnable_Click"

Enabled="false" />

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

1

2

3

4

5

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的事件处理一模一样

帆软fineReport为例,它本质上是一个Web项目,自然少不了相关的servlet,filter等相关概念。

第一步:一个要求登录的请求当没有登录时,如果重定向到登录请求的。

帆软报表里的每个请求都会进入到com.fr.third.springframework.web.servlet.DispatcherServlet的doDispatch中。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest=request;
        HandlerExecutionChain mappedHandler=null;
        boolean multipartRequestParsed=false;
        WebAsyncManager asyncManager=WebAsyncUtils.getAsyncManager(request);

        try {
            try {
                ModelAndView mv=null;
                Exception dispatchException=null;

                try {
                    processedRequest=this.checkMultipart(request);
                    multipartRequestParsed=processedRequest !=request;
                    mappedHandler=this.getHandler(processedRequest);
                    if (mappedHandler==null || mappedHandler.getHandler()==null) {
                        this.noHandlerFound(processedRequest, response);
                        return;
                    }

                    HandlerAdapter ha=this.getHandlerAdapter(mappedHandler.getHandler());
                    String method=request.getMethod();
                    boolean isGet="GET".equals(method);
                    if (isGet || "HEAD".equals(method)) {
                        long lastModified=ha.getLastModified(request, mappedHandler.getHandler());
                        if (this.logger.isDebugEnabled()) {
                            this.logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                        }

                        if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                            return;
                        }
                    }

                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }

                    try {
                        mv=ha.handle(processedRequest, response, mappedHandler.getHandler());
                    } finally {
                        if (asyncManager.isConcurrentHandlingStarted()) {
                            return;
                        }

                    }

                    this.applyDefaultViewName(request, mv);
                    mappedHandler.applyPostHandle(processedRequest, response, mv);
                } catch (Exception var27) {
                    dispatchException=var27;
                }

                this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
            } catch (Exception var28) {
                this.triggerAfterCompletion(processedRequest, response, mappedHandler, var28);
            } catch (Error var29) {
                this.triggerAfterCompletionWithError(processedRequest, response, mappedHandler, var29);
            }

        } finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                return;
            } else {
                if (multipartRequestParsed) {
                    this.cleanupMultipart(processedRequest);
                }

            }
        }
    }

1 if (!mappedHandler.applyPreHandle(processedRequest, response)) 这里 会进入到com.fr.decision.webservice.interceptor.preHandle方法,如果没通过后面就不走下去了。



public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        EventDispatcher.fire(RequestBeginEndEvent.REQUEST_BEGIN, new RequestBeginEndEventInfo(request, response));
        HandlerMethod handlerMethod=(HandlerMethod)handler;
        RequestChecker checker=PreHandlerFactory.getInstance().getRequestChecker(request, handlerMethod);
        return checker.checkRequest(request, response, handlerMethod);
    }

PreHandlerFactory.getInstance().getRequestChecker(request, handlerMethod); 默认会返回com.fr.decision.webservice.interceptor.handler.DecisionRequestChecker对象,如果我们想要改变DecisionRequestChecker的逻辑,可以使用 PreHandlerFactory.getInstance().registerRequestCheckers添加我们自己的RequestChecker,

看看DecisionRequestChecker的checkRequest方法:

public boolean checkRequest(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        LoginStatusValidator loginStatusValidator=this.getLoginStatusValidator(handlerMethod);
        if (!loginStatusValidator.isNeedCheck()) {
            return true;
        } else if (!SystemService.getInstance().checkSystemInit()) {
            PreHandlerFactory.getInstance().getRequestInterceptorAction(request).dealServerInitStatus(response);
            return false;
        } else {
            LoginClientBean loginClientBean=this.checkLogin(request, response, handlerMethod, loginStatusValidator);
            if (loginClientBean==null) {
                return false;
            } else {
                this.detectVisit(handlerMethod, loginClientBean.getUserId());
                this.checkFunctionSupport(handlerMethod);
                this.checkWebAppName(request);
                return true;
            }
        }
    }

1 this.getLoginStatusValidator(handlerMethod) 这里是检查我们当前访问的请求方法或者方法所在的类上是不是加了LoginStatusChecker注解,比如,在Controller上加上:

    @LoginStatusChecker(
       required=false
    )

表示该controller上的方法丢不需要登录验证。

2 检查系统是否初始化完成,如果没有,返回false

3 检查登录状态,如果没有登录,返回false,如果登录了,检查visit状态,visit是检查我们的方法是否添加了VisitRefer注解,这个注解是对登录用户的userid来做判断。看是否该用户可以访问这个请求

下面看看this.checkLogin(request, response, handlerMethod, loginStatusValidator);

 LoginClientBean checkLogin(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, LoginStatusValidator loginStatusValidator) throws Exception {
        try {
            TerminalHandler terminal=TerminalHandler.getTerminal(request, NetworkHelper.getDevice(request));
            return LoginService.getInstance().loginStatusValid(loginStatusValidator.getTokenResource().getToken(request), terminal);
        } catch (Exception var8) {
            JSONPResponseBody jsonpResponseBody=(JSONPResponseBody)handlerMethod.getMethod().getAnnotation(JSONPResponseBody.class);
            if (jsonpResponseBody !=null) {
                JSONPMethodProcessor jsonpMethodProcessor=new JSONPMethodProcessor();
                jsonpMethodProcessor.handleJsonpRequest(this.getErrorMap(var8), request, response);
            } else {
                PreHandlerFactory.getInstance().getRequestInterceptorAction(request).dealLoginInvalidStatus(request, response, var8);
            }

            return null;
        }
    }

1如果没有登录,就会进入异常,如果该方法没有JSONResponseBody注解,就会进入到PreHandlerFactory.getInstance().getRequestInterceptorAction(request).dealLoginInvalidStatus(request, response, var8);

2 PreHandlerFactory.getInstance().getRequestInterceptorAction(request)默认返回com.fr.decision.webservice.interceptor.handler.redirect.RedirectAction,我们也可以PreHandlerFactory.getInstance().registerRequestInterceptorActions来注册自己的action。

下面来看RedirectAction的dealLoginInvalidStatus:

public void dealLoginInvalidStatus(HttpServletRequest request, HttpServletResponse response, Exception ex) throws Exception {
        FineLoggerFactory.getLogger().info(ex.getMessage());
        OriginUrlResponseBean originUrlResponseBean=getOriginalRedirectedUrl(request);
        String originUrlKey=UUIDUtil.generate();
        DecisionStatusService.originUrlStatusService().put(originUrlKey, originUrlResponseBean, (int)FSConfig.getInstance().getLoginConfig().getLoginTimeout());
        String urlWithOrigin=HttpToolbox.appendQuery("${fineServletURL}/login", "origin", originUrlKey);
        response.sendRedirect(TemplateUtils.render(urlWithOrigin));
    }

这里就会将我们的请求重定向到login请求上。


第二步,重定向到登录后会发生什么?

当请求/login时,会进入到com.fr.web.controller.decision.api.auth.LoginResource的page方法上,该类加了如下注解:

@LoginStatusChecker(
    required=false
)
public class LoginResource {

所以它里面定义的方法都不需要验证登录情况。所以直接就能进入login请求方法,该方法为:

@RequestMapping(
        value={"/login"},
        method={RequestMethod.GET},
        produces={"text/html"}
    )
    @ResponseBody
    public String page(HttpServletRequest req, HttpServletResponse res) throws Exception {
        if (LoginService.getInstance().isLogged(req) && FSConfig.getInstance().getLoginConfig().isForceRedirectAfterLogin()) {
            res.sendRedirect(TemplateUtils.render("${fineServletURL}"));
            return "";
        } else if (AppearanceConfig.getInstance().getLoginType()==LoginAppearanceType.LOGIN_URL.toInteger()) {
            String url=this.dealWithLoginUrl(AppearanceConfig.getInstance().getLoginUrl());
            String origin=req.getParameter("origin");
            if (origin !=null) {
                url=HttpToolbox.appendQuery(url, "origin", origin);
            }

            res.sendRedirect(url);
            return "";
        } else {
            Map<String, Object> param=new HashMap();
            ObjectMapper mapper=new ObjectMapper();
            param.put("title", AppearanceConfig.getInstance().getPlatformTitle());
            param.put("loginConfig", mapper.writeValueAsString(ConfigService.getInstance().getLoginAppearanceConfig()));
            param.put("charset", ServerConfig.getInstance().getServerCharset());
            PathGroup group=AtomBuilder.create().buildAssembleFilePath(Browser.resolve(req), LoginComponent.KEY);
            param.put("styleTag", AtomBuilder.create().toHtmlTag(group.toStylePathGroup()));
            param.put("scriptTag", AtomBuilder.create().toHtmlTag(group.toScriptPathGroup()));
            Map<String, Object> system=new HashMap();
            system.put("frontSeed", SecurityConfig.getInstance().getFrontSeed());
            system.put("transmissionEncryption", SystemEncryptionManager.getInstance().getTransmissionEncryption().getType());
            system.put("frontSM4Key", SM4TransmissionEncryption.getInstance().getTransmissionKey());
            system.put("cloudEnabled", CloudCenterConfig.getInstance().isOnline());
            system.put("urlIP", CloudCenter.getInstance().acquireConf("decision.queryip", ""));
            if (AppearanceConfig.getInstance().isCopyrightInfoDisplay()) {
                system.putAll(LoginService.getInstance().getCopyrightInfo(req));
            }

            param.put("system", mapper.writeValueAsString(system));
            return WebServiceUtils.parseWebPageResourceSafe("/com/fr/web/controller/decision/entrance/resources/login.html", param);
        }
}

1:判断是否已经登录了,如果登录了并且isForceRedirectAfterLogin为true,则直接重定向到主页面下。

2:判断loginType与loginUrl,如果满足直接重定向到loginUrl,这里我们可以做自己的登录界面。定制。

3:跳转到系统默认的登录界面/com/fr/web/controller/decision/entrance/resources/login.html。并且会为前端界面设置一些参数,以及样式,js脚本渲染到login.html


第三步:当渲染到前端界面/com/fr/web/controller/decision/entrance/resources/login.html,login.html大致如下:

 <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
        <title>数据决策系统</title>
        <!--css文件-->
        <link rel="stylesheet" type="text/css" href="/webroot/decision/file?path=/com/fr/web/ui/fineui.min.css&type=plain&parser=plain"/>

    <link rel="stylesheet" type="text/css" href="/webroot/decision/file?path=/com/fr/web/ui/materials.min.css&type=plain&parser=plain"/>
    <link rel="stylesheet" type="text/css" href="/webroot/decision/file?path=/com/fr/web/resources/dist/login.min.css&type=plain&parser=dynamic"/>

    </head>
    <body>
    <div id="wrapper"></div>

    <script type="text/javascript">
        window.Dec=window.Dec || {};
        window.Dec.injection=window.Dec.injection || {};
    </script>

    <script type="text/javascript">
        Dec.fineServletURL="/webroot/decision";
        Dec.loginConfig=JSON.parse('{\"loginImg\":false,
        \"loginImgId\":\"\",
        \"loginLogoImgId\":\"\",
        \"loginLogoImgName\":\"\",
        \"loginUrl\":\"\",
        \"loginType\":0,
        \"loginTitle\":\"数据决策系统\",
        \"loginColor\":\"#3685F2\",
        \"loginPages\":[],
        \"loginPageId\":\"\",
        \"cookiePath\":\"\/\",
        \"copyrightInfoDisplay\":true
        }');
        Dec.system=JSON.parse('{
        \"urlIP\":\"https:\/\/cloud.fanruan.com\/api\/query\/ip?timeout=10000\",
        \"loginCopyright\":\"Powered by 帆软\",
        \"transmissionEncryption\":2,
        \"frontSM4Key\":\"46910b85bcb2caf4db5a5dbabec3ccfa\",
        \"frontSeed\":\"FGljWYEQgXJGEhTP\",
        \"cloudEnabled\":true,
        \"url\":\"http:\/\/www.fanruan.com\/?utm_source=frexe&utm_medium=trial&utm_campaign=platform\",
        \"templateCopyright\":\"上BI选帆软,专注BI十五年。 Powered by 帆软\"
        }');
        }
    </script>
    <script type="text/javascript" src="/webroot/decision/file?path=/com/fr/web/ui/fineui.min.js&type=plain&parser=plain"></script> //UI框架
    <script type="text/javascript" src="/webroot/decision/file?path=com.fr.decision.web.i18n.I18nTextGenerator&type=class&parser=plain"></script>
    <script type="text/javascript" src="/webroot/decision/file?path=com.fr.decision.web.constant.ConstantGenerator&type=class&parser=plain"></script>
    <script type="text/javascript" src="/webroot/decision/file?path=/com/fr/web/ui/materials.min.js&type=plain&parser=plain"></script>
    <script type="text/javascript" src="/webroot/decision/file?path=/com/fr/web/resources/dist/login.min.js&type=plain&parser=plain"></script> //登录界面构建

    <script>
        Dec.start();
    </script>
    </body>

    </html>

这个界面是通过帆软的 fineUI框架来构建页面的。当点击登录按钮时,会调用POST的/login请求来登录,当登录成功后会将返回的token写入cookie,界面定位到返回得originUrlResponse.originUrl上。