引子
自从 2009 年开始在博客园写文章, 这是目前我写的最长的一篇文章了.
前前后后, 我总共花了 5 天的时间, 每天超过 3 小时不间断写作和代码调试. 总共有 8 篇文章, 每篇 5~6 个小结, 总截图数高达 60 多个.
俗话说, 桃李不言下自成蹊.
希望我的辛苦和努力能得到你的认可, 并对你的学习和工作有所帮助.
欢迎评论和 (这是一个可以点击的按钮, 点击即可推荐本文!)
前言
这是一个系列教程, 以自微软的官方文档为基础, 与微软官方文档的区别主要有如下几点:
更通俗易懂的语言
从代码入手(而非依赖 VS 的基架模板)
关键知识点的深入解读
加入和 WebForms / MVC 的对比
使用 FineUICore 控件库(而非原生的控件)
更少的代码和更现代化的界面(得益于 FineUICore 强大的控件库)
本教程包含如下内容:
Razor Pages 项目
安装软件
下载 FineUICore 空项目
项目目录
项目运行截图
向 Razor Pages 添加模型
POCO 类
DbContext 类
配置数据库连接字符串
在 Startup.cs 中注册数据库服务
初始化数据库和数据迁移
列表页面
新增 Movie 页面
默认生成的页面和模型类
异步获取数据并通过表格控件展示
列标题文字是怎么来的?
格式化显示日期
新增页面
新增页面模型
新增页面视图
查看 HTTP POST 请求的数据
客户端模型验证
自定义 JavaScript 来绕开客户端验证
自定义模型验证错误消息
编辑页面
编辑页面模型
编辑页面视图
路由模板
更新电影信息
处理并发冲突
列表页面和弹出窗体
更新表格页面
行编辑按钮
窗体的关闭事件
更新编辑页面
先弹出提示对话框, 再关闭当前窗体
表格与窗体互动(动图)
搜索框与行删除按钮
行删除按钮
行删除按钮的自定义回发
行删除事件
搜索框
搜索框事件
服务端标记搜索框不能为空
分页与排序
数据库分页
保持分页状态和搜索状态
将 5 个回发事件合并为 1 个
排序
SortBy 扩展方法
下载项目源代码
最终完整的作品是一个简单的电影数据管理页面, 如下所示:
如果你希望了解 ASP.NET MVC 的基础知识, 请查阅我之前写的系列教程: ASP.NET MVC 快速入门(MVC5+EF6)
一, Razor Pages 项目
1.1, 安装软件
在进行本教程之前需要安装如下两个软件:
VS2019(需要选择 ASP.NET and Web development 工作负载)
.NET Core SDK 最新版: https://dotnet.microsoft.com/download
1.2, 下载 FineUICore 空项目
FineUICore 相关产品可以到我的知识星球内下载: https://fineui.com/fans/
FineUICore 空项目已经完成相关的配置, 并可以 F5 直接运行. 建议初学者从空项目入手, 在熟悉 ASP.NET Core 开发流程后再自行创建项目.
在知识星球内, 我们提供两个空项目, 分别是:
[空项目] FineUICore_EmptyProject_RazorPages_vxxx.zip
[空项目] FineUICore_EmptyProject_vxxx.zip
其中, 不带 RazorPages 字符串的是基于 MVC 架构的项目, 而本教程需要使用的是带 RazorPages 标识的.
在 FineUICore_EmptyProject_RazorPages 项目中, 页面视图中使用了 TagHelpers 标签, 使得页面结构更加清晰, 和 WebForms 的标签更加类似.
我之前曾经写过一篇文章, 对比 RazorPages + TagHelpers 的项目和传统的 ASP.NET MVC + HtmlHelpers 的区别, 有兴趣可以了解一下:
全新 ASP.NET Core, 比 WebForms 还简单!
1.3, 项目目录
这里面有一些主要的文件和目录, 从上到下分别是:
1. wwwroot 目录
包含静态文件, 如 HTML 文件, JavaScript 文件和 CSS 文件.
这是 ASP.NET Core 引入的一个命名约定, 将全部的静态资源放置于 wwwroot 目录有助于保持项目结构的清晰, 之前的 ASP.NET MVC 和 WebForms 项目, 我们一般都自行创建一个 res 目录.
我的理解, 这样的结构有助于提高项目的编译速度, 如果对比 ASP.NET MVC/WebForms 和 ASP.NET Core 的项目文件(.csproj), 你会发现之前的文件是显式包含进来的:
- <ItemGroup>
- <Content Include="res\images\themes\vader.png" />
- <Compile Include="Areas\Button\Controllers\ButtonController.cs" />
- ...
- </ItemGroup>
而 ASP.NET Core 项目文件已经没有了这些配置项, 说明是隐式包含的, 也就是说:
wwwroot 目录中的是网站内容, 无需编译
其他目录中的需要编译
2. Code 目录
自行创建的目录, 主要放置页面基类, 已经自定义类.
3. Pages 目录
包含 Razor 页面和帮助文件(以下划线开头).
每个 Razor 页面都由两个文件组成:
一个 .cshtml 文件, 其中包含使用 Razor 语法的 C# 代码的 HTML 标签 .
一个 .cshtml.cs 文件, 其中包含处理页面事件的 C# 代码 .
Razor 页面的访问遵循着简单的目录结构, 比如:
Pages/Index.cshtml 的访问 URL 地址:/Index 或者 /
Pages/Admin/Users.cshtml 的访问 URL 地址:/Admin/Users
相比 ASP.NET MVC 架构的页面, 这是一个巨大的进步, 在 MVC 中我们需要借助于抽象的 Areas 目录, 并且很难支持 3 级以上的 URL 网址, 比如:/Mobile/Button/Group
帮助文件主要有如下几个:
Shared/_Layout.cshtml: 主要放置页面框架标签, 比如页面 < HTML><head><body > 标签, 以及引入共用的 CSS 和 JS 文件, 类似于 WebForms 中的母版页(Master Page).
_ViewImports.cshtml: 一个 using 指令和 addTagHelpers 指令, 以便在 Razor 页面使用不加前缀的控件名和标签.
_ViewStart.cshtml:Razor 页面的启动文件, 会在页面执行之前调用, 默认包含了对布局页面的调用. 这个文件是可以在目录中嵌套的, 运行是会先执行最外层目录中的_ViewStart.cshtml 文件, 再执行内层目录中的_ViewStart.cshtml. 这也很好理解, 为了确保最靠近 Razor 页面的内层定义覆盖外层定义.
4. appSettings.JSON
包含配置数据, 如数据库连接字符串. 默认包含了 FineUICore 的一些全局配置信息:
5. Program.cs
包含程序的入口点.
6. Startup.cs
包含配置应用行为的代码. 这个文件非常关键, 里面定义了用于依赖注入的配置项, 已经执行 ASP.NET Core HTTP 请求管道的插件.
当然, 对于初学者不需要关注这些细节问题, 我们简单看下在请求管道中添加 FineUICore 插件的地方:
1.4, 项目运行截图
可以直接 Ctrl + F5 不调试运行项目, 运行截图如下:
项目默认的是 Pure_Black 主题, 这个在 appSettings.JSON 中有定义 .
为了和 VS2019 的深色主题相配, 我们特意选取了 Dark_Hive 深色主题:
二, 向 Razor Pages 添加模型
2.1,POCO 类
本示例将实现一个简单的电影管理页面, 所以需要添加一个数据模型, 也称为 POCO 类(plain-old CLR objects), 因为它们与 EF Core 没有任何依赖关系.
在 Code 目录中新建一个 Movie.cs 文件:
- using System;
- using System.ComponentModel.DataAnnotations;
- namespace FineUICore.EmptyProject.RazorPages
- {
- public class Movie
- {
- public int ID { get; set; }
- [Required]
- [Display(Name = "名称")]
- public string Title { get; set; }
- [DataType(DataType.Date)]
- [Display(Name = "发布日期")]
- public DateTime ReleaseDate { get; set; }
- [Display(Name = "类型")]
- public string Genre { get; set; }
- [Display(Name = "价格")]
- public decimal Price { get; set; }
- }
- }
Movie 类包含:
ID 字段: 数据库表主键, 遵循命名约定, 可以是 ID 或者 MovieID.
[Require]: 指定字段为必填项.
[Display(Name = "名称")]: 指定字段在前端界面的显示名称, 主要用于如下两个地方:
表格的表头文字
表单字段的标题文字
[DataType(DataType.Date)]: 指定此字段的数据类型为日期. 这个特性有两个作用:
不仅影响数据库中的字段类型(仅包含日期部分, 需要包含时间);
也影响客户端的表格展示, 和数据录入.
2.2,DbContext 类
为了能正确初始化数据库, 我们还需要一个继承自 DbContext 的类, 如下所示:
- namespace FineUICore.EmptyProject.RazorPages
- {
- public class MovieContext : DbContext
- {
- public MovieContext(DbContextOptions<MovieContext> options) : base(options)
- {
- }
- public DbSet<Movie> Movies { get; set; }
- }
- }
由于空项目尚未引入 EF Core, 所以上述代码会有错误提示.
下面我们需要安装 EntityFrameworkCore 相关程序包, 打开菜单[工具] ->[Nuget 包管理器] :
我们需要安装如下两个程序包:
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer:Microsoft SqlServer 数据库支持.
Microsoft.EntityFrameworkCore.Tools: 用于在包管理控制台使用 EF Core 的数据迁移命令, 比如 Add-Migration 等.
安装完成后, 我们需要更新 MovieContext.cs 文件, 在文件头添加如下指令:
using Microsoft.EntityFrameworkCore;
2.3, 配置数据库连接字符串
本示例使用 LocalDb 数据库, LocalDb 是轻型版的 SQL Server Express 数据库引擎, 主要用于开发阶段. 默认情况下, LocalDB 数据库在 C:\Users\<user>\AppData 目录下创建 *.mdf 文件.
从[视图] 菜单中, 打开[SQL Server 对象资源管理器] , 如下所示:
在 SQL Server 节点上点击右键, 选中[添加 SQL Server ...] :
这时, 可以看到我们连接的 LocalDb 数据库:
右键, 点击[属性] , 找到[连接字符串] :
将这个数据库字符串拷贝出来, 放到 appSettings.JSON 文件中:
- {
- "FineUI": {
- "DebugMode": false,
- "CustomTheme": "pure_black",
- "EnableAnimation": false
- },
- "ConnectionStrings": {
- "MovieContext": "Data Source=(localdb)\\MSSQLLocalDB;Database=MovieContext;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
- }
- }
注意: 在数据库连接字符串中添加 Database=MovieContext; 用来指定我们自己的数据库, 否则新建的表都会添加到系统表 master 中.
2.4, 在 Startup.cs 中注册数据库服务
ASP.NET Core 内置了依赖注入的支持. 我们首先需要在 Startup.cs 中注册各种服务(比如 Razor Pages,FineUICore 以及 EF Core 服务), 然后在页面中通过构造函数传入已经注册的服务.
简化后的代码:
- public void ConfigureServices(IServiceCollection services)
- {
- // FineUI 服务
- services.AddFineUI(Configuration);
- services.AddRazorPages();
- services.AddDbContext<MovieContext>(options =>
- options.UseSqlServer(Configuration.GetConnectionString("MovieContext")));
- }
在 AddDbContext 中, 我们通过 Configuration 来获取 appSettings.JSON 中定义的数据库连接字符串.
2.5, 初始化数据库和数据迁移
这一节, 我们会使用 EF Core 提供的数据迁移工具 (Data Migration) 来初始化数据库.
首先打开 VS 的包管理控制台(Package Manager Console), 位于菜单项[工具] 下面:
在 PM> 提示符下输入: Add-Migration InitialCreate
安装完成后, 我们的项目多了一个 Migrations 目录, 里面有一个类似 20200309093752_InitialCreate.cs 的文件.
这个就是初始化迁移脚本, 里面包含一个 Up 方法和一个 Down 方法, 分别对应于应用本迁移和取消本迁移:
上面的 Up 方法主要做了是三个事情:
创建名为 Movies 的表格
分别定义表格列 ID,Title,ReleaseDate....
定义表格主键为列 ID
此时数据库尚未创建 Movies 表, 为了执行 Up 函数, 我们还需要执行 Update-Database 命名.
在 PM> 提示符下输入: Update-Database
运行结束后, 在[Sql Server 对象资源管理器] 面板中, 找到刚刚创建的 MovieContext 数据库:
查看 Movies 的视图设计器:
通过 Movies 的数据预览面板, 我们还可以新增一条数据:
三, 列表页面
3.1, 新增 Movie 页面
在 VS 的资源管理器面板, Pages 目录右键, 并添加一个 Razor 页面, 命名为 Movie:
这个面板中, 使用布局页留空, 默认使用 _ViewStart.cshtml 中定义的布局文件(Shared/_Layout.cshtml).
默认生成的页面文件 Movie.cshtml:
- @page
- @model FineUICore.EmptyProject.RazorPages.MovieModel
- @{
- ViewData["Title"] = "Movie";
- }
- <h1>Movie</h1>
在这个页面中:
@page: 指示这是一个页面, 可以通过命名约定来访问(/Movie),@page 指令必须是页面上的第一个指令.
@model: 指示本页面对应的页面模型, 类似于 WebForms 的后台文件.
ViewData: 用来在模型和视图之间, 以及视图之间传值, 可以在 Shared/_Layout.cshtml 访问这里定义的 ViewData["Title"] 数据.
3.2, 默认生成的页面和模型类
默认生成的页面文件 Movie.cshtml.cs 模型类:
- using Microsoft.AspNetCore.Mvc.RazorPages;
- namespace FineUICore.EmptyProject.RazorPages
- {
- public class MovieModel : PageModel
- {
- public void OnGet()
- {
- }
- }
- }
这是一个继承自 PageModel 的类, OnGet 方法用来初始化页面数据, ASP.NET Core 还支持异步调用, 这个函数的异步签名如下所示:
- public async Task OnGetAsync()
- {
- await _context.Students.ToListAsync();
- }
通过在 OnGet 后面添加 Async, 并且返回 async Task 这样的命名约定来启用异步调用.
本示例中的 HTTP 请求 (Get,Post) 以及对数据库的操作我们都将使用异步调用的形式, 以提高性能.
3.3, 异步获取数据并通过表格控件展示
将 Movie.cshtml.cs 模型类更新为:
- using System.Collections.Generic;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Mvc.RazorPages;
- using Microsoft.EntityFrameworkCore;
- namespace FineUICore.EmptyProject.RazorPages
- {
- public class MovieModel : PageModel
- {
- private readonly MovieContext _context;
- public MovieModel(MovieContext context)
- {
- _context = context;
- }
- public IList<Movie> Movies { get; set; }
- public async Task OnGetAsync()
- {
- Movies = await _context.Movies.ToListAsync();
- }
- }
- }
这段代码中:
构造函数使用依赖注入将数据库上下文 DbContext 添加到页面中
属性 Movies 保存获取的电影列表
_context.Movies.ToListAsync() 通过异步的方法获取电影列表
页面上通过一个 FineUICore 表格控件, 用来展示电影列表数据, 修改后的 Movie.cshtml 文件:
- @page
- @model FineUICore.EmptyProject.RazorPages.MovieModel
- @{
- ViewData["Title"] = "Movie";
- }
- @section body {
- <f:Grid ID="Grid1" ShowBorder="true" ShowHeader="true" Title="电影列表" IsViewPort="true"
- DataIDField="ID" DataTextField="Title" DataSource="@Model.Movies">
- <Columns>
- <f:RowNumberField />
- <f:RenderField For="Movies.First().Title" ExpandUnusedSpace="true" />
- <f:RenderField For="Movies.First().ReleaseDate" Width="200" />
- <f:RenderField For="Movies.First().Genre" />
- <f:RenderField For="Movies.First().Price" />
- </Columns>
- </f:Grid>
- }
打开 Index.cshtml 框架页, 将 Movie 页面添加到左侧菜单项:
- <f:TreeNode Text="默认分类" Expanded="true">
- <f:TreeNode Text="开始页面" NavigateUrl="@Url.Content("~/Hello")"></f:TreeNode>
- <f:TreeNode Text="登录页面" NavigateUrl="@Url.Content("~/Login")"></f:TreeNode>
- <f:TreeNode Text="电影管理" NavigateUrl="@Url.Content("~/Movie")"></f:TreeNode>
- </f:TreeNode>
Ctrl+F5 运行, 此时的页面效果如下所示:
现在, 我们已经完成了对数据库的读操作, 并通过 FineUICore 的表格控件展现出来.
3.4, 列标题文字是怎么来的?
如果你细心观察, 可以发现在 Movie.cshtml 的表格控件中, 我们并没有显示的定义表格列标题, 而实际页面是有的, 这是怎么回事?
其实这个功能是 ASP.NET Core 和 FineUICore 共同努力的结果:
1. 首先 Movie.cs 模型中使用 Display 注解来标识列的显示文本
- [Display(Name = "名称")]
- public string Title {
- get; set;
- }
2. 然后 FineUICore 的表格控件通过 RenderField 的 For 属性来关联模型类属性
<f:RenderField For="Movies.First().Title" ExpandUnusedSpace="true" />
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.AspNetCore.Mvc.RazorPages;
- namespace FineUICore.EmptyProject.RazorPages
- {
- public class MovieNewModel : PageModel
- {
- private readonly MovieContext _context;
- public MovieNewModel(MovieContext context)
- {
- _context = context;
- }
- public void OnGet()
- {
- }
- [BindProperty]
- public Movie Movie { get; set; }
- public async Task<IActionResult> OnPostBtnSave_ClickAsync()
- {
- if (ModelState.IsValid)
- {
- _context.Movies.Add(Movie);
- await _context.SaveChangesAsync();
- Alert.Show("保存成功!");
- }
- return UIHelper.Result();
- }
- }
- }
- @page
- @model FineUICore.EmptyProject.RazorPages.MovieNewModel
- @{
- ViewData["Title"] = "MovieNew";
- }
- @section body {
- <f:SimpleForm ID="SimpleForm1" ShowBorder="true" ShowHeader="true" BodyPadding="10" Title="新建" IsViewPort="true">
- <Items>
- <f:TextBox For="Movie.Title"></f:TextBox>
- <f:DatePicker For="Movie.ReleaseDate"></f:DatePicker>
- <f:TextBox For="Movie.Genre"></f:TextBox>
- <f:NumberBox For="Movie.Price"></f:NumberBox>
- <f:Button ID="BtnSave" ValidateForms="SimpleForm1" Icon="SystemSave"
- OnClick="@Url.Handler("BtnSave_Click")" OnClickFields="SimpleForm1" Text="保存"></f:Button>
- </Items>
- </f:SimpleForm>
- }
- [Required(ErrorMessage = "名称不能为空!")]
- [Display(Name = "名称")]
- public string Title {
- get; set;
- }
- using System.Linq;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.AspNetCore.Mvc.RazorPages;
- using Microsoft.EntityFrameworkCore;
- namespace FineUICore.EmptyProject.RazorPages
- {
- public class MovieEditModel : PageModel
- {
- private readonly MovieContext _context;
- public MovieEditModel(MovieContext context)
- {
- _context = context;
- }
- [BindProperty]
- public Movie Movie { get; set; }
- public async Task<IActionResult> OnGetAsync(int id)
- {
- Movie = await _context.Movies.FirstOrDefaultAsync(m => m.ID == id);
- if (Movie == null)
- {
- return NotFound();
- }
- return Page();
- }
- public async Task<IActionResult> OnPostBtnSave_ClickAsync()
- {
- if (ModelState.IsValid)
- {
- _context.Attach(Movie).State = EntityState.Modified;
- try
- {
- await _context.SaveChangesAsync();
- Alert.Show("修改成功!");
- }
- catch (DbUpdateConcurrencyException)
- {
- if (!_context.Movies.Any(e => e.ID == Movie.ID))
- {
- Alert.Show("指定的电影不存在:" + Movie.Title);
- }
- else
- {
- throw;
- }
- }
- }
- return UIHelper.Result();
- }
- }
- }
- @page "{id:int}"
- @model FineUICore.EmptyProject.RazorPages.MovieEditModel
- @{
- ViewData["Title"] = "MovieEdit";
- }
- @section body {
- <f:SimpleForm ID="SimpleForm1" ShowBorder="true" ShowHeader="true" BodyPadding="10" Title="编辑" IsViewPort="true">
- <Items>
- <f:HiddenField For="Movie.ID"></f:HiddenField>
- <f:TextBox For="Movie.Title"></f:TextBox>
- <f:DatePicker For="Movie.ReleaseDate"></f:DatePicker>
- <f:TextBox For="Movie.Genre"></f:TextBox>
- <f:NumberBox For="Movie.Price"></f:NumberBox>
- <f:Button ID="BtnSave" ValidateForms="SimpleForm1" Icon="SystemSave"
- OnClick="@Url.Handler("BtnSave_Click")" OnClickFields="SimpleForm1" Text="保存"></f:Button>
- </Items>
- </f:SimpleForm>
- }
- Movie = await _context.Movies.FirstOrDefaultAsync(m => m.ID == id);
- if (Movie == null)
- {
- return NotFound();
- }
- _context.Attach(Movie).State = EntityState.Modified;
- await _context.SaveChangesAsync();
- Alert.Show("修改成功!");
- if (!_context.Movies.Any(e => e.ID == Movie.ID))
- {
- Alert.Show("指定的电影不存在:" + Movie.Title);
- }
- <f:Windows ID="Window1" IsModal="true" Hidden="true" Target="Top" EnableResize="true"
- EnableMaximize="true" EnableIFrame="true" Width="650" Height="400"
- OnClose="@Url.Handler("Window1_Close")" OnCloseFields="Panel1">
- </f:Windows>
- function onNewClick(event) {
- F.ui.Window1.show('@Url.Content("~/MovieNew")', '新增');
- }
- @page
- @model FineUICore.EmptyProject.RazorPages.MovieModel
- @{
- ViewData["Title"] = "Movie";
- }
- @section body {
- <f:Panel ID="Panel1" BodyPadding="10" ShowBorder="false" Layout="Fit" ShowHeader="false" Title="用户管理" IsViewPort="true">
- <Items>
- <f:Grid ID="Grid1" ShowBorder="true" ShowHeader="false" DataIDField="ID" DataTextField="Title" DataSource="@Model.Movies">
- <Columns>
- <f:RowNumberField />
- <f:RenderField For="Movies.First().Title" ExpandUnusedSpace="true" />
- <f:RenderField For="Movies.First().ReleaseDate" FieldFormat="yyyy-MM-dd" Width="200" />
- <f:RenderField For="Movies.First().Genre" />
- <f:RenderField For="Movies.First().Price" />
- <f:RenderField Width="50" RendererFunction="renderActionEdit"></f:RenderField>
- </Columns>
- </f:Grid>
- </Items>
- <Toolbars>
- <f:Toolbar ID="Toolbar1" Position="Top">
- <Items>
- <f:ToolbarFill></f:ToolbarFill>
- <f:Button ID="btnNew" IconFont="PlusCircle" Text="新增">
- <Listeners>
- <f:Listener Event="click" Handler="onNewClick"></f:Listener>
- </Listeners>
- </f:Button>
- </Items>
- </f:Toolbar>
- </Toolbars>
- </f:Panel>
- <f:Windows ID="Window1" IsModal="true" Hidden="true" Target="Top" EnableResize="true"
- EnableMaximize="true" EnableIFrame="true" Width="650" Height="400"
- OnClose="@Url.Handler("Window1_Close")" OnCloseFields="Panel1">
- </f:Windows>
- }
- @section script {
- <script>
- function onNewClick(event) {
- F.ui.Window1.show('@Url.Content("~/MovieNew")', '新增');
- }
- function renderActionEdit(value, params) {
- return '<a class="action-btn edit"href="javascript:;"><i class="f-icon f-icon-pencil f-grid-cell-iconfont"></a>';
- }
- F.ready(function () {
- var grid1 = F.ui.Grid1;
- grid1.el.on('click', 'a.action-btn', function (event) {
- var cnode = $(this);
- var rowData = grid1.getRowData(cnode.closest('.f-grid-row'));
- if (cnode.hasClass('edit')) {
- F.ui.Window1.show('@Url.Content("~/MovieEdit/")' + rowData.id, '编辑');
- }
- });
- });
- </script>
- }
function renderActionEdit(value, params) { return '<a class="action-btn edit"href="javascript:;"><i class="f-icon f-icon-pencil f-grid-cell-iconfont"></a>'; }
F.ready(function () { var grid1 = F.ui.Grid1; grid1.el.on('click', 'a.action-btn', function (event) { var cnode = $(this); var rowData = grid1.getRowData(cnode.closest('.f-grid-row')); if (cnode.hasClass('edit')) { F.ui.Window1.show('@Url.Content("~/MovieEdit/")' + rowData.id, '编辑'); } }); });
public async Task<IActionResult> OnPostWindow1_CloseAsync(string[] Grid1_fields) { var Grid1 = UIHelper.Grid("Grid1"); var movies = await _context.Movies.ToListAsync(); Grid1.DataSource(movies, Grid1_fields); return UIHelper.Result(); }
@page "{id:int}" @model FineUICore.EmptyProject.RazorPages.MovieEditModel @{ ViewData["Title"] = "MovieEdit"; } @section body { <f:Panel ID="Panel1" ShowBorder="false" ShowHeader="false" AutoScroll="true" IsViewPort="true" Layout="Fit"> <Toolbars> <f:Toolbar Position="Bottom" ToolbarAlign="Center"> <Items> <f:Button ID="BtnClose" IconFont="Close" Text="关闭"> <Listeners> <f:Listener Event="click" Handler="F.activeWindow.hide();"></f:Listener> </Listeners> </f:Button> <f:ToolbarSeparator></f:ToolbarSeparator> <f:Button ID="BtnSave" ValidateForms="SimpleForm1" IconFont="Save" OnClick="@Url.Handler("BtnSave_Click")" OnClickFields="SimpleForm1" Text="保存后关闭"></f:Button> </Items> </f:Toolbar> </Toolbars> <Items> <f:SimpleForm ID="SimpleForm1" ShowBorder="false" ShowHeader="false" BodyPadding="10"> <Items> <f:HiddenField For="Movie.ID"></f:HiddenField> <f:TextBox For="Movie.Title"></f:TextBox> <f:DatePicker For="Movie.ReleaseDate"></f:DatePicker> <f:TextBox For="Movie.Genre"></f:TextBox> <f:NumberBox For="Movie.Price"></f:NumberBox> </Items> </f:SimpleForm> </Items> </f:Panel> }
public async Task<IActionResult> OnPostBtnSave_ClickAsync() { if (ModelState.IsValid) { _context.Attach(Movie).State = EntityState.Modified; try { await _context.SaveChangesAsync(); Alert.Show("修改成功!", string.Empty, MessageBoxIcon.Success, ActiveWindow.GetHidePostBackReference()); } catch (DbUpdateConcurrencyException) { if (!_context.Movies.Any(e => e.ID == Movie.ID)) { Alert.Show("指定的电影不存在:" + Movie.Title); } else { throw; } } } return UIHelper.Result(); }
function renderActionDelete(value, params) { return '<a class="action-btn delete"href="javascript:;"><i class="f-icon f-icon-trash f-grid-cell-iconfont"></a>'; }
F.ready(function () { var grid1 = F.ui.Grid1; grid1.el.on('click', 'a.action-btn', function (event) { var cnode = $(this); var rowData = grid1.getRowData(cnode.closest('.f-grid-row')); if (cnode.hasClass('delete')) { F.confirm({ message: '确定删除此记录?', target: '_top', ok: function () { F.doPostBack('@Url.Handler("Grid_RowDelete")', 'Panel1', { deletedRowID: rowData.id }); } }); } }); });
public async Task<IActionResult> OnPostGrid_RowDeleteAsync(string[] Grid1_fields, int deletedRowID) { var Grid1 = UIHelper.Grid("Grid1"); var movie = await _context.Movies.FindAsync(deletedRowID); if (movie != null) { _context.Movies.Remove(movie); await _context.SaveChangesAsync(); var movies = await _context.Movies.ToListAsync(); Grid1.DataSource(movies, Grid1_fields); } return UIHelper.Result(); }
<f:Panel ID="Panel1" BodyPadding="10" ShowBorder="false" Layout="VBox" ShowHeader="false" Title="用户管理" IsViewPort="true"> <Items> <f:Form ShowBorder="false" ShowHeader="false"> <Rows> <f:FormRow> <Items> <f:TwinTriggerBox ID="TBSearchMessage" ShowLabel="false" EmptyText="在名称中搜索" Trigger1Icon="Clear" ShowTrigger1="false" Trigger2Icon="Search" OnTrigger1Click="@Url.Handler("TBSearchMessage_Trigger1")" OnTrigger1ClickFields="Panel1" OnTrigger2Click="@Url.Handler("TBSearchMessage_Trigger2")" OnTrigger2ClickFields="Panel1"> </f:TwinTriggerBox> <f:Label></f:Label> </Items> </f:FormRow> </Rows> </f:Form> <f:Grid ID="Grid1" BoxFlex="1" ShowBorder="true" ShowHeader="false" DataIDField="ID" DataTextField="Title" DataSource="@Model.Movies"> <Columns> <f:RowNumberField /> <f:RenderField For="Movies.First().Title" ExpandUnusedSpace="true" /> <f:RenderField For="Movies.First().ReleaseDate" FieldFormat="yyyy-MM-dd" Width="200" /> <f:RenderField For="Movies.First().Genre" /> <f:RenderField For="Movies.First().Price" /> <f:RenderField Width="50" RendererFunction="renderActionEdit"></f:RenderField> <f:RenderField Width="50" RendererFunction="renderActionDelete"></f:RenderField> </Columns> <Toolbars> <f:Toolbar ID="Toolbar1" Position="Top"> <Items> <f:ToolbarFill></f:ToolbarFill> <f:Button ID="btnNew" IconFont="PlusCircle" Text="新增"> <Listeners> <f:Listener Event="click" Handler="onNewClick"></f:Listener> </Listeners> </f:Button> </Items> </f:Toolbar> </Toolbars> </f:Grid> </Items> </f:Panel>
OnTrigger1Click="@Url.Handler("TBSearchMessage_Trigger1")" OnTrigger1ClickFields="Panel1" OnTrigger2Click="@Url.Handler("TBSearchMessage_Trigger2")" OnTrigger2ClickFields="Panel1"
private async Task ReloadGrid(string[] Grid1_fields, string searchMessage) { IQueryable<Movie> q = _context.Movies; // 搜索框 searchMessage = searchMessage?.Trim(); if (!string.IsNullOrEmpty(searchMessage)) { q = q.Where(s => s.Title.Contains(searchMessage)); } Movies = await q.ToListAsync(); UIHelper.Grid("Grid1").DataSource(Movies, Grid1_fields); }
public async Task<IActionResult> OnPostTBSearchMessage_Trigger1Async(string[] Grid1_fields) { var TBSearchMessageUI = UIHelper.TwinTriggerBox("TBSearchMessage"); // 清空搜索框, 并隐藏清空图标 TBSearchMessageUI.Text(string.Empty); TBSearchMessageUI.ShowTrigger1(false); // 重新加载表格数据 await ReloadGrid(Grid1_fields, string.Empty); return UIHelper.Result(); } public async Task<IActionResult> OnPostTBSearchMessage_Trigger2Async(string[] Grid1_fields, string TBSearchMessage) { var TBSearchMessageUI = UIHelper.TwinTriggerBox("TBSearchMessage"); // 显示清空图标 TBSearchMessageUI.ShowTrigger1(true); // 重新加载表格数据 await ReloadGrid(Grid1_fields, TBSearchMessage); return UIHelper.Result(); }
public async Task<IActionResult> OnPostTBSearchMessage_Trigger2Async(string[] Grid1_fields, string TBSearchMessage) { var TBSearchMessageUI = UIHelper.TwinTriggerBox("TBSearchMessage"); if (string.IsNullOrEmpty(TBSearchMessage)) { TBSearchMessageUI.MarkInvalid("搜索文本不能为空!"); } else { // 显示清空图标 TBSearchMessageUI.ShowTrigger1(true); // 重新加载表格数据 await ReloadGrid(Grid1_fields, TBSearchMessage); } return UIHelper.Result(); }
@page @model FineUICore.EmptyProject.RazorPages.MovieModel @{ ViewData["Title"] = "Movie"; } @section body { <f:Panel ID="Panel1" BodyPadding="10" ShowBorder="false" Layout="VBox" ShowHeader="false" Title="用户管理" IsViewPort="true"> <Items> <f:Form ShowBorder="false" ShowHeader="false"> <Rows> <f:FormRow> <Items> <f:TwinTriggerBox ID="TBSearchMessage" ShowLabel="false" EmptyText="在名称中搜索" Trigger1Icon="Clear" ShowTrigger1="false" Trigger2Icon="Search" OnTrigger1Click="@Url.Handler("TBSearchMessage_Trigger1")" OnTrigger1ClickFields="Panel1" OnTrigger2Click="@Url.Handler("TBSearchMessage_Trigger2")" OnTrigger2ClickFields="Panel1"> </f:TwinTriggerBox> <f:Label></f:Label> </Items> </f:FormRow> </Rows> </f:Form> <f:Grid ID="Grid1" BoxFlex="1" ShowBorder="true" ShowHeader="false" DataIDField="ID" DataTextField="Title" DataSource="@Model.Movies"> <Columns> <f:RowNumberField /> <f:RenderField For="Movies.First().Title" ExpandUnusedSpace="true" /> <f:RenderField For="Movies.First().ReleaseDate" FieldFormat="yyyy-MM-dd" Width="200" /> <f:RenderField For="Movies.First().Genre" /> <f:RenderField For="Movies.First().Price" /> <f:RenderField Width="50" RendererFunction="renderActionEdit"></f:RenderField> <f:RenderField Width="50" RendererFunction="renderActionDelete"></f:RenderField> </Columns> <Toolbars> <f:Toolbar ID="Toolbar1" Position="Top"> <Items> <f:ToolbarFill></f:ToolbarFill> <f:Button ID="btnNew" IconFont="PlusCircle" Text="新增"> <Listeners> <f:Listener Event="click" Handler="onNewClick"></f:Listener> </Listeners> </f:Button> </Items> </f:Toolbar> </Toolbars> </f:Grid> </Items> </f:Panel> <f:Windows ID="Window1" IsModal="true" Hidden="true" Target="Top" EnableResize="true" EnableMaximize="true" EnableIFrame="true" Width="650" Height="400" OnClose="@Url.Handler("Window1_Close")" OnCloseFields="Panel1"> </f:Windows> } @section script { <script> function onNewClick(event) { F.ui.Window1.show('@Url.Content("~/MovieNew")', '新增'); } function renderActionEdit(value, params) { return '<a class="action-btn edit"href="javascript:;"><i class="f-icon f-icon-pencil f-grid-cell-iconfont"></a>'; } function renderActionDelete(value, params) { return '<a class="action-btn delete"href="javascript:;"><i class="f-icon f-icon-trash f-grid-cell-iconfont"></a>'; } F.ready(function () { var grid1 = F.ui.Grid1; grid1.el.on('click', 'a.action-btn', function (event) { var cnode = $(this); var rowData = grid1.getRowData(cnode.closest('.f-grid-row')); if (cnode.hasClass('delete')) { F.confirm({ message: '确定删除此记录?', target: '_top', ok: function () { F.doPostBack('@Url.Handler("Grid_RowDelete")', 'Panel1', { deletedRowID: rowData.id }); } }); } else if (cnode.hasClass('edit')) { F.ui.Window1.show('@Url.Content("~/MovieEdit/")' + rowData.id, '编辑'); } }); }); </script> } using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; namespace FineUICore.EmptyProject.RazorPages { public class MovieModel : PageModel { private readonly MovieContext _context; public MovieModel(MovieContext context) { _context = context; } public IList<Movie> Movies { get; set; } public async Task OnGetAsync() { Movies = await _context.Movies.ToListAsync(); } private async Task ReloadGrid(string[] Grid1_fields, string searchMessage) { IQueryable<Movie> q = _context.Movies; // 搜索框 searchMessage = searchMessage?.Trim(); if (!string.IsNullOrEmpty(searchMessage)) { q = q.Where(s => s.Title.Contains(searchMessage)); } Movies = await q.ToListAsync(); UIHelper.Grid("Grid1").DataSource(Movies, Grid1_fields); } public async Task<IActionResult> OnPostWindow1_CloseAsync(string[] Grid1_fields) { // 重新加载表格数据 await ReloadGrid(Grid1_fields, string.Empty); return UIHelper.Result(); } public async Task<IActionResult> OnPostGrid_RowDeleteAsync(string[] Grid1_fields, int deletedRowID) { var movie = await _context.Movies.FindAsync(deletedRowID); if (movie != null) { _context.Movies.Remove(movie); await _context.SaveChangesAsync(); // 重新加载表格数据 await ReloadGrid(Grid1_fields, string.Empty); } return UIHelper.Result(); } public async Task<IActionResult> OnPostTBSearchMessage_Trigger1Async(string[] Grid1_fields) { var TBSearchMessageUI = UIHelper.TwinTriggerBox("TBSearchMessage"); // 清空搜索框, 并隐藏清空图标 TBSearchMessageUI.Text(string.Empty); TBSearchMessageUI.ShowTrigger1(false); // 重新加载表格数据 await ReloadGrid(Grid1_fields, string.Empty); return UIHelper.Result(); } public async Task<IActionResult> OnPostTBSearchMessage_Trigger2Async(string[] Grid1_fields, string TBSearchMessage) { var TBSearchMessageUI = UIHelper.TwinTriggerBox("TBSearchMessage"); if (string.IsNullOrEmpty(TBSearchMessage)) { TBSearchMessageUI.MarkInvalid("搜索文本不能为空!"); } else { // 显示清空图标 TBSearchMessageUI.ShowTrigger1(true); // 重新加载表格数据 await ReloadGrid(Grid1_fields, TBSearchMessage); } return UIHelper.Result(); } } }
<f:Grid ID="Grid1" BoxFlex="1" ShowBorder="true" ShowHeader="false" DataIDField="ID" DataTextField="Title" DataSource="@Model.Movies" AllowPaging="true" IsDatabasePaging="true" PageSize="@Model.PageSize" RecordCount="@Model.RecordCount" OnPageIndexChanged="@Url.Handler("Grid1_PageIndexChanged")" OnPageIndexChangedFields="Panel1">
// 每页显示记录数 public int PageSize { get; set; } = 5; // 总记录数 public int RecordCount { get; set; }
private async Task PrepareGridData(string searchMessage, int pageIndex) { IQueryable<Movie> q = _context.Movies; // 搜索框 searchMessage = searchMessage?.Trim(); if (!string.IsNullOrEmpty(searchMessage)) { q = q.Where(s => s.Title.Contains(searchMessage)); } RecordCount = await q.CountAsync(); // 对传入的 pageIndex 进行有效性验证 int pageCount = RecordCount / PageSize; if (RecordCount % PageSize != 0) { pageCount++; } if (pageIndex> pageCount - 1) { pageIndex = pageCount - 1; } if (pageIndex <0) { pageIndex = 0; } // 分页 q = q.Skip(pageIndex * PageSize).Take(PageSize); Movies = await q.ToListAsync(); }
public async Task OnGetAsync() { await PrepareGridData(string.Empty, 0); }
public async Task<IActionResult> OnPostGrid1_PageIndexChangedAsync(string[] Grid1_fields, int Grid1_pageIndex, string TBSearchMessage) { // 重新加载表格数据 await ReloadGrid(Grid1_fields, Grid1_pageIndex, TBSearchMessage); return UIHelper.Result(); } private async Task ReloadGrid(string[] Grid1_fields, int Grid1_pageIndex, string searchMessage) { await PrepareGridData(searchMessage, Grid1_pageIndex); var Grid1UI = UIHelper.Grid("Grid1"); // 设置总记录数 Grid1UI.RecordCount(RecordCount); // 设置分页数据 Grid1UI.DataSource(Movies, Grid1_fields); }
using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; namespace FineUICore.EmptyProject.RazorPages { public class MovieModel : PageModel { private readonly MovieContext _context; public MovieModel(MovieContext context) { _context = context; } public IList<Movie> Movies { get; set; } // 每页显示记录数 public int PageSize { get; set; } = 5; // 总记录数 public int RecordCount { get; set; } public async Task OnGetAsync() { await PrepareGridData(string.Empty, 0); } private async Task PrepareGridData(string searchMessage, int pageIndex) { IQueryable<Movie> q = _context.Movies; // 搜索框 searchMessage = searchMessage?.Trim(); if (!string.IsNullOrEmpty(searchMessage)) { q = q.Where(s => s.Title.Contains(searchMessage)); } RecordCount = await q.CountAsync(); // 对传入的 pageIndex 进行有效性验证 int pageCount = RecordCount / PageSize; if (RecordCount % PageSize != 0) { pageCount++; } if (pageIndex> pageCount - 1) { pageIndex = pageCount - 1; } if (pageIndex <0) { pageIndex = 0; } // 分页 q = q.Skip(pageIndex * PageSize).Take(PageSize); Movies = await q.ToListAsync(); } private async Task ReloadGrid(string[] Grid1_fields, int Grid1_pageIndex, string searchMessage) { await PrepareGridData(searchMessage, Grid1_pageIndex); var Grid1UI = UIHelper.Grid("Grid1"); // 设置总记录数 Grid1UI.RecordCount(RecordCount); // 设置分页数据 Grid1UI.DataSource(Movies, Grid1_fields); } public async Task<IActionResult> OnPostWindow1_CloseAsync(string[] Grid1_fields, int Grid1_pageIndex, string TBSearchMessage) { // 重新加载表格数据 await ReloadGrid(Grid1_fields, Grid1_pageIndex, TBSearchMessage); return UIHelper.Result(); } public async Task<IActionResult> OnPostGrid_RowDeleteAsync(string[] Grid1_fields, int Grid1_pageIndex, string TBSearchMessage, int deletedRowID) { var movie = await _context.Movies.FindAsync(deletedRowID); if (movie != null) { _context.Movies.Remove(movie); await _context.SaveChangesAsync(); // 重新加载表格数据 await ReloadGrid(Grid1_fields, Grid1_pageIndex, TBSearchMessage); } return UIHelper.Result(); } public async Task<IActionResult> OnPostTBSearchMessage_Trigger1Async(string[] Grid1_fields, int Grid1_pageIndex, string TBSearchMessage) { var TBSearchMessageUI = UIHelper.TwinTriggerBox("TBSearchMessage"); // 清空搜索框, 并隐藏清空图标 TBSearchMessageUI.Text(string.Empty); TBSearchMessageUI.ShowTrigger1(false); // 重新加载表格数据 await ReloadGrid(Grid1_fields, Grid1_pageIndex, string.Empty); return UIHelper.Result(); } public async Task<IActionResult> OnPostTBSearchMessage_Trigger2Async(string[] Grid1_fields, int Grid1_pageIndex, string TBSearchMessage) { var TBSearchMessageUI = UIHelper.TwinTriggerBox("TBSearchMessage"); if (string.IsNullOrEmpty(TBSearchMessage)) { TBSearchMessageUI.MarkInvalid("搜索文本不能为空!"); } else { // 显示清空图标 TBSearchMessageUI.ShowTrigger1(true); // 重新加载表格数据 await ReloadGrid(Grid1_fields, Grid1_pageIndex, TBSearchMessage); } return UIHelper.Result(); } public async Task<IActionResult> OnPostGrid1_PageIndexChangedAsync(string[] Grid1_fields, int Grid1_pageIndex, string TBSearchMessage) { // 重新加载表格数据 await ReloadGrid(Grid1_fields, Grid1_pageIndex, TBSearchMessage); return UIHelper.Result(); } } }
@page @model FineUICore.EmptyProject.RazorPages.MovieModel @{ ViewData["Title"] = "Movie"; } @section body { <f:Panel ID="Panel1" BodyPadding="10" ShowBorder="false" Layout="VBox" ShowHeader="false" Title="用户管理" IsViewPort="true"> <Items> <f:Form ShowBorder="false" ShowHeader="false"> <Rows> <f:FormRow> <Items> <f:TwinTriggerBox ID="TBSearchMessage" ShowLabel="false" EmptyText="在名称中搜索" Trigger1Icon="Clear" ShowTrigger1="false" Trigger2Icon="Search" OnTrigger1Click="@Url.Handler("TBSearchMessage_Trigger1")" OnTrigger1ClickFields="Panel1" OnTrigger2Click="@Url.Handler("TBSearchMessage_Trigger2")" OnTrigger2ClickFields="Panel1"> </f:TwinTriggerBox> <f:Label></f:Label> </Items> </f:FormRow> </Rows> </f:Form> <f:Grid ID="Grid1" BoxFlex="1" ShowBorder="true" ShowHeader="false" DataIDField="ID" DataTextField="Title" DataSource="@Model.Movies" AllowPaging="true" IsDatabasePaging="true" PageSize="@Model.PageSize" RecordCount="@Model.RecordCount" OnPageIndexChanged="@Url.Handler("Grid1_PageIndexChanged")" OnPageIndexChangedFields="Panel1"> <Columns> <f:RowNumberField /> <f:RenderField For="Movies.First().Title" ExpandUnusedSpace="true" /> <f:RenderField For="Movies.First().ReleaseDate" FieldFormat="yyyy-MM-dd" Width="200" /> <f:RenderField For="Movies.First().Genre" /> <f:RenderField For="Movies.First().Price" /> <f:RenderField Width="50" RendererFunction="renderActionEdit"></f:RenderField> <f:RenderField Width="50" RendererFunction="renderActionDelete"></f:RenderField> </Columns> <Toolbars> <f:Toolbar ID="Toolbar1" Position="Top"> <Items> <f:ToolbarFill></f:ToolbarFill> <f:Button ID="btnNew" IconFont="PlusCircle" Text="新增"> <Listeners> <f:Listener Event="click" Handler="onNewClick"></f:Listener> </Listeners> </f:Button> </Items> </f:Toolbar> </Toolbars> </f:Grid> </Items> </f:Panel> <f:Windows ID="Window1" IsModal="true" Hidden="true" Target="Top" EnableResize="true" EnableMaximize="true" EnableIFrame="true" Width="650" Height="400" OnClose="@Url.Handler("Window1_Close")" OnCloseFields="Panel1"> </f:Windows> } @section script { <script> function onNewClick(event) { F.ui.Window1.show('@Url.Content("~/MovieNew")', '新增'); } function renderActionEdit(value, params) { return '<a class="action-btn edit"href="javascript:;"><i class="f-icon f-icon-pencil f-grid-cell-iconfont"></a>'; } function renderActionDelete(value, params) { return '<a class="action-btn delete"href="javascript:;"><i class="f-icon f-icon-trash f-grid-cell-iconfont"></a>'; } F.ready(function () { var grid1 = F.ui.Grid1; grid1.el.on('click', 'a.action-btn', function (event) { var cnode = $(this); var rowData = grid1.getRowData(cnode.closest('.f-grid-row')); if (cnode.hasClass('delete')) { F.confirm({ message: '确定删除此记录?', target: '_top', ok: function () { F.doPostBack('@Url.Handler("Grid_RowDelete")', 'Panel1', { deletedRowID: rowData.id }); } }); } else if (cnode.hasClass('edit')) { F.ui.Window1.show('@Url.Content("~/MovieEdit/")' + rowData.id, '编辑'); } }); }); </script> }
F.doPostBack('@Url.Handler("Grid_RowDelete")', 'Panel1', { deletedRowID: rowData.id });
F.doPostBack('@Url.Handler("Movie_PostBack")', 'Panel1', { actionType: 'delete', deletedRowID: rowData.id });
OnTrigger1Click="@Url.Handler("TBSearchMessage_Trigger1")" OnTrigger1ClickFields="Panel1" OnTrigger2Click="@Url.Handler("TBSearchMessage_Trigger2")" OnTrigger2ClickFields="Panel1"
OnTrigger1Click="@Url.Handler("Movie_PostBack")" OnTrigger1ClickFields="Panel1" OnTrigger1ClickParameter1="@(new Parameter("actionType","trigger1", ParameterMode.String))" OnTrigger2Click="@Url.Handler("Movie_PostBack")" OnTrigger2ClickFields="Panel1" OnTrigger2ClickParameter1="@(new Parameter("actionType","trigger2", ParameterMode.String))"
@page @model FineUICore.EmptyProject.RazorPages.MovieModel @{ ViewData["Title"] = "Movie"; } @section body { <f:Panel ID="Panel1" BodyPadding="10" ShowBorder="false" Layout="VBox" ShowHeader="false" Title="用户管理" IsViewPort="true"> <Items> <f:Form ShowBorder="false" ShowHeader="false"> <Rows> <f:FormRow> <Items> <f:TwinTriggerBox ID="TBSearchMessage" ShowLabel="false" EmptyText="在名称中搜索" Trigger1Icon="Clear" ShowTrigger1="false" Trigger2Icon="Search" OnTrigger1Click="@Url.Handler("Movie_PostBack")" OnTrigger1ClickFields="Panel1" OnTrigger1ClickParameter1="@(new Parameter("actionType","trigger1", ParameterMode.String))" OnTrigger2Click="@Url.Handler("Movie_PostBack")" OnTrigger2ClickFields="Panel1" OnTrigger2ClickParameter1="@(new Parameter("actionType","trigger2", ParameterMode.String))"> </f:TwinTriggerBox> <f:Label></f:Label> </Items> </f:FormRow> </Rows> </f:Form> <f:Grid ID="Grid1" BoxFlex="1" ShowBorder="true" ShowHeader="false" DataIDField="ID" DataTextField="Title" DataSource="@Model.Movies" AllowPaging="true" IsDatabasePaging="true" PageSize="@Model.PageSize" RecordCount="@Model.RecordCount" OnPageIndexChanged="@Url.Handler("Movie_PostBack")" OnPageIndexChangedFields="Panel1" OnPageIndexChangedParameter1="@(new Parameter("actionType","page", ParameterMode.String))"> <Columns> <f:RowNumberField /> <f:RenderField For="Movies.First().Title" ExpandUnusedSpace="true" /> <f:RenderField For="Movies.First().ReleaseDate" FieldFormat="yyyy-MM-dd" Width="200" /> <f:RenderField For="Movies.First().Genre" /> <f:RenderField For="Movies.First().Price" /> <f:RenderField Width="50" RendererFunction="renderActionEdit"></f:RenderField> <f:RenderField Width="50" RendererFunction="renderActionDelete"></f:RenderField> </Columns> <Toolbars> <f:Toolbar ID="Toolbar1" Position="Top"> <Items> <f:ToolbarFill></f:ToolbarFill> <f:Button ID="btnNew" IconFont="PlusCircle" Text="新增"> <Listeners> <f:Listener Event="click" Handler="onNewClick"></f:Listener> </Listeners> </f:Button> </Items> </f:Toolbar> </Toolbars> </f:Grid> </Items> </f:Panel> <f:Windows ID="Window1" IsModal="true" Hidden="true" Target="Top" EnableResize="true" EnableMaximize="true" EnableIFrame="true" Width="650" Height="400" OnClose="@Url.Handler("Movie_PostBack")" OnCloseFields="Panel1" OnCloseParameter1="@(new Parameter("actionType","close", ParameterMode.String))"> </f:Windows> } @section script { <script> function onNewClick(event) { F.ui.Window1.show('@Url.Content("~/MovieNew")', '新增'); } function renderActionEdit(value, params) { return '<a class="action-btn edit"href="javascript:;"><i class="f-icon f-icon-pencil f-grid-cell-iconfont"></a>'; } function renderActionDelete(value, params) { return '<a class="action-btn delete"href="javascript:;"><i class="f-icon f-icon-trash f-grid-cell-iconfont"></a>'; } F.ready(function () { var grid1 = F.ui.Grid1; grid1.el.on('click', 'a.action-btn', function (event) { var cnode = $(this); var rowData = grid1.getRowData(cnode.closest('.f-grid-row')); if (cnode.hasClass('delete')) { F.confirm({ message: '确定删除此记录?', target: '_top', ok: function () { F.doPostBack('@Url.Handler("Movie_PostBack")', 'Panel1', { actionType: 'delete', deletedRowID: rowData.id }); } }); } else if (cnode.hasClass('edit')) { F.ui.Window1.show('@Url.Content("~/MovieEdit/")' + rowData.id, '编辑'); } }); }); </script> }
using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; namespace FineUICore.EmptyProject.RazorPages { public class MovieModel : PageModel { private readonly MovieContext _context; public MovieModel(MovieContext context) { _context = context; } public IList<Movie> Movies { get; set; } // 每页显示记录数 public int PageSize { get; set; } = 5; // 总记录数 public int RecordCount { get; set; } public async Task OnGetAsync() { await PrepareGridData(string.Empty, 0); } private async Task PrepareGridData(string searchMessage, int pageIndex) { IQueryable<Movie> q = _context.Movies; // 搜索框 searchMessage = searchMessage?.Trim(); if (!string.IsNullOrEmpty(searchMessage)) { q = q.Where(s => s.Title.Contains(searchMessage)); } RecordCount = await q.CountAsync(); // 对传入的 pageIndex 进行有效性验证 int pageCount = RecordCount / PageSize; if (RecordCount % PageSize != 0) { pageCount++; } if (pageIndex> pageCount - 1) { pageIndex = pageCount - 1; } if (pageIndex <0) { pageIndex = 0; } // 分页 q = q.Skip(pageIndex * PageSize).Take(PageSize); Movies = await q.ToListAsync(); } private async Task ReloadGrid(string[] Grid1_fields, int Grid1_pageIndex, string searchMessage) { await PrepareGridData(searchMessage, Grid1_pageIndex); var Grid1UI = UIHelper.Grid("Grid1"); // 设置总记录数 Grid1UI.RecordCount(RecordCount); // 设置分页数据 Grid1UI.DataSource(Movies, Grid1_fields); } public async Task<IActionResult> OnPostMovie_PostBackAsync(string actionType, string[] Grid1_fields, int Grid1_pageIndex, string TBSearchMessage, int deletedRowID) { var TBSearchMessageUI = UIHelper.TwinTriggerBox("TBSearchMessage"); if (actionType == "delete") { var movie = await _context.Movies.FindAsync(deletedRowID); if (movie == null) { Alert.Show("指定的电影不存在!"); return UIHelper.Result(); } else { _context.Movies.Remove(movie); await _context.SaveChangesAsync(); } } else if (actionType == "trigger1") { // 清空搜索框, 并隐藏清空图标 TBSearchMessageUI.Text(string.Empty); TBSearchMessageUI.ShowTrigger1(false); // 不要忘记设置搜索文本为空字符串 TBSearchMessage = string.Empty; } else if (actionType == "trigger2") { if (string.IsNullOrEmpty(TBSearchMessage)) { TBSearchMessageUI.MarkInvalid("搜索文本不能为空!"); return UIHelper.Result(); } else { // 显示清空图标 TBSearchMessageUI.ShowTrigger1(true); } } // actionType: page, close 无需特殊处理 // 重新加载表格数据 await ReloadGrid(Grid1_fields, Grid1_pageIndex, TBSearchMessage); return UIHelper.Result(); } } }
<f:Grid ID="Grid1" BoxFlex="1" ShowBorder="true" ShowHeader="false" DataIDField="ID" DataTextField="Title" DataSource="@Model.Movies" AllowPaging="true" IsDatabasePaging="true" PageSize="@Model.PageSize" RecordCount="@Model.RecordCount" OnPageIndexChanged="@Url.Handler("Movie_PostBack")" OnPageIndexChangedFields="Panel1" OnPageIndexChangedParameter1="@(new Parameter("actionType","page", ParameterMode.String))" AllowSorting="true" SortField="@Model.SortField" SortDirection="@Model.SortDirection" OnSort="@Url.Handler("Movie_PostBack")" OnSortFields="Panel1" OnSortParameter1="@(new Parameter("actionType","sort", ParameterMode.String))"> <Columns> <f:RowNumberField /> <f:RenderField For="Movies.First().Title" SortField="Title" ExpandUnusedSpace="true" /> <f:RenderField For="Movies.First().ReleaseDate" SortField="ReleaseDate" FieldFormat="yyyy-MM-dd" Width="200" /> <f:RenderField For="Movies.First().Genre" SortField="Genre" /> <f:RenderField For="Movies.First().Price" SortField="Price" /> <f:RenderField Width="50" RendererFunction="renderActionEdit"></f:RenderField> <f:RenderField Width="50" RendererFunction="renderActionDelete"></f:RenderField> </Columns> </f:Grid>
// 排序 q = q.SortBy(SortField + " " + SortDirection); // 分页 q = q.Skip(PageIndex * PageSize).Take(PageSize);
if (SortDirection == "DESC") { if (SortField == "Title") { q = q.OrderByDescending(q => q.Title); } else if (SortField == "ReleaseDate") { q = q.OrderByDescending(q => q.ReleaseDate); } else if (SortField == "Price") { q = q.OrderByDescending(q => q.Price); } else if (SortField == "Genre") { q = q.OrderByDescending(q => q.Genre); } } else { if (SortField == "Title") { q = q.OrderBy(q => q.Title); } else if (SortField == "ReleaseDate") { q = q.OrderBy(q => q.ReleaseDate); } else if (SortField == "Price") { q = q.OrderBy(q => q.Price); } else if (SortField == "Genre") { q = q.OrderBy(q => q.Genre); } }
@page @model FineUICore.EmptyProject.RazorPages.MovieModel @{ ViewData["Title"] = "Movie"; } @section body { <f:Panel ID="Panel1" BodyPadding="10" ShowBorder="false" Layout="VBox" ShowHeader="false" Title="用户管理" IsViewPort="true"> <Items> <f:Form ShowBorder="false" ShowHeader="false"> <Rows> <f:FormRow> <Items> <f:TwinTriggerBox ID="TBSearchMessage" ShowLabel="false" EmptyText="在名称中搜索" Trigger1Icon="Clear" ShowTrigger1="false" Trigger2Icon="Search" OnTrigger1Click="@Url.Handler("Movie_PostBack")" OnTrigger1ClickFields="Panel1" OnTrigger1ClickParameter1="@(new Parameter("actionType","trigger1", ParameterMode.String))" OnTrigger2Click="@Url.Handler("Movie_PostBack")" OnTrigger2ClickFields="Panel1" OnTrigger2ClickParameter1="@(new Parameter("actionType","trigger2", ParameterMode.String))"> </f:TwinTriggerBox> <f:Label></f:Label> </Items> </f:FormRow> </Rows> </f:Form> <f:Grid ID="Grid1" BoxFlex="1" ShowBorder="true" ShowHeader="false" DataIDField="ID" DataTextField="Title" DataSource="@Model.Movies" AllowPaging="true" IsDatabasePaging="true" PageSize="@Model.PageSize" RecordCount="@Model.RecordCount" OnPageIndexChanged="@Url.Handler("Movie_PostBack")" OnPageIndexChangedFields="Panel1" OnPageIndexChangedParameter1="@(new Parameter("actionType","page", ParameterMode.String))" AllowSorting="true" SortField="@Model.SortField" SortDirection="@Model.SortDirection" OnSort="@Url.Handler("Movie_PostBack")" OnSortFields="Panel1" OnSortParameter1="@(new Parameter("actionType","sort", ParameterMode.String))"> <Columns> <f:RowNumberField /> <f:RenderField For="Movies.First().Title" SortField="Title" ExpandUnusedSpace="true" /> <f:RenderField For="Movies.First().ReleaseDate" SortField="ReleaseDate" FieldFormat="yyyy-MM-dd" Width="200" /> <f:RenderField For="Movies.First().Genre" SortField="Genre" /> <f:RenderField For="Movies.First().Price" SortField="Price" /> <f:RenderField Width="50" RendererFunction="renderActionEdit"></f:RenderField> <f:RenderField Width="50" RendererFunction="renderActionDelete"></f:RenderField> </Columns> <Toolbars> <f:Toolbar ID="Toolbar1" Position="Top"> <Items> <f:ToolbarFill></f:ToolbarFill> <f:Button ID="btnNew" IconFont="PlusCircle" Text="新增"> <Listeners> <f:Listener Event="click" Handler="onNewClick"></f:Listener> </Listeners> </f:Button> </Items> </f:Toolbar> </Toolbars> </f:Grid> </Items> </f:Panel> <f:Windows ID="Window1" IsModal="true" Hidden="true" Target="Top" EnableResize="true" EnableMaximize="true" EnableIFrame="true" Width="650" Height="400" OnClose="@Url.Handler("Movie_PostBack")" OnCloseFields="Panel1" OnCloseParameter1="@(new Parameter("actionType","close", ParameterMode.String))"> </f:Windows> } @section script { <script> function onNewClick(event) { F.ui.Window1.show('@Url.Content("~/MovieNew")', '新增'); } function renderActionEdit(value, params) { return '<a class="action-btn edit"href="javascript:;"><i class="f-icon f-icon-pencil f-grid-cell-iconfont"></a>'; } function renderActionDelete(value, params) { return '<a class="action-btn delete"href="javascript:;"><i class="f-icon f-icon-trash f-grid-cell-iconfont"></a>'; } F.ready(function () { var grid1 = F.ui.Grid1; grid1.el.on('click', 'a.action-btn', function (event) { var cnode = $(this); var rowData = grid1.getRowData(cnode.closest('.f-grid-row')); if (cnode.hasClass('delete')) { F.confirm({ message: '确定删除此记录?', target: '_top', ok: function () { F.doPostBack('@Url.Handler("Movie_PostBack")', 'Panel1', { actionType: 'delete', deletedRowID: rowData.id }); } }); } else if (cnode.hasClass('edit')) { F.ui.Window1.show('@Url.Content("~/MovieEdit/")' + rowData.id, '编辑'); } }); }); </script> }
using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; namespace FineUICore.EmptyProject.RazorPages { public class MovieModel : PageModel { private readonly MovieContext _context; public MovieModel(MovieContext context) { _context = context; } public IList<Movie> Movies { get; set; } // 当前所在的页 public int PageIndex { get; set; } = 0; // 每页显示记录数 public int PageSize { get; set; } = 5; // 总记录数 public int RecordCount { get; set; } // 排序字段名称 public string SortField { get; set; } = "Title"; // 排序方向(DESC: 倒序, ASC: 正序) public string SortDirection { get; set; } = "DESC"; public async Task OnGetAsync() { await PrepareGridData(string.Empty); } private async Task PrepareGridData(string searchMessage) { IQueryable<Movie> q = _context.Movies; // 搜索框 searchMessage = searchMessage?.Trim(); if (!string.IsNullOrEmpty(searchMessage)) { q = q.Where(s => s.Title.Contains(searchMessage)); } RecordCount = await q.CountAsync(); // 对传入的 pageIndex 进行有效性验证 int pageCount = RecordCount / PageSize; if (RecordCount % PageSize != 0) { pageCount++; } if (PageIndex> pageCount - 1) { PageIndex = pageCount - 1; } if (PageIndex <0) { PageIndex = 0; } // 排序 q = q.SortBy(SortField + " " + SortDirection); // 分页 q = q.Skip(PageIndex * PageSize).Take(PageSize); Movies = await q.ToListAsync(); } private async Task ReloadGrid(string[] Grid1_fields, string searchMessage) { await PrepareGridData(searchMessage); var Grid1UI = UIHelper.Grid("Grid1"); // 设置总记录数 Grid1UI.RecordCount(RecordCount); // 设置分页数据 Grid1UI.DataSource(Movies, Grid1_fields); } public async Task<IActionResult> OnPostMovie_PostBackAsync(string actionType, string[] Grid1_fields, int Grid1_pageIndex, string Grid1_sortField, string Grid1_sortDirection, string TBSearchMessage, int deletedRowID) { var TBSearchMessageUI = UIHelper.TwinTriggerBox("TBSearchMessage"); if (actionType == "delete") { var movie = await _context.Movies.FindAsync(deletedRowID); if (movie == null) { Alert.Show("指定的电影不存在!"); return UIHelper.Result(); } else { _context.Movies.Remove(movie); await _context.SaveChangesAsync(); } } else if (actionType == "trigger1") { // 清空搜索框, 并隐藏清空图标 TBSearchMessageUI.Text(string.Empty); TBSearchMessageUI.ShowTrigger1(false); // 不要忘记设置搜索文本为空字符串 TBSearchMessage = string.Empty; } else if (actionType == "trigger2") { if (string.IsNullOrEmpty(TBSearchMessage)) { TBSearchMessageUI.MarkInvalid("搜索文本不能为空!"); return UIHelper.Result(); } else { // 显示清空图标 TBSearchMessageUI.ShowTrigger1(true); } } // actionType: page, close 无需特殊处理 PageIndex = Grid1_pageIndex; SortField = Grid1_sortField; SortDirection = Grid1_sortDirection; // 重新加载表格数据 await ReloadGrid(Grid1_fields, TBSearchMessage); return UIHelper.Result(); } } }
来源: https://www.cnblogs.com/sanshi/p/12441884.html