Serenity框架官方文档翻译3.1(教程)

3.教程

3.1教程: Movie Database

我们来用Serenity创建一个和IMDB相似的编辑界面的站点。

你能在下面的站点找到教程的源代码:

https://github.com/volkanceylan/Serenity-Tutorials/tree/master/MovieTutorial

创建一个新的项目名称为 MovieTutorial

在Visual Studio 点击 File -> New Project. 确保你选择了 Serene template. 输入 MovieTutorial 作为名称点击 OK.

在解决方案资源管理器中, 你应该看到两个名称为MovieTutorial.WebMovieTutorial.Script工程文件。

确保 MovieTutorial.Web 是启动项目 (会被加粗), 假如没有,点击 设为启动项目

这些项目文件是什么?

Serenity 应用程序通常有至少两个项目。一个用于服务器端代码+ css等静态资源文件,图片等。(MovieTutorial.Web),一个用于客户端代码(MovieTutorial.Script)。

MovieTutorial。脚本看起来像一个普通的c#类库,但是它所包含的代码实际上都使用Saltarelle编译为Javascript。

它的输出(MovieTutorial.Script.js)将复制到文件夹/网站/脚本MovieTutorial.Web之下。所以在运行时,只有MovieTutorial。使用Web项目。

添加项目的依赖

默认情况下,当你按F5运行Web项目,Visual Studio只会构建MovieTutorial 。

这也可以通过设置改变在 Visual Studio Options -> Projects and Solutions -> Build And Run -> "运行时仅仅构建启动项目"。不建议去改变它。

要使脚本项目在运行时也生成 Web 项目,右击 MovieTutorial .Web 项目, 在依存关系选项卡下单击生成依赖项-> 项目依赖项和检查 MovieTutorial .Script。

不幸的是,我们没有办法可以在Serene的模板中设置该依赖项。

3.1.1创建Movie

为了存储电影列表,我们需要一个Movie  。 我们可以使用类似 SQL 管理工具的老派的技术创建此表,但我们更喜欢使用Fluent Migrator创建一个迁移

Fluent Migrator是一个和Ruby on Rails Migrations很相似的.NET迁移框架。

迁移是一个使用结构化的方式来改变你的数据库架构,创建大量的必须通过涉及每个开发人员手动运行的 sql 脚本的替代方法

迁移解决了为多个数据库 (例如,开发人员的本地数据库,测试数据库和生产数据库)架构的演变问题。数据库架构更改介绍类写在 C# 中,可以签入到版本控制系统中。

查看https://github.com/schambers/fluentmigrator FluentMigrator的更多信息。

用解决方案资源管理器导航到MovieTutorial.Web / Modules / Common / Migrations / DefaultDB.

29886a23-8764-46d1-aeca-19b72aa2eb0e

这里已经有三个迁移了。迁移就像一个操纵数据库结构的DML脚本。

DefaultDB_20141103_140000_Initial.cs迁移 包含了我们的初始化创建Northwind tables和 Users 表.

在同一个目录下创建一个新的名字为DefaultDB_20150915_185137_Movie.cs迁移文件. 你能复制并且改变已经存在的迁移文件,包括重命名和改变内容。

迁移文件名称 / 类名是其实不重要,但建议一致和正确排序。

20150915_185137 对应于我们在迁移时间的 yyyyMMdd_HHmmss 格式。它也将作为这种迁移的唯一键。

我们的迁移应该看起来像下面这样子:

using FluentMigrator;
using System;

namespace MovieTutorial.Migrations.DefaultDB
{
    [Migration(20150915185137)]
    public class DefaultDB_20150915_185137_Movie : Migration
    {
        public override void Up()
        {
            Create.Schema("mov");

            Create.Table("Movie").InSchema("mov")
                .WithColumn("MovieId").AsInt32().Identity().PrimaryKey().NotNullable()
                .WithColumn("Title").AsString(200).NotNullable()
                .WithColumn("Description").AsString(1000).Nullable()
                .WithColumn("Storyline").AsString(Int32.MaxValue).Nullable()
                .WithColumn("Year").AsInt32().Nullable()
                .WithColumn("ReleaseDate").AsDateTime().Nullable()
                .WithColumn("Runtime").AsInt32().Nullable();    
        }

        public override void Down()
        {
        }
    }
}

请确保您使用的命名空间 MovieTutorial.Migrations.DefaultDB因为Serene模板只在此命名空间中适用于默认数据库的迁移。

在 Up() 方法中我们指定这种迁移,应用时,将创建名为 mov 的架构。我们将使用单独的架构,为电影表以避免与现有的表的冲突。它还将创建带有"MovieId, Title, Description..."字段的表Movie

我们可以实现 Down() 方法,使它能够撤消此迁移 (drop movie table 和 mov 架构等),但为范围的此示例中,让我们将它保留为空。

无法撤消迁移可能会无关痛痒,但误删除表可以造成更多的伤害。

在类上面我们打上迁移特性。

[Migration(20150915185137)]

此选项指定为此迁移的唯一键。迁移应用到数据库后,其关键记录在特殊表特定于 FluentMigrator ([dbo].[VersionInfo]),因此不会再应用相同的迁移。

Migration key should be in sync with class name (for consistency) but without underscore as migration keys are Int64 numbers.

迁移的主键应该是与类不带下划线的名称 (以保持一致性) 一致

迁移是按key顺序执行,所以为迁移键使用一个可排序的像 yyyyMMdd 日期时间模式看起来像个好主意。

运行迁移

默认情况下,Serene模板运行 MovieTutorial.Migrations.DefaultDB 命名空间中的所有的迁移。这会自动在应用程序启动时发生。运行迁移的代码在 App_Start/SiteInitialization.cs 文件中 ︰

public static partial class SiteInitialization
{
    public static void ApplicationStart()
    {
        // ...
        EnsureDatabase();
    }

    private static void EnsureDatabase()
    {
        // ...
        RunMigrations();
    }

    private static void RunMigrations()
    {
        var defaultConnection = SqlConnections.GetConnectionString("Default");

        // safety check to ensure that we are not modifying another databaseif (defaultConnection.ConnectionString.IndexOf(
            typeof(SiteInitialization).Namespace + @"_Default_v1") < 0)
            return;

        using (var sw = new StringWriter())
        {
            //...var runner = new RunnerContext(announcer)
            {
                // ...
                Namespace = "MovieTutorial.Migrations.DefaultDB"
            };

            new TaskExecutor(runner).Execute();
        }
    }

还有一次安全检查数据库名称,以避免在一些非默认Serene 数据库 (MovieTutorial_Default_v1) 的任意数据库上运行迁移。如果您了解的风险,您可以删除此检查。例如,如果您更改 web.config 中的默认连接到您自己的生产数据库,迁移将在其上运行,即使你不想要你将会有Northwind等数据库的表。

现在按F5 来运行你的程序并且在默认的数据库中创建 Movie表。

验证迁移运行。

用Sql Server Management Studio 或则Visual Studio -> Connection To Database,连接到 (localdb)\v11.0. 数据库服务 MovieTutorial_Default_v1

(localdb)\v11.0是一个通过 SQL Server 2012 LocalDB创建的实例LocalDB。

假如你还没安装LocalDB, 你可以从https://www.microsoft.com/en-us/download/details.aspx?id=29062下载它。

假如你还有SQL Server 2014 LocalDB,你的服务名称将会是 (localdb)\MSSqlLocalDB or (localdb)\v12.0, 所以在web.config 文件中改变连接字符串。

你也可以用其他SQL server 实例,静静需要改变连接字符串到默认的目标数据库并且移除迁移安全检查。

你能够在SQL server资源管理器中看到[mov].[Movies] 表。

你也可以查看 [dbo].[VersionInfo] 表的数。,Version列最后一行应该是20150915185137。此参数指定与该版本号 (迁移密钥) 迁移已经在此数据库上执行。

通常情况下,你不需要每个迁移后做这个检查。在这里我们展示这些解释去哪里找,以防万一你在将来会有任何麻烦。

3.1.2为Movie Table生成代码

Serenity 代码生成

在你确定数据库中已经存在表后,我们将会用Serenity Code Generator (sergen.exe) 来生成初始化编辑界面。

在Visual Studio中,通过单击View => Other Windows => Package Manager Console.打开包管理器控制台。

Type sergen and press Enter.

程序包管理器控制台有时不能正确设置路径,你可能会得到执行 Sergen 时出错。重新启动 Visual Studio 可能会解决此问题。

另一个选项是从 Windows 资源管理器中打开 Sergen.exe。右键点击 MovieTutorial 解决方案在解决方案资源管理器中,单击打开在文件浏览器。Sergen.exe 在packages\Serenity.CodeGenerator.X.Y.Z\tools 目录下。

f0527296-5285-47b6-aba7-54c20693d50d

设置项目位置

当你首次运行Sergen, Web Project 和Script Project 将会预加载字段. 假如你用的是比Serene  1.6.2f更老的版本 ,请按照下面的步骤来做:

通过用using "..." 按钮浏览你的解决方案和本地 web和 script项目路径。

另一个选择是将它们设置为以下值 ︰

  • ..\..\..\MovieTutorial\MovieTutorial.Web\MovieTutorial.Web.csproj
  • ..\..\..\MovieTutorial\MovieTutorial.Script\MovieTutorial.Script.csproj

如果您使用另一个项目名称 MovieTutorial,例如 MyMovies,用它替换 MovieTutorial。

一旦你色字了这个值并且在第一页生成了,你没必要再次设置。 这个选项被储存在Serenity.CodeGenerator.config在你的解决方案文件夹。

这个值时必填的因为 Sergen 将会在你的项目中包含生成的文件

根 Namespace 选项

将根命名空间选项设置为使用解决方案名称,例如 MovieTutorial。如果您的项目名称是 MyProject.Web 和 MyProject.Script,您的根命名空间默认情况下是 MyProject。这是关键的所以请确保你不要将其设置为其他的任何东西,因为默认情况下,Serene模板期望所有生成的代码要在这个根命名空间下面。

选择连接字符串

一旦你设置了项目名称, Sergen 填充与 web.config 文件中的连接字符串连接到下拉列表。可能有 DefaultNorthwind在里面, 选择 Default.

选择要为其生成代码的表

Sergen一次为一个表生成代码. 一旦你选择了连接字符串,表下拉框从数据库中填充表名称。

选择Movie 表.

设置模块名称

在 Serenity 术语中, 一个模块是页面的逻辑组。

比如:在Serene 模版中, 所有的有关于Northwind页面都属于 Northwind 模块。

像一般与管理有关的网站,用户一样,角色等属于管理模块的页面。

一个模块通常对应于数据库架构,或单个数据库,但是不阻止你在一个单一的数据库中使用多个模块 / 架构或者相反的在一个模块中使用多个数据库。

本教程中,我们将为所有页面使用 MovieDB (类似于 IMDB)。

模块名称用于确定命名空间和生成的页面的 url。

例如,我们新的一页将在 MovieTutorial.MovieDB 命名空间下,将使用 /MovieDB 相对 url。

连接Key参数

连接Key是一个在 web.config 文件中设置为选定的连接字符串。你通常不需要更改它,只是保留默认值。

实体标志

This usually corresponds to the table name but sometimes table names might have underscores or other invalid characters, so you decide what to name your entity in generated code (a valid identifier name).

这通常对应于表名称但有时表名称可能有下划线或其他无效的字符,所以你决定在生成的代码 (一个有效的标识符名称)时怎么样命名实体。

从 Serene 1.6.2+ 开始实体标志自动的使用pascalized版本的表名。

我们的表名是 Movie所以它在C#标识符里面也是个有效的表名 ,所以让我们用 Movie 作为实体标志。我们的实体类将被命名为 MovieRow.

这个名字也会在其他的类里面用到。This name is also used in other class names.比如说我们的控制器名称为MovieController, 它也会确定页面url名称,比如说编辑页面将会是 URL /MovieDB/Movie.

权限Key

在 Serenity,对资源 (页面、 服务等) 的访问控制受权限键名为简单的字符串。用户或角色被授予这些权限。

我们影片页面将仅使用由administrative 用户 (或也许以后是内容版主) 因此,让我们设置它为现在的Administration 。默认情况下,在Serene 的模板中,只有管理员用户具有此权限。

为第一页面生成代码

在展示的上图中设置了参数之后。点击Generate Code for Entity 按钮. Sergen 将会生成几个 文件并且包含进 MovieTutorial.Web 和MovieTutorial.Script 项目中.

现在你可以关闭 Sergen,返回Visual Studio。

因为项目被修改了所以Visual Studio 会询问你是否重新加载点击重新加载所有.

重新生成解决方案F5 启动项目

确保你从新生成解决方案,通过右键点击解决方案名称从新生成. 一些用户报告说在生成代码后他们得到了一个空的页面可能是脚本项目没有编译的原因. 你应该显示的编译MovieTutorial.Script 项目. 他会被输出来重置在 MovieTutorial.Web/Scripts/site路径下的文件.

另一种选择是添加某个项目依赖项。要使脚本项目也在 Web 项目运行时生成,右击 MovieTutorial.Web 项目,依存关系选项卡下单击生成依赖项-> 项目依赖项和检查 MovieTutorial.Script 。

用admin as 用户名, serenity作为登录密码.

当你看到欢迎界面的时候你会注意到有一个新的菜单节 MovieDB在导航菜单的底部。

点击展开并且单击 Movie来打开我们第一个生成的页面。

a42355eb-6ed4-40d1-8d13-8d186f70e366

现在试着添加一个新的movie, 然后试着更新和删除它。

我们不用写一行代码,Sergen 会给我们的表生成代码。

这并不意味着我不太喜欢写代码。与此相反的是,我爱它。其实我不是大多数设计师和代码生成器的粉丝,他们产生的代码是混乱的。

Sergen 只被帮助我们在这里初次安装所需要的分层的体系结构和平台标准。我们要创建实体、 存储库、 页、 终结点、 网格、 形式等约 10 个文件。我们还要做一些设置在其他一些地方。

即使我们做复制粘贴,从一些其他页面的代码替换,大概需要 5-10 分钟而且还容易出错。

Sergen 生成的代码文件中也包含绝对基础的最少的代码。这是因为Serenity 基类在处理大多数逻辑。一旦我们为一些表生成代码,我们可能永远不会再一次 (为此表),使用 Sergen,我们需要修改生成的代码。我们将看到如何做。

3.1.3自定义Movie界面

自定义字段标题

在我们的movie 网格和窗体,我们有一个名为Runtime字段。这个字段预计是几分钟的一个整数数字,但是标题没有这个迹象。让我们改变他的标题为 Runtime (mins)。

有几种方法做到这一点。我们的选择包括服务器端窗体定义的服务器端列定义,从脚本网格代码等。但让这种变化在中央的位置,该实体本身,所以其标题的变化无处不在。

当 Sergen 生成的代码为Movie 表时,它创建一个名为 MovieRow 的实体类。 你可以在 MovieTutorial.Web/Modules/MovieDB/Movie/MovieRow.cs 上找到它。

这里是一个Runtime 属性的源代码摘录:

namespace MovieTutorial.MovieDB.Entities
{
    // ...
    [ConnectionKey("Default"), DisplayName("Movie"), InstanceName("Movie"), 
     TwoLevelCached]
    public sealed class MovieRow : Row, IIdRow, INameRow
    {
        // ...
        [DisplayName("Runtime")]
        public Int32? Runtime
        {
            get { return Fields.Runtime[this]; }
            set { Fields.Runtime[this] = value; }
        }
        //...
    }
}

我们会在之后谈论实体 (或行),让我们现在专注于我们的目标并更改其显示名称特性值和  *Runtime (mins)":

namespace MovieTutorial.MovieDB.Entities
{
    // ...
    [ConnectionKey("Default"), DisplayName("Movie"), InstanceName("Movie"), 
     TwoLevelCached]
    public sealed class MovieRow : Row, IIdRow, INameRow
    {
        // ...
        [DisplayName("Runtime (mins)")]
        public Int32? Runtime
        {
            get { return Fields.Runtime[this]; }
            set { Fields.Runtime[this] = value; }
        }
        //...
    }
}

现在生成解决方案并运行应用程序。你会看到字段标题在网格和对话框中改变了。

列标题中有"...",当列不够宽,虽然其提示显示完整标题。我们将看到如何处理这很快。

e9ac3590-94f1-4c9e-bdee-cb7081c6c1a1

重写列标题和宽

迄今为止一切都很好,如果我们想要显示网格 (列) 或对话框 (窗体) 中的另一个标题。我们可以重写它相应的定义文件。

让我们先在列上面做。在 MovieRow.cs,你可以找到一个名为 MovieColumns.cs 的源代码文件 ︰

namespace MovieTutorial.MovieDB.Columns
{
    // ...

    [ColumnsScript("MovieDB.Movie")]
    [BasedOnRow(typeof(Entities.MovieRow))]
    public class MovieColumns
    {
        [EditLink, DisplayName("Db.Shared.RecordId"), AlignRight]
        public Int32 MovieId { get; set; }
        //...public Int32 Runtime { get; set; }
    }
}

你可能会注意到这一列定义基于Movie实体 (BasedOnRow 特性)。

写在这里的任何属性将覆盖实体类中定义的特性。

让我们添加一个DisplayName特性到 Runtime 属性中:

namespace MovieTutorial.MovieDB.Columns
{
    // ...

    [ColumnsScript("MovieDB.Movie")]
    [BasedOnRow(typeof(Entities.MovieRow))]
    public class MovieColumns
    {
        [EditLink, DisplayName("Db.Shared.RecordId"), AlignRight]
        public Int32 MovieId { get; set; }
        //...
        [DisplayName("Runtime in Minutes"), Width(150), AlignRight]
        public Int32 Runtime { get; set; }
    }
}

现在我们可以设置标题为"Runtime in Minutes".

我们实际上可以添加多于两个的 attributes.

一个来重写列宽度为150px.

Serenity 适用于基于字段类型和字符长度的列自动宽度,除非您显式设置宽度

另一个为列右对齐 (AlignCenter,AlignLeft 也是可用的)。

重新生成和运行:

8a69bc73-1994-40e1-9421-df2781c73daf

表单字段相同,但是列标题已经改变了。

如果我们想要重写窗体字段标题,我们在 MovieForm.cs做类似的步骤。

为Description 和Storyline 改变编辑器类型说明

Description 和Storyline 字段可以相比标题字段长一点,所以,让我们将他们编辑器类型更改为文本区域。

在MovieColumns.csMovieRow.cs同一个文件夹下打开 MovieForm.cs

namespace MovieTutorial.MovieDB.Forms
{
    //...
    [FormScript("MovieDB.Movie")]
    [BasedOnRow(typeof(Entities.MovieRow))]
    public class MovieForm
    {
        public String Title { get; set; }
        public String Description { get; set; }
        public String Storyline { get; set; }
        public Int32 Year { get; set; }
        public DateTime ReleaseDate { get; set; }
        public Int32 Runtime { get; set; }
    }
}

都添加extAreaEditor 特性

namespace MovieTutorial.MovieDB.Forms
{
    //...
    [FormScript("MovieDB.Movie")]
    [BasedOnRow(typeof(Entities.MovieRow))]
    public class MovieForm
    {
        public String Title { get; set; }
        [TextAreaEditor(Rows = 3)]
        public String Description { get; set; }
        [TextAreaEditor(Rows = 8)]
        public String Storyline { get; set; }
        public Int32 Year { get; set; }
        public DateTime ReleaseDate { get; set; }
        public Int32 Runtime { get; set; }
    }
}

我留了更多的编辑行给 Storyline (8)因为他相比 Description (3)有更长的字段。

在重新创建和运行之后我们得到了这个:

a6fc0f92-96d8-40df-b9a3-3401acc824a9

Serene有几个编辑类型给表单选择。有些是自动选取基于字段的数据类型,而您需要显式设置别的。

您也可以开发您自己的编辑器类型。你可以采取现有编辑器类型的基类,或从stratch开发自己的,我们将在下面的章节中看到。

当编辑变得高一点,窗体高度超出默认的Serenity 窗体高度 (这设置为 Sergen 260px),所以我们有一个垂直滚动条。让我们将其移除。

用CSS(LESS)初始化设置对话框的大小

Sergen在MovieTutorial.Web/Content/site/site.less 文件中为movie对话框生成一些 CSS 。

如果你打开它并且滚动到最下面,你将会看到下面这些:

/* ------------------------------------------------------------------------- *//* APPENDED BY CODE GENERATOR, MOVE TO CORRECT PLACE AND REMOVE THIS COMMENT *//* ------------------------------------------------------------------------- */

.s-MovieDialog {
    > .size { .widthAndMin(650px); }
    .dialog-styles(@h: auto, @l: 150px, @e: 400px);
    .s-PropertyGrid .categories { height: 260px; }
}

你可以安全地删除 3 注释行 (由代码生成器添加...)。这是只是提醒您准备将它们移动到特定于此模块 (推荐)来说更好的地方,像一个 site.movies.less 文件。

这些规则将应用于.s-MovieDialog 类的元素。These rules are applied to elements with .s-MovieDialog class.。我们的Movie 对话框默认有此类。

在第二行指定此对话框是 650px 宽 (,还其最小宽度 650px,这将会获得一些意义后我们让此对话框可调整大小)。

在第三行中,我们指定对话框高度应自动 (@h ︰ 自动),字段标签应该是 150px (@l: 150px) 和编辑应该是在宽度 400px (@e: 400px)。

我们窗体的高度由s-PropertyGrid .categories { height: 260px; } 行指定。我们将其更改为 400px 所以它不会需要一个垂直滚动条。

.s-MovieDialog {
    > .size { .widthAndMin(650px); }
    .dialog-styles(@h: auto, @l: 150px, @e: 400px);
    .s-PropertyGrid .categories { height: 400px; }
}
改变页面标题

我们的页面标题为 Movie. 让我们把他改为Movies.

再次打开 MovieRow.cs

namespace MovieTutorial.MovieDB.Entities
{
    // ...
    [ConnectionKey("Default"), DisplayName("Movie"), InstanceName("Movie"), 
     TwoLevelCached]
    public sealed class MovieRow : Row, IIdRow, INameRow
    {
        [DisplayName("Movie Id"), Identity]
        public Int32? MovieId

改变显示名字DisplayName 特性为Movies.这是引用了此表时, 使用的名称,它通常是复数名称。此特性用于确定默认页面标题。

它也是可以在 MoviePage.Index.cshtml 文件中重写页标题的,但在之前,我们更喜欢从一个中央位置重写因此此信息可以在其他地方重复使用。

InstanceName corresponds to singular name and is used in New Record (New Movie) button of the grid and determines the dialog title (e.g. Edit Movie).

InstanceName对应单数名称和在新记录 (新电影)网格 按钮的使用,确定对话框标题 (例如编辑电影)。

namespace MovieTutorial.MovieDB.Entities
{
    // ...
    [ConnectionKey("Default"), DisplayName("Movies"), InstanceName("Movie"), 
     TwoLevelCached]
    public sealed class MovieRow : Row, IIdRow, INameRow
    {
        [DisplayName("Movie Id"), Identity]
        public Int32? MovieId

3.1.4Handing Movie Navigation

设置导航项目的标题和图标

当Sergen  为Movie  表现层生成代码后,它也创建了一个导航条目。在Serene 里面导航条目可以用assembly特性生成。

在同一个文件夹下打开MoviePage.cs,顶部有一行:

[assembly:Serenity.Navigation.NavigationLink(int.MaxValue, "MovieDB/Movie", 
    typeof(MovieTutorial.MovieDB.Pages.MovieController))]

namespace MovieTutorial.MovieDB.Pages
{
    using Serenity;
    using Serenity.Web;

对此特性的第一个参数是此导航项的显示顺序。由于我们只在电影类别中有一个导航项目,我们的顺序还不会混乱。

第二个参数是导航标题"节标题/链接标题"格式。节和导航项目被以斜杠 (/) 分隔。

我们来改变它到Movie 数据库/Movies。

[assembly:Serenity.Navigation.NavigationLink(int.MaxValue, "Movie Database/Movies", 
    typeof(MovieTutorial.MovieDB.Pages.MovieController), icon: "icon-camrecorder")]

namespace MovieTutorial.MovieDB.Pages
{

//..

movies_navigation_links_30

我们也改的导航项目图标为 icon-camcorder。Serene 的模板有两套字体图标,Simple Line Icons 和Font Awesome。在这里我们使用simple line icons的glyph。

若要查看simple line icons 和他们的 css 类,请访问以下链接 ︰

http://thesabbir.github.io/simple-line-icons/

FontAwesome能在这儿看到:

https://fortawesome.github.io/Font-Awesome/icons/

排序导航节

因为我们的Movie Database 节是最后自动生成的,它显示在导航菜单的最底下。

我们将要把它移动到Northwind前面。

因为我们之前看到,Sergen 在MoviePage.cs创建了导航项如果导航项目都分散到这样的页面。它将很难看到大的图片 (所有导航项目列表) 和很容易命令他们。

所以我们将它移动到我们中央的位置,即在 MovieTutorial.Web/Modules/Common/Navigation/NavigationItems.cs。

仅仅剪贴以下几行from MoviePage.cs:

[assembly:Serenity.Navigation.NavigationLink(int.MaxValue, "Movie Database/Movies", 
    typeof(MovieTutorial.MovieDB.Pages.MovieController), icon: "icon-camrecorder")]

把它移进NavigationItems.cs 并且把它改为这样:

using Serenity.Navigation;
using Northwind = MovieTutorial.Northwind.Pages;
using Administration = MovieTutorial.Administration.Pages;
using MovieDB = MovieTutorial.MovieDB.Pages;

[assembly: NavigationLink(1000, "Dashboard", url: "~/", permission: "",
    icon: "icon-speedometer")]

[assembly: NavigationMenu(2000, "Movie Database", icon: "icon-film")]
[assembly: NavigationLink(2100, "Movie Database/Movies", 
    typeof(MovieDB.MovieController), icon: "icon-camcorder")]

[assembly: NavigationMenu(8000, "Northwind", icon: "icon-anchor")]
[assembly: NavigationLink(8200, "Northwind/Customers", 
    typeof(Northwind.CustomerController), icon: "icon-wallet")]
[assembly: NavigationLink(8300, "Northwind/Products", 
    typeof(Northwind.ProductController), icon: "icon-present")]
// ...

在这里我们也申明了一个电影图标的导航菜单 (Movie 数据库)。当你没有显式定义的导航菜单时,Serenity 隐式地创建一个,但在这种情况下你不能自己排序菜单,或设置菜单图标。

我们分配了他的导航排序为2000,因此它在Dashboard(1000)之后但是在Northwind 菜单(8000)之前。

我们分配我们 Movies为2100显示顺序值但是现在没关系,由于目前我们尚未下Movie 数据库菜单的只有一个导航项目。

第一级别的链接和导航菜单先按照它们的显示顺序排序,然后第二个层级按照他们的兄弟姐妹之间的联系排序。

这些改变看起来像下面这样子:movies_navigation_moved_30

Visual studio 的一些问题故障排除

万一你没有注意到已经,当运行您的网站时Visual Studio 不让你修改代码。当你停止调试的时候您的网站也会停止,所以您不能浏览器窗口保持打开状态并重建后刷新。

要解决这个问题,我们需要禁用编辑,继续 (不知道为什么)。

右键点击MovieTutorial.Web project,点击属性 Properties,在 Web 标签, 取消勾选Enable Edit And ContinueDebuggers下面。

此外,在您的网站,顶部的蓝色进度栏 (即 Pace.js 动画),保持运行所有的时间像它仍在加载的东西。正是由于 Visual Studio 的浏览器链接功能。要禁用它,在 Visual Studio 工具栏,看上去像刷新按钮 (在播放图标与像 Chrome 浏览器名称),单击下拉列表,并取消勾选启用浏览器链接中找到它的按钮。

也可以用一个 web.config 设置禁用它

<appsettings><add key="vs:EnableBrowserLink" value="false" /></appsettings>

Serene 1.5.4 和日后会默认设置,所以你不可能会遇到此问题

3.1.5个性化快速搜索

添加几个条目。

以下各节中,我们需要一些示例数据。我们可以从 IMDB复制和粘贴一些。

如果你不想浪费您的时间进入该示例数据,下面的链接可以作为一个迁移使用 ︰

https://github.com/volkanceylan/Serenity-Tutorials/blob/master/MovieTutorial/MovieTutorial/MovieTutorial.Web/Modules/Common/Migrations/DefaultDB/DefaultDB_20150923_172700_SampleMovies.cs

movies_data_11

假如我们在搜索框中输入我们能看到两个电影被过滤了: The Good, the Bad and the UglyThe Godfather.

如果输入Gandalf我们将不会得到任何数据

默认情况下,Sergen确定第一个文本字段的表作为名称字段。在movies 表,它是Title。这一字段有一个快速搜索特性指定,应该对它执行文本搜索。

这个名称字段也指定了初始的排序和编辑窗口的字段标题。

有时,第一个文本列可能不是名称字段。如果你想要改变到另一个字段,你可以在 MovieRow.cs 中 ︰


namespace MovieTutorial.MovieDB.Entities
{
    //...
    public sealed class MovieRow : Row, IIdRow, INameRow
    {
        //...
        StringField INameRow.NameField
        {
            get { return Fields.Title; }
        }
}

代码生成器确定我们表中的Title是第一个文本 (字符串) 字段。所以它到我们Movies 行添加一个 INameRow 接口和实现通过返回标题字段。如果想要使用Description 为名称字段,我们可以替换它。

在这里,Title 是实际上名称字段,所以我们将其保持原样。但我们想要Serenity ,也搜索DescriptionStoryline 。要做到这一点,您需要将快速搜索特性也添加到这些字段,如下所示 ︰

namespace MovieTutorial.MovieDB.Entities
{
    //...
    public sealed class MovieRow : Row, IIdRow, INameRow
    {
        //...
        [DisplayName("Title"), Size(200), NotNull, QuickSearch]
        public String Title
        {
            get { return Fields.Title[this]; }
            set { Fields.Title[this] = value; }
        }

        [DisplayName("Description"), Size(1000), QuickSearch]
        public String Description
        {
            get { return Fields.Description[this]; }
            set { Fields.Description[this] = value; }
        }

        [DisplayName("Storyline"), QuickSearch]
        public String Storyline
        {
            get { return Fields.Storyline[this]; }
            set { Fields.Storyline[this] = value; }
        }
        //...
    }
}

现在,假如我们搜索Gandalf,我将会得到The Lord of the Rings 条目:

movies_search_gandalf_10

快速搜索特性默认情况下是用contains  过滤的。它具有一些选项,使其与筛选器匹配的 starts with或只匹配精确值。 I如果我们想要只显示键入的文本的 starts with,行,我们可以改变特性到︰

[DisplayName("Title"), Size(200), NotNull, QuickSearch(SearchType.StartsWith)]
public String Title
{
    get { return Fields.Title[this]; }
    set { Fields.Title[this] = value; }
}

在这里这一快速搜索功能不是很有用,但对于像 SSN、 序列号、 身份证号码、 电话号码等的值,它可能是有用的。

如果我们也想要搜索year 列,但只精确的整数值 (1999匹配而不是 19) ︰

[DisplayName("Year"), QuickSearch(SearchType.Equals, numericOnly: 1)]
public Int32? Year
{
    get { return Fields.Year[this]; }
    set { Fields.Year[this] = value; }
}

你可能已经注意到,我们没有写出任何这些基本的功能的 C# 或 SQL 代码。我们只需指定我们想要什么,而不是如何去做。这就是声明性编程。

它也能够为用户提供以确定她想要搜索的字段的能力。

打开 MovieTutorial.Script/MovieDB/Movie/MovieGrid.cs 像下面这样修改:


namespace MovieTutorial.MovieDB
{
    //...
    public class MovieGrid : EntityGrid<MovieRow>
    {
        public MovieGrid(jQueryObject container)
            : base(container)
        {
        }

        protected override List<QuickSearchField> GetQuickSearchFields()
        {
            return new List<QuickSearchField>
            {
                new QuickSearchField { Name = "", Title = "all" },
                new QuickSearchField { Name = "Description", Title = "description" },
                new QuickSearchField { Name = "Storyline", Title = "storyline" },
                new QuickSearchField { Name = "Year", Title = "year" }
            };
        }
    }
    ///...
}

现在,我们得到了一个快速搜索输入下拉框。

movies_quick_fields_10

事先与示例不同,我们修改了服务器端代码,这一次我们做脚本方面的修改,实际上是对 javascript 代码进行修改。

运行 T4 模版(.tt 文件)

在前面的例子中,我们硬编码了像 Description, Storyline 等字段。假如我们忘记了实际的属性名称,可能导致输入错误。 This may lead to typing errors if we forgot actual property names at server side.

Serene contains some T4 (.tt) files to transfer such information from server side (rows etc) to client side for intellisense purposes.

Before running these templates, please make sure that your solution builds successfully as templates uses your output DLL files (MovieTutorial.Web.dll, MovieTutorial.Script.dll) to generate code.

After building your solution, click on Build menu, than Transform All Templates.

如果你在用Serene1.6.0以前的版本, 你可能会得到一个像下面这样的错误:

Error CS0579 Duplicate 'Imported' attribute ...

要解决这个错误,你仅仅需要移除MovieTutorial.Script/MovieDB/Movie路径下MovieGrid.cs文件的以下几行:

// Please remove this partial class or the first line below, // after you run ScriptContexts.tt
[Imported, Serializable, PreserveMemberCase] 
public partial class MovieRow
{
}

We can use intellisense to replace hardcoded field names with compile time checked versions:

我们可以使用智能感知来代替编译时检查版本替换硬编码字段名称 ︰

namespace MovieTutorial.MovieDB
{
    // ...
    public class MovieGrid : EntityGrid<MovieRow>
    {
        public MovieGrid(jQueryObject container)
            : base(container)
        {
        }

        protected override List<QuickSearchField> GetQuickSearchFields()
        {
            return new List<QuickSearchField>
            {
                new QuickSearchField { Name = "", Title = "all" },
                new QuickSearchField { Name = MovieRow.Fields.Description, 
                    Title = "description" },
                new QuickSearchField { Name = MovieRow.Fields.Storyline, 
                    Title = "storyline" },
                new QuickSearchField { Name = MovieRow.Fields.Year, 
                    Title = "year" }
            };
        }
    }
}

3.1.6添加一个 Movie Kind字段

假如我们想要在movie 表保存TV连续剧和mini剧信息, 我们也需要另外一个字段来存储它: MovieKind.

因为我们没有在创建表时添加这个字段,所以我们现在要写一个迁移来添加这个字段到我们的数据库。

MovieTutorial.Web/Modules/Common/Migrations/DefaultDB/DefaultDB_20150924_142200_MovieKind.cs文件创建另一个迁移:

using FluentMigrator;
using System;

namespace MovieTutorial.Migrations.DefaultDB
{
    [Migration(20150924142200)]
    public class DefaultDB_20150924_142200_MovieKind : Migration
    {
        public override void Up()
        {
            Alter.Table("Movie").InSchema("mov")
                .AddColumn("Kind").AsInt32().NotNullable()
                    .WithDefaultValue(1);
        }

        public override void Down()
        {
        }
    }
}
声明MovieKind  枚举

现在我们给Movie 表添加了一个Kind 列 , 我们需要一组电影类型的值。我们在MovieTutorial.Web/Modules/MovieDB/Movie/MovieKind.cs 里面把它定义为一个枚举:

using Serenity.ComponentModel;
using System.ComponentModel;

namespace MovieTutorial.MovieDB
{
    [EnumKey("MovieDB.MovieKind")]
    public enum MovieKind
    {
        [Description("Film")]
        Film = 1,
        [Description("TV Series")]
        TvSeries = 2,
        [Description("Mini Series")]
        MiniSeries = 3
    }
}
添加 Kind 字段 到 MovieRow 实体

因为我们不再用Sergen 了,我们需要在MovieRow.cs给Kind 列手动的添加一个映射,在 MovieRow.cs的 Runtime 属性后面声明下面这样的一个属性:

[DisplayName("Runtime (mins)")]
public Int32? Runtime
{
    get { return Fields.Runtime[this]; }
    set { Fields.Runtime[this] = value; }
}

[DisplayName("Kind"), NotNull]
public MovieKind? Kind
{
    get { return (MovieKind?)Fields.Kind[this]; }
    set { Fields.Kind[this] = (Int32?)value; }
} 

Serenity 的实体系统中我们也需要声明一个Int32 的类型对象。在 MovieRow.cs的底部,定位到RowFields  类在Runtime 字段后添加一个RowFields 。On the bottom of MovieRow.cs locate RowFields class and modify it to add Kind field after the Runtime field:

public class RowFields : RowFieldsBase
{
    // ...public readonly Int32Field Runtime;
    public readonly Int32Field Kind;

    public RowFields()
        : base("[mov].Movie")
    {
        LocalTextPrefix = "MovieDB.Movie";
    }
}
添加分类选择框到 Movie 表单

加入项目正在运行,我们能看到Movie 表单没有变化。即使我们添加Kind 字段映射到MovieRow。这是因为,字段的显示/编辑是在MovieForm.cs里面控制声明的。

像下面这样修改 MovieForm.cs :

namespace MovieTutorial.MovieDB.Forms
{
    // ...
    [FormScript("MovieDB.Movie")]
    [BasedOnRow(typeof(Entities.MovieRow))]
    public class MovieForm
    {
        // ...public Int32 Runtime { get; set; }
        public MovieKind Kind { get; set; }
    }
}

现在,构建项目并且运行,。在你试着编辑movie  或者新增一个,什么都不会发生。这是一个预期的情况,假如你检查浏览器的开发者工具的console ,你将会看到下面的错误:

Uncaught Can't find MovieTutorial.MovieDB.MovieKind enum type!

这是因为MoveKind 枚举在客户端是不可用的。我们应该在程序启动之前运行T4模板。

现在,在Visual Studio里面, 再次点击 Build -> Transform All Template.

重建我们的解决方案并且运行,现在我们的表单中有一个漂亮的下拉框来选择movie  分类。

movies_kind_selection_5

给Movie声明一个默认值

因为 Kind 是一个必须的字段, 我们必须在 Add Movie 弹出框中填充它, 否则我们会得到一个验证错误。

但是大多数电影我们都是存储为故事片,所以我们默认值就是它。

像下面这个添加一个DefaultValue特性来给Kind 属性加一个默认值。

[DisplayName("Kind"), NotNull, DefaultValue(1)]
public MovieKind? Kind
{
    get { return (MovieKind?)Fields.Kind[this]; }
    set { Fields.Kind[this] = (Int32?)value; }
}

现在,添加一个Movie 弹出框,Film将会作为电影类型的预加载。

3.1.7添加 Movie Genres

添加 Genre 字段

为了放Movie genres,我们需要一个查找表。但是字段不能用枚举,因为这次,Kind  字段genres 不能静态的声明为一个枚举类型。

通常,我们启动一个迁移。

MovieTutorial.Web/Modules/Common/Migrations/DefaultDB/DefaultDB_20150924_151600_GenreTable.cs:

using FluentMigrator;
using System;

namespace MovieTutorial.Migrations.DefaultDB
{
    [Migration(20150924151600)]
    public class DefaultDB_20150924_151600_GenreTable : Migration
    {
        public override void Up()
        {
            Create.Table("Genre").InSchema("mov")
                .WithColumn("GenreId").AsInt32().NotNullable()
                    .PrimaryKey().Identity()
                .WithColumn("Name").AsString(100).NotNullable();

            Alter.Table("Movie").InSchema("mov")
                .AddColumn("GenreId").AsInt32().Nullable();
        }

        public override void Down()
        {
        }
    }
}

我们也添加一个GenreId  字段到电影表

实际上,一个电影可能有多个流派,所以我们需要在一个单独的MovieGenres 表中保存他。但是现在我们仅仅把它看作是单一的,我们将在后面看到怎么把它改成多个。

为Genre  表生成代码

再次在Package Manager Console里面打开 sergen.exe  并且依照下面的参数生成代码

f105ef1c-6eda-4a89-8784-8cefb2351ce5

重新生成解决方案并且跑起来,我们将会得到一个像这样的新页面。

98b70764-98e0-48cb-b2ff-8ff89874019d

就像你在截图里面看到的那样,它在MovieDB 下面生成了一个节,而不是在我们最近命名的数据库Movie  。

这是因为Sergen 不知道我们在Movie页面的个性化喜好。

打开MovieTutorial.Web/Modules/Movie/GenrePage.cs, 剪切下面这个导航连接:

[assembly:Serenity.Navigation.NavigationLink(int.MaxValue, "MovieDB/Genre",
    typeof(MovieTutorial.MovieDB.Pages.GenreController))]

`

把它移动到 MovieTutorial.Web/Modules/Common/Navigation/NavigationItems.cs:

//...
[assembly: NavigationMenu(2000, "Movie Database", icon: "icon-film")]
[assembly: NavigationLink(2100, "Movie Database/Movies", 
    typeof(MovieDB.MovieController), icon: "icon-camcorder")]
[assembly: NavigationLink(2200, "Movie Database/Genres", 
    typeof(MovieDB.GenreController), icon: "icon-pin")]
//...
添加几个 Genre 定义

现在让我们添加一些genres样本,我将会不会通过迁移来添加他们,以便我们不会再另外一台PC上重复,但是你可能想在Genre 页面上手动的添加。

using FluentMigrator;
using System;

namespace MovieTutorial.Migrations.DefaultDB
{
    [Migration(20150924154100)]
    public class DefaultDB_20150924_154100_SampleGenres : Migration
    {
        public override void Up()
        {
            Insert.IntoTable("Genre").InSchema("mov")
                .Row(new
                {
                    Name = "Action"
                })
                .Row(new
                {
                    Name = "Drama"
                })
                .Row(new
                {
                    Name = "Comedy"
                })
                .Row(new
                {
                    Name = "Sci-fi"
                })
                .Row(new
                {
                    Name = "Fantasy"
                })
                .Row(new
                {
                    Name = "Documentary"
                });
        }

        public override void Down()
        {
        }
    }
} 
在 MovieRow映射 GenreId 字段

因为我们之前用 Kind 字段做过一次, GenreId 字段需要在MovieRow.cs里面映射。

namespace MovieTutorial.MovieDB.Entities
{
    // ...
    public sealed class MovieRow : Row, IIdRow, INameRow
    {
        [DisplayName("Kind"), NotNull, DefaultValue(1)]
        public MovieKind? Kind
        {
            get { return (MovieKind?)Fields.Kind[this]; }
            set { Fields.Kind[this] = (Int32?)value; }
        }

        [DisplayName("Genre"), ForeignKey("[mov].Genre", "GenreId"), LeftJoin("g")]
        public Int32? GenreId
        {
            get { return Fields.GenreId[this]; }
            set { Fields.GenreId[this] = value; }
        }

        [DisplayName("Genre"), Expression("g.Name")]
        public String GenreName
        {
            get { return Fields.GenreName[this]; }
            set { Fields.GenreName[this] = value; }
        }

        // ...

        public class RowFields : RowFieldsBase
        {
            // ...
            public readonly Int32Field Kind;
            public readonly Int32Field GenreId;
            public readonly StringField GenreName;

            public RowFields()
                : base("[mov].Movie")
            {
                LocalTextPrefix = "MovieDB.Movie";
            }
        }
    }
}

这里我们也映射了GenreId 并且用ForeignKey 特性声明了GenreId  作为外键关联 到[mov].Genre  表。

如果我们在添加了Genre 表之后生成Movie表代码,Sergen将会在数据库水平检查外键从而理解这些关系,并且给我们生成相似的代码。

我们也添加另外一个字段,GenreName实际上不是一个Movie 表中的字段,但是他在Genre 表中。

Serenity 实体更像SQL视图,你可以通过Join从别的表中带进来字段。

通过添加LeftJoin MovieId属性(“g”)特性,我们可以在任何需要的时候Join到Genre 表,其别名将会是g。

所以当Serenity 需要从表中查询的时候,它会生成像这样的sql语句:

SELECT t0.MovieId, t0.Kind, t0.GenreId, g.Name as GenreName 
FROM Movies t0
LEFT JOIN Genre g on t0.GenreId = g.GenreId

这个Join只会在如果从类型表字段要求选择的时候被执行,例如它的列在数据网格是可见的。

GenreName 属性上面通过添加 Expression("g.Name") , 我们指定这个字段有 g.Name 的SQL表达式,这是一个从我们的g join进来的字段。

添加Genre 选择项到Movie 表单

让我们添加 GenreId 字段到 MovieForm.cs表单:

namespace MovieTutorial.MovieDB.Forms
{
    //...
    [FormScript("MovieDB.Movie")]
    [BasedOnRow(typeof(Entities.MovieRow))]
    public class MovieForm
    {
        //...public Int32 GenreId { get; set; }
        public MovieKind Kind { get; set; }
    }
}

现在加入我们生成并且运行程序,我们将会看到一个Genre 字段已经添加到我们的表单中了。问题是,他接受的数据类型是int,我们想让他用下拉框。

很明显,我们需要为GenreId 字段改变编辑器类型。

声明一个Genres 查找脚本

为了给Genre 显示一个编辑器,genres 列表必须在客户端是可用的。

对枚举类型来说他是简单的,我们仅仅需要运行T4模板,他们复制枚举到脚本端。

我们不能照样做,因为Genre是一个动态的列

Serenity 提供动态数据在运行时生成静态脚本的概念。

动态脚本类似于web服务的,但它们的输出是动态javascript文件,可以在客户端缓存。

要给Genre 表生成一个动态查找脚本类型,打开 GenreRow.cs 并且像下面这样修改:

namespace MovieTutorial.MovieDB.Entities
{
    // ...

    [ConnectionKey("Default"), DisplayName("Genre"), InstanceName("Genre"), 
        TwoLevelCached]
    [ReadPermission("Administration")]
    [ModifyPermission("Administration")]
    [JsonConverter(typeof(JsonRowConverter))]
    [LookupScript("MovieDB.Genre")]
    public sealed class GenreRow : Row, IIdRow, INameRow
    {
        // ...
    }

仅仅需要添加一行 [LookupScript("MovieDB.Genre")].

重新编译启动运行登陆之后,按F12 打开开发者工具

输入Q.getLookup('MovieDB.Genre')

你会得到像下面这样的:

7b762d4d-b8e5-47e7-8ed9-dcdc8304aa98

这里MovieDB.Genre 是我们声明时分配给查找脚本的key

[LookupScript("MovieDB.Genre")]

这一步是为了展示如何检查是否一个查找的客户端脚本可用。

为Genre 字段用LookupEditor

有两个地方给GenreId 字段设置编辑器类型,一个是MovieForm.cs, 另一个是MovieRow.cs.

我通常选择后者,因为是中央位置,假如编辑器的类型仅仅特定于表单的,你可能选择在表单中设置

打开 MovieRow.cs 像下面这样添加LookupEditor 特性到 GenreId属性

    [DisplayName("Genre"), ForeignKey("[mov].Genre", "GenreId"), LeftJoin("g")]
    [LookupEditor("MovieDB.Genre")]
    public Int32? GenreId
    {
        get { return Fields.GenreId[this]; }
        set { Fields.GenreId[this] = value; }
    }

在编译运行项目之后,我们将会看到搜索类型的dropdown 在Genre字段。

222830b6-80fe-4872-9256-ef105a13d292

在Movie 网格显示Genre

当前, 电影genre can 能够在表单中被编辑但是不能显示在网格中. 编辑MovieColumns.cs 来显示 GenreName (不是GenreId).

namespace MovieTutorial.MovieDB.Forms
{
    // ...public class MovieColumns
    {
        //...
        [Width(100)]
        public String GenreName { get; set; }
        [DisplayName("Runtime in Minutes"), Width(150), AlignRight]
        public Int32 Runtime { get; set; }
    }
}

现在 GenreName 在网格中显示了

eac18fc8-aeca-4a70-874f-330abded49f2

标记它可以用来定义一个新的 Genre Inplace

当我们为movies示例设置 genre , 我们注意到 The Good, the Bad and the UglyWestern但是在 Genre dropdown 还没有这样的类型

一个方法是打开 Genres 页, 添加他, 在返回movie 表单. 不是那么完美...

幸运的是Serenity 集成了原地定义 查找编辑器的能力:

打开MovieRow.cs 修改 LookupEditor 特性,像这样:

[DisplayName("Genre"), ForeignKey("[mov].Genre", "GenreId"), LeftJoin("g")]
[LookupEditor("MovieDB.Genre", InplaceAdd = true)]
public Int32? GenreId
{
    get { return Fields.GenreId[this]; }
    set { Fields.GenreId[this] = value; }
}

现在我们可以通过点击genre  字段旁边的star/pen 图标来定义一个新的 Genre  :

5270ecfd-0078-4838-8d20-f2ad898465bf

这里我们也看到我们可以从另一个页面使用一个对话框(GenreDialog)电影页面。在Serenity的应用程序中,所有客户端对象(对话框、网格、编辑、格式器等)是独立的可重用的组件(部件)不绑定到任何页面。

也可以开始键入类型编辑器,它将为您提供一个选项来添加一个新类型。

49f81f57-c91e-4f16-beaa-fa1c8c47247d

重新运行 T4 模板

因为我们天下将额一个新的实体到程序中,我们应该运行 T4 模板

假如你在用的是Serene1.6.0之前的版本,你可能抱怨重复的特性,你仅仅需要删除 GenreGrid.cs里面的GenreRow分部类。

3.1.8更新Serenity 包

当我开始写这个教程的时候,Serenity (NuGet 包包括Serenity 程序集和标准  scripts 库) 和Serene (the application template) 是 1.5.4版.

当你读这些的时候可能有更新的版本了, 所以你可能还需要更新serenity。

但是我想展示一下你能怎么样更新Serenity NuGet 包,以防将来出现另外的新版本。

我倾向于在NuGet包管理工具工具行而不是图形界面下用,因为它快得多。

所以,点击 View -> Other Windows -> Package Manager Console.

输入:

Update-Package Serenity.Web

由于依赖关系,它将会更新在MovieTutorial.Web 里面的NuGet 包:

Serenity.Core
Serenity.Data
Serenity.Data.Entity
Serenity.Services

要更新Serenity.CodeGenerator (containg sergen.exe), 输入:

Update-Package Serenity.CodeGenerator

Serenity.CodeGenerator 也在MovieTutorial.Web 里面安装了。

现在,让我们更新脚本包:

Update-Package Serenity.Script

Serenity.Script 包包含三个文件集: Serenity.Script.Imports, Serenity.Script.CoreSerenity.Script.UI, 所以都更新他们:

更新期间,如果NuGet询问是否覆盖变化在某些脚本文件,您可以安全地说,是的,除非你手动修改宁静脚本文件(我建议你避免)。

现在重新构建解决方案,它会提示构建成功。

时不时,Serenity可能会发生破坏性的变化,但他们保持最低限度,你可能需要做一些手动更改应用到程序代码。

这种变化是会在日志记录上打一个[BREAKING CHANGE] 标签: https://github.com/volkanceylan/Serenity/blob/master/CHANGELOG.md

假如你在升级之后还有问题,在下面随意开个问题https://github.com/volkanceylan/Serenity/issues

升级了什么

升级 Serenity NuGet 包,, 保持 Serenity 程序集到最新的版本。

他可能也会升级一些其他的第三方包,像ASP.NET MVC, FluentMigrator, Select2.js, SlickGrid等等。

暂时请不要升级Select2.js到3.5.1以后的版本,因为它配合Query validation时还有一些兼容性问题

Serenity.Web包还带有一些静态的脚本和CSS资源如下:

Content/serenity/serenity.css
Scripts/saltarelle/mscorlib.js
Scripts/saltarelle/linq.js
Scripts/serenity/Serenity.Core.js
Scripts/serenity/Serenity.Script.UI
Scripts/serenity/Serenity.Externals.js
Scripts/serenity/Serenity.Externals.Slick.js

所以,在MovieApplication.Web里面,这些还有其他的也升级了

没有升级什么(或者是不能自动升级什么)

更新Serenity 包,, 更新Serenity 程序集 和大多数静态scripts, 但是不是所有的Serene 模板 内容升级了。

我们正在尽可能简单地更新您的应用程序,但是但Serene 只是一个项目模板,而不是静态包。您的应用程序是一个可定制的Serene副本。

您可能已经做了修改应用程序源代码,所以更新一个Serene 的应用程序创建的一个旧版本的Serene 的模板,可能不会像听起来那么容易。

因此,有时你可能需要创建一个新的Serene 的应用程序与最新的Serene 的模板版本,并与你的程序比较,并合并你需要的功能。这是一个手工的过程。

我们有一些计划把Serene 一部分模板做成Nuget包,但是它仍然不容易的更新你的程序而不重写你的变化,比如共享的代码比如导航条目。假如你移除了Northwind 代码,但是我们的更新重新安装他?我开放这个讨论...

在下面的主题中我将需要一些Serene1.5.9的代码,并且我们将会看到怎么从我们的MovieTutorial 得到他们。

他们的角色和演员。

加入我们想保存演员和他们扮演的角色记录:

Actor/Actress
Character

Keanu Reeves
Neo

Laurence Fishburne
Morpheus

Carrie-Anne Moss
Trinity

我们需要一个MovieCast  表和列像这样:

MovieCastId
MovieId
PersonId
Character

...
...
...
...

11
2 (Matrix)
77 (Keanu Reeves)
Neo

12
2 (Matrix)
99 (Laurence Fisburne)
Morpheus

13
2 (Matrix)
30 (Carrie-Anne Moss)
Trinitity

...
...
...
...

很明显我们需要一个人员表因为我们要保存演员/女演员的 ID

最好叫它Person ,因为演员/女演员可能变为导演、编剧和其他的。

创建Person 和 MovieCast 表

是时候创建这两个表的迁移了:

MovieTutorial.Web/Modules/Common/Migrations/DefaultDB/DefaultDB_20151025_170200_PersonAndMovieCast.cs:

using FluentMigrator;
using System;

namespace MovieTutorial.Migrations.DefaultDB
{
    [Migration(20151025170200)]
    public class DefaultDB_20151025_170200_PersonAndMovieCast : Migration
    {
        public override void Up()
        {
            Create.Table("Person").InSchema("mov")
                .WithColumn("PersonId").AsInt32().Identity()
                    .PrimaryKey().NotNullable()
                .WithColumn("Firstname").AsString(50).NotNullable()
                .WithColumn("Lastname").AsString(50).NotNullable()
                .WithColumn("BirthDate").AsDateTime().Nullable()
                .WithColumn("BirthPlace").AsString(100).Nullable()
                .WithColumn("Gender").AsInt32().Nullable()
                .WithColumn("Height").AsInt32().Nullable();

            Create.Table("MovieCast").InSchema("mov")
                .WithColumn("MovieCastId").AsInt32().Identity()
                    .PrimaryKey().NotNullable()
                .WithColumn("MovieId").AsInt32().NotNullable()
                    .ForeignKey("FK_MovieCast_MovieId", "mov", "Movie", "MovieId")
                .WithColumn("PersonId").AsInt32().NotNullable()
                    .ForeignKey("FK_MovieCast_PersonId", "mov", "Person", "PersonId")
                .WithColumn("Character").AsString(50).Nullable();
        }

        public override void Down()
        {
        }
    }
}
为表Person生成代码

首先为Person 表生成代码:

104c7d12-b5d5-4109-b2b3-69c6c44144a2

改变性别为枚举

性别列在Person 表中应该是一个枚举,在PersonRow.cs后面声明一个Gender.cs 枚举

using Serenity.ComponentModel;
using System.ComponentModel;

namespace MovieTutorial.MovieDB
{
    [EnumKey("MovieDB.Gender")]
    public enum Gender
    {
        [Description("Male")]
        Male = 1,
        [Description("Female")]
        Female = 2
    }
}

像下面这样在 PersonRow.cs 改变性别属性声明:

//...
        [DisplayName("Gender")]
        public Gender? Gender
        {
            get { return (Gender?)Fields.Gender[this]; }
            set { Fields.Gender[this] = (Int32?)value; }
        }
//...

为了一致性,在PersonForm.cs and PersonColumns.cs  改变Gender  属性int32 为Gender 。

重新编译T4模板

因为我们声明了一个枚举并且应用他,我们因为重新生成解决方案,转换T4模板。

If you are using a Serene version before 1.6.0, delete partial MovieRow declaration from MovieGrid.cs.

如果您使用的是Serene 1.6.0之前版本,从MovieGrid.cs删除部分MovieRow声明。

在我们启动项目之后,我们能进入角色。

79d972e5-92c0-4c2d-b29a-0e20eea4ab91

声明FullName 字段

在编辑对话框的标题,显示人的名字(Carrie-Anne)。最好是全名。同时在网格搜索全名。

我们来编辑PersonRow.cs:

namespace MovieTutorial.MovieDB.Entities
{
    //...
    public sealed class PersonRow : Row, IIdRow, INameRow
    {
        //... remove QuickSearch from FirstName
        [DisplayName("First Name"), Size(50), NotNull]
        public String Firstname
        {
            get { return Fields.Firstname[this]; }
            set { Fields.Firstname[this] = value; }
        }

        [DisplayName("Last Name"), Size(50), NotNull]
        public String Lastname
        {
            get { return Fields.Lastname[this]; }
            set { Fields.Lastname[this] = value; }
        }

        [DisplayName("Full Name"), 
         Expression("(t0.Firstname + ' ' + t0.Lastname)"), QuickSearch]
        public String Fullname
        {
            get { return Fields.Fullname[this]; }
            set { Fields.Fullname[this] = value; }
        }

        //... change NameField to Fullname
        StringField INameRow.NameField
        {
            get { return Fields.Fullname; }
        }

        //...

        public class RowFields : RowFieldsBase
        {
            public readonly Int32Field PersonId;
            public readonly StringField Firstname;
            public readonly StringField Lastname;
            public readonly StringField Fullname;
            //...
        }
    }
}

我们在Fullname 属性上面指定 SQL expression 表达式("(t0.Firstname + ' ' + t0.Lastname)")特性 。 因此,它是一个服务器端计算字段。

通过在FullName 上面添加QuickSearch 特性而不是在Firstname, 表格将会默认搜索 Fullname 字段.

但是弹出框仍然会显示 Firstname.为此,我们需要做出改变首先改变模板,然后在PersonDialog.cs做以下更改:

namespace MovieTutorial.MovieDB
{
    using jQueryApi;
    using Serenity;
    using System.Collections.Generic;

    [IdProperty("PersonId"), NameProperty(PersonRow.Fields.Fullname)]
    [FormKey("MovieDB.Person"), LocalTextPrefix("MovieDB.Person"), 
        Service("MovieDB/Person")]
    public class PersonDialog : EntityDialog<PersonRow>
    {
    }
}

为了在编译时检查,而不是写* NameProperty(“Fullname”),我使用T4模板生成的字段名。

我们还可以使用类似的其他特性的信息:

namespace MovieTutorial.MovieDB
{
    using jQueryApi;
    using Serenity;
    using System.Collections.Generic;

    [IdProperty(PersonRow.IdProperty), NameProperty(PersonRow.NameProperty)]
    [FormKey(PersonForm.FormKey), LocalTextPrefix(PersonRow.LocalTextPrefix),         
        Service(PersonService.BaseUrl)]
    public class PersonDialog : EntityDialog<PersonRow>
    {
    }
}

PersonRow.NameProperty对应NameField设置在服务器端。

现在PersonDialog有全名的标题。

这里,我们给Person表声明一个LookupScript :

namespace MovieTutorial.MovieDB.Entities
{
    //...
    [LookupScript("MovieDB.Person")]
    public sealed class PersonRow : Row, IIdRow, INameRow
    //...

以后我们将用它来编辑电影演员。

为 MovieCast 表生成代码

sergen给MovieCast表生成代码:

59ac2c0e-c19b-4234-a921-8bdccc3fa5a5

主/详细编辑逻辑MovieCast表

到目前为止,我们为每个表创建了一个页面,这个页面和编辑的记录列表。这一次我们要使用不同的策略。

我们会为电影演员列表在电影编辑对话框,允许他们的电影。同时,演员与电影实体在一个事务中一起将被保存。

因此,编辑会在内存中,当用户按下保存按钮在电影的对话框中,电影和演员一箭将被保存到数据库(一个事务)。

有可能独立编辑演员,我们只是想表明这是可以做到的。

对于某些类型的主/详细记录订单/细节,细节不应该允许编辑独立原因一致性。Serene 已经有一个样本对这种Northwind/Order编辑对话框。

从 Serene 模板 1.5.9+获得必须的基类

你可能不需要这一步,但是当我开始本教程在平静的订单/细节编辑示例之前,我必须从最近的三个类模板。

这只是一个从最近的一个Serene模板如何获得新功能的例子。

So i will create a new Serene application (NewApp), take these three files below from it:

所以我将创建一个新的Serene的应用程序(NewApp),从它下面的三个文件:

NewApp.Script/Common/Helper/GridEditorBase.cs
NewApp.Script/Common/Helper/GridEditorDialog.cs
NewApp.Web/Modules/Common/Helpers/DetailListSaveHandler.cs

复制他们到

MovieTutorial.Script/Common/Helper/GridEditorBase.cs
MovieTutorial.Script/Common/Helper/GridEditorDialog.cs
MovieTutorial.Web/Modules/Common/Helpers/DetailListSaveHandler.cs

将他们包括在项目中,用MovieTutorial替换NewApp文本。

一旦这些基类稳定和足够灵活,他们将被集成到Serenity 。

创建一个电影演员表编辑器

MovieCastGrid.cs旁边 (在 MovieTutorial.Script/MovieDB/MovieCast/), 用下面的内容创建一个 MovieCastEditor.cs 文件:

namespace MovieTutorial.MovieDB
{
    using Common;
    using jQueryApi;
    using Serenity;
    using System.Linq;

    [ColumnsKey("MovieDB.MovieCast"), LocalTextPrefix("MovieDB.MovieCast")]
    public class MovieCastEditor : GridEditorBase<MovieCastRow>
    {
        public MovieCastEditor(jQueryObject container)
            : base(container)
        {
        }
    }
}

请不要使用Visual Studio添加菜单项在项目脚本文件创建一个.cs 。使用复制粘贴来创建一个新文件,并修改它。否则,Visual Studio项目添加一个系统参考脚本,这不是与Saltarelle兼容。 如果你做了这个错误的动作,你需要删除系统引用。

从服务器端引用这个新的编辑器类型,重建方案,将所有模板(如果使用的是旧版本,删除无用的从MovieGrid MovieCastRow部分。cs,并再次构建(我不得不重新运行模板)

在电影中使用MovieCastEditor形式

打开MovieForm.cs,DescriptionStoryline 之间的字段,添加一个CastList属性:

namespace MovieTutorial.MovieDB.Forms
{
    //...public class MovieForm
    {
        public String Title { get; set; }
        [TextAreaEditor(Rows = 3)]
        public String Description { get; set; }
        [MovieCastEditor]
        public List<Entities.MovieCastRow> CastList { get; set; }
        [TextAreaEditor(Rows = 8)]
        public String Storyline { get; set; }
        //...
    }
}

通过将 [MovieCastEditor] 特性放到 CastList属性之上,我们指定这个属性将由我们的新编辑MovieCastEditor类型中定义的脚本代码。

我们也可以写EditorType("MovieDB.MovieCast")] 但是谁喜欢硬编码字符串呢?反正不是我...

现在构建和启动应用程序。电影打开一个对话框,你就会得到我们的新编辑器:

1b12103d-b9bc-45e8-8148-a9c88bb54247

好吧,看起来容易,但是老实说,我们甚至没有一半的方法。

新MovieCast按钮不起作用,需要定义一个对话框,网格列不是我想他们和字段和按钮标题并不是非常用户友好……

也因为这不是一个综合功能(还没有),我必须处理更多的管道如加载和保存在服务器端。

配置MovieCastEditor 来用MovieCastEditDialog

得到MovieCastDialog.cs的副本作为MovieCastEditDialog cs像下图修改它:

namespace MovieTutorial.MovieDB
{
    using jQueryApi;
    using Common;
    using Serenity;
    using System.Collections.Generic;

    [NameProperty("Character"), FormKey("MovieDB.MovieCast"),
        LocalTextPrefix("MovieDB.MovieCast")]
    public class MovieCastEditDialog : GridEditorDialog<MovieCastRow>
    {
    }
}

打开MovieCastEditor.cs又添加一个DialogType属性并且覆盖GetAddButtonCaption:

namespace MovieTutorial.MovieDB
{
    //..
    [DialogType(typeof(MovieCastEditDialog))]
    public class MovieCastEditor : GridEditorBase<MovieCastRow>
    {
        public MovieCastEditor(jQueryObject container)
            : base(container)
        {
        }

        protected override string GetAddButtonCaption()
        {
            return "Add";
        }
    }
}

我们指定MovieCastEditor默认使用MovieCastEditDialog也使用Add按钮。

现在,什么都不做,而是添加按钮显示一个对话框。

84fd0d2b-acd9-4326-9597-6821de7fff3c

这个对话框需要一些CSS格式化。电影标题和人的名字字段接受整数输入(因为它们实际上是MovieId PersonId字段)。

编辑 MovieCastForm.cs

我们有 FormKey("MovieDB.MovieCast") 在MovieCastEditDialog顶部, 所以用 MovieCastForm, 这也是由MovieCastDialog共享。

在Serenity中一个实体可以有多种形式表单。我会像MovieCastEditForm那样定义一个新的形式,但最终我将删除MovieCastDialog和MovieCastGrid类,我不介意。

Open MovieCastForm.cs and modify it:

namespace MovieTutorial.MovieDB.Forms
{
    using Serenity.ComponentModel;
    using System;

    [FormScript("MovieDB.MovieCast")]
    [BasedOnRow(typeof(Entities.MovieCastRow))]
    public class MovieCastForm
    {
        [LookupEditor(typeof(Entities.PersonRow))]
        public Int32 PersonId { get; set; }
        public String Character { get; set; }
    }
}

我已经删除MovieId因为这个表单将会用在 MovieDialog,所以MovieCast实体将会自动有MovieDialog正在编辑的当前电影MovieId 。打开《指环王》电影和添加一个Matrix条目的想法看起来没有道理。

我已经设置了PersonId字段查询编辑器编辑器类型并且因为我已经添加了一个LookupScript MovieCastRow特性,我可以重用该设置查找关键信息。

我们也能够写 [LookupEditor("MovieDB.Person")]

构建解决方案,启动,现在MovieCastEditDialog有更好的编辑体验。但仍有一个坏的外观并且PersonId字段有一个标题(或人与< Firstname 1.6.1),为什么?

写这篇文章时,有一个新的Serene版本(1.6.0)。我现在在更新Serenity包来保持教程是最新的。

修复MovieCastEditDialog的外观

Let's check site.less to understand why our MovieCastDialog is not styled.

让我们检查site.less 来理解为什么我们MovieCastDialog不是时尚的。

.s-MovieCastDialog {
    > .size { .widthAndMin(650px); }
    .dialog-styles(@h: auto, @l: 150px, @e: 400px);
    .s-PropertyGrid .categories { height: 260px; }
}

site.less 的底部是MovieCastDialog,不是MovieCastEditDialog,因为我们这个类定义自己,而不是代码生成的。

我们创建了一个新的对话框类型,通过复制MovieCastDialog略有和修改它,所以现在我们的新对话框的CSS类s-MovieCastEditDialog,但代码生成器生成s-MovieCastDialog CSS规则。

Serenity 对话框自动分配CSS类对话框元素,在类型名称前面加上“s -”。你可以看到通过检查开发工具中的对话框MovieCastEditDialog s-MovieCastEditDialog和s-MovieDB-MovieCastEditDialog CSS类,还有一些像ui-dialog。

当我们两个模块有一个类型名称相同的时候,s-ModuleName-TypeName CSS类帮助我们区分样式。

我们不会真正使用MovieCastDialog(我们会删除它),让我们在site.less重命名一个:

.s-MovieCastEditDialog {
    > .size { .widthAndMin(550px); }
    .dialog-styles(@h: auto, @l: 150px, @e: 300px);
    .s-PropertyGrid .categories { height: 160px; }
}
修复对话框Dialog 和 PersonId 字段标题

对话框还有标题MovieCast,我们记得怎么改正它吗?

打开MovieCastRow.cs和执行这些修改:

namespace MovieTutorial.MovieDB.Entities
{

    //..
    [ConnectionKey("Default"), DisplayName("Movie Casts"), InstanceName("Cast"), 
        TwoLevelCached]
    //..
    public sealed class MovieCastRow : Row, IIdRow, INameRow
    {
        /...
        [DisplayName("Actor/Actress"), NotNull, 
            ForeignKey("[mov].Person", "PersonId"), LeftJoin("jPerson")]
        public Int32? PersonId
        {
            get { return Fields.PersonId[this]; }
            set { Fields.PersonId[this] = value; }
        }
    }
}

首先,我们都改变DisplayName并且将他的特性设置为对话框的标题。也将PersonId字段标题更改为演员。现在MovieCastEditDialog看起来好一点:

b3a50180-3543-4ca2-a771-4b67a00fb0cd

修复MovieCastEditor列

MovieCastEditor目前使用MovieCastColumns.cs中定义的列(因为它在类声明有[ColumnsKey(“MovieDB.MovieCast”)])。

我们有MovieCastId、MovieId PersonId(显示为演员)和字符列。最好是只显示演员和角色列。

但是我们不想显示PersonId(整数值),而是他们的全名,所以我们将在MovieCastRow.cs定义这个字段

namespace MovieTutorial.MovieDB.Entities
{
    //...
    public sealed class MovieCastRow : Row, IIdRow, INameRow
    {
        // ...

        [DisplayName("Person Firstname"), Expression("jPerson.Firstname")]
        public String PersonFirstname
        {
            get { return Fields.PersonFirstname[this]; }
            set { Fields.PersonFirstname[this] = value; }
        }

        [DisplayName("Person Lastname"), Expression("jPerson.Lastname")]
        public String PersonLastname
        {
            get { return Fields.PersonLastname[this]; }
            set { Fields.PersonLastname[this] = value; }
        }

        [DisplayName("Actor/Actress"), 
         Expression("(jPerson.Firstname + ' ' + jPerson.Lastname)")]
        public String PersonFullname
        {
            get { return Fields.PersonFullname[this]; }
            set { Fields.PersonFullname[this] = value; }
        }

        // ...

        public class RowFields : RowFieldsBase
        {
            // ...
            public readonly StringField PersonFirstname;
            public readonly StringField PersonLastname;
            public readonly StringField PersonFullname;
            // ...
        }
    }
}

修改MovieCastColumns.cs:

namespace MovieTutorial.MovieDB.Columns
{
    using Serenity.ComponentModel;
    using System;

    [ColumnsScript("MovieDB.MovieCast")]
    [BasedOnRow(typeof(Entities.MovieCastRow))]
    public class MovieCastColumns
    {
        [EditLink, Width(220)]
        public String PersonFullname { get; set; }
        [EditLink, Width(150)]
        public String Character { get; set; }
    }
}

重建项目,演员网格具有更好的列:

8b587a89-7e04-44c2-acf4-0e4e9ddc9292

现在尝试添加一个演员,例如,基努·里维斯/ Neo:

9325965e-ea58-4bf5-997a-80e9108c7e2e

为什么演员列是空的? ?

解决空演员列问题

记住,我们正在编辑内存。这里不涉及服务调用。因此,网格显示任何对话框发送回它的实体。

当您单击save按钮时,对话框构建一个这样的实体保存:

{
    PersonId: 7,
    Character: 'Neo'
}

这些字段对应于这样的在MovieCastForm.cs您以前设置的表单字段:

public class MovieCastForm
{
    [LookupEditor(typeof(Entities.PersonRow))]
    public Int32 PersonId { get; set; }
    public String Character { get; set; }
}

在这个实体里面没有PersonFullname字段,所以网格不能显示它的值。

我们需要设置PersonFullname自己。让我们首先变换T4模板接收我们最近添加字段PersonFullname,然后编辑MovieCastEditor.cs:

namespace MovieTutorial.MovieDB
{
    // ...
    public class MovieCastEditor : GridEditorBase<MovieCastRow>
    {
        // ...
        protected override bool ValidateEntity(MovieCastRow row, int? id)
        {
            if (!base.ValidateEntity(row, id))
                return false;

            row.PersonFullname = PersonRow.Lookup
                .ItemById[row.PersonId.Value].Fullname;

            return true;
        }
    }
}

ValidateEntity是在GridEditorBase类里面的方法。点击保存按钮时调用此方法来验证实体,之前它将被添加到网格。但是我们在这里覆盖它是另一个目的(设置PersonFullname字段值)而不是验证。

正如我们之前看到的,我们的实体PersonId和字符字段填充。我们可以使用PersonId字段的值来确定人的全名。

我们需要一个字典映射PersonId Fullname值。幸运的是person 查找有那个字典。我们可以通过查找属性查找PersonRow。

另一种方法来访问person 查找是通过Q.GetLookup(“MovieDB.Person”)。在PersonRow只是一个T4模板定义的快捷键。

所有查找ItemById字典,允许您访问该类型的一个实体的ID。

Lookups是一种简单的方式来分享与客户端服务器端数据。但是他们只适合小的数据集。

如果一个表有成千上万的记录,它不会是合理定义一个查询。在这种情况下,我们将使用一个服务请求查询记录的ID。

Defining CastList in MovieRow在MovieRow定义CastList

当Movie 对话框打开,至少一个演员在CastList,单击save按钮,你会得到这样一个错误:

Could not find field 'CastList' on row of type 'MovieRow'.

这个错误是由- > Row deserializer (JsonRowConverter for JSON.NET) 在服务器端。

我们在MovieForm 定义CastList属性,但没有在MovieRow对应字段声明。所以反序列化器找不到在哪里写CastList从客户端收到的值。

如果你打开与F12开发工具,点击网络选项卡,并观察AJAX请求单击Save按钮后,你会发现它有这样的请求负载:

{
    "Entity": {
        "Title": "The Matrix",
        "Description": "A computer hacker...",
        "CastList": [
            {
                "PersonId":"1",
                "Character":"Neo",
                "PersonFullname":"Keanu Reeves"
            }
        ],
        "Storyline":"Thomas A. Anderson is a man living two lives...",
        "Year":1999,
        "ReleaseDate":"1999-03-31",
        "Runtime":136,
        "GenreId":"",
        "Kind":"1",
        "MovieId":1
    }
}

这里,CastList属性不能在服务器端反序列化。所以我们需要在MovieRow.cs声明它:

namespace MovieTutorial.MovieDB.Entities
{
    // ...
    public sealed class MovieRow : Row, IIdRow, INameRow
    {
        [DisplayName("Cast List"), SetFieldFlags(FieldFlags.ClientSide)]
        public List<MovieCastRow> CastList
        {
            get { return Fields.CastList[this]; }
            set { Fields.CastList[this] = value; }
        }

        public class RowFields : RowFieldsBase
        {
            // ...
            public readonly RowListField<MovieCastRow> CastList;
            // ...
        }
    }
}

我们定义一个CastList属性,将接受MovieCastRow对象的列表。Field字段的类型类,用于列表属性是RowListField这样行

通过添加[SetFieldFlags(FieldFlags.ClientSide)特性,我们指定,这个字段是不可以直接在数据库表中,因此不能通过简单的SQL查询。它类似于一个在其他ORM系统未映射字段。

现在,当你单击Save按钮时,你不会得到一个错误。

但是刚才保存的Matrix的实体重新打开。没有演员条目。Neo怎么了?

因为这是一个未映射的字段,所以movie 保存服务只是忽略了CastList属性。

处理保存CastList

打开MovieRepository.cs,找到空MySaveHandler类,并像下图修改它:

private class MySaveHandler : SaveRequestHandler<MyRow>
{
    protected override void AfterSave()
    {
        base.AfterSave();

        if (Row.CastList != null)
        {
            var mc = Entities.MovieCastRow.Fields;
            var oldList = IsCreate ? null :
                Connection.List<Entities.MovieCastRow>(
                    mc.MovieId == this.Row.MovieId.Value);

            new Common.DetailListSaveHandler<Entities.MovieCastRow>(
                oldList, Row.CastList,
                x => x.MovieId = Row.MovieId.Value).Process(this.UnitOfWork);
        }
    }
}

MySaveHandler、流程创建(插入),更新为电影服务请求的行。大部分的逻辑是由SaveRequestHandler基类,其类定义之前是空的。

插入/更新演员表之前,我们应该首先等待电影实体插入/更新成功。因此,我们通过重写基AfterSave方法包括定制的代码。

如果这是创建操作(插入),我们需要重用在MovieCast记录重用MovieId字段的值。因为MovieId是IDENTITY字段,它可以用来插入movie 记录。

当我们正在编辑演员表在内存中(客户端),这将是一个批量更新。

我们需要比较这部电影的就的演员记录列表和新列表记录,并插入/更新/删除它们。

假设我们有记录,在电影数据库B,C,D X。

用户在编辑对话框,列表做了一些修改,现在我们有A,B,D,E,F。

所以我们需要更新A,B,D(以防字符/演员改变),删除C,E和F和插入新记录。

DetailListSaveHandler处理这些比较和自动插入/更新/删除操作(通过ID值)。

为得到旧的列表记录,我们需要查询数据库如果这是一个电影更新操作。如果这是一个创建操作电影不应该有任何旧记录。

我们使用的是Connection.List< Entities.MovieCastRow >扩展方法。连接是SaveRequestHandler返回当前连接的属性。列表中选择指定的条件相匹配的记录(mc.MovieId = = this.Row.MovieId.Value)。

this.Row refers是指目前插入/更新记录(电影)的新字段值,所以它包含MovieId值(新的或已经存在的)。

要更新cast 记录,我们创建一个DetailListHandler对象,与老演员表,新演员表,委托设置MovieId字段值的记录。这是与当前的电影链接新记录。

然后我们DetailListHandler打电话。过程与当前工作单元的进程。UnitOfWork包装是一个特殊的对象当前的连接/事务。

所有Serenity创建/更新/删除处理工作使用隐式事务(IUnitOfWork)。

处理检索CastList

我们还没有完成。当在Movie 表格中点击一条Movie实体,电影对话框加载电影记录通过调用电影检索服务。CastList是一个未映射的字段,即使我们保存了他们,他们不会加载到对话框。

我们也需要编辑MyRetrieveHandler MovieRepository.cs类:

private class MyRetrieveHandler : RetrieveRequestHandler<MyRow>
{
    protected override void OnReturn()
    {
        base.OnReturn();

        var mc = Entities.MovieCastRow.Fields;
        Row.CastList = Connection.List<Entities.MovieCastRow>(q => q
            .SelectTableFields()
            .Select(mc.PersonFullname));
    }
}

在这里,我们重写了OnReturn方法,在返回检索服务之前,给电影注入CastList行。

我使用一个不同的 Connection.List扩展重载,它允许我修改select查询。

默认情况下,列表中选择表所有字段(不是外国字段来自其他表),但为了显示演员的名字,我也需要选择PersonFullName字段。

Now build the solution, and we can finally list / edit the cast.现在构建解决方案,最后我们列出/编辑演员了。

处理CastList删除

当你试图删除电影实体,你会得到外键错误。在创建MovieCast表的时候,您可以使用 "CASCADE DELETE"外键。但我们在仓储级别会再处理这个问题:

private class MyDeleteHandler : DeleteRequestHandler<MyRow>
{
    protected override void OnBeforeDelete()
    {
        base.OnBeforeDelete();

        var mc = Entities.MovieCastRow.Fields;
        foreach (var detailID in Connection.Query<Int32>(
            new SqlQuery()
                .From(mc)
                .Select(mc.MovieCastId)
                .Where(mc.MovieId == Row.MovieId.Value)))
        {
            new DeleteRequestHandler<Entities.MovieCastRow>().Process(this.UnitOfWork,
                new DeleteRequest
                {
                    EntityId = detailID
                });
        }
    }
}

我们实现这个主/细节处理不是很直观,它包括了几个手动步骤库层。继续阅读,看看可以通过使用一个集成的特性(MasterDetailRelationAttribute)轻松做出来。

处理保存/检索/删除(Serenity 1.6.3 +)

主/明细关系是一个Serenity 1.6.3 +集成的功能(至少在服务器端),而不是手动覆盖保存/检索和删除处理程序,我将使用一个新的特性MasterDetailRelation(当然我必须升级到1.6.3)。

打开MovieRow.cs然后修改CastList属性:

[DisplayName("Cast List"), MasterDetailRelation(foreignKey: "MovieId"), ClientSide]
public List<MovieCastRow> CastList
{
    get { return Fields.CastList[this]; }
    set { Fields.CastList[this] = value; }
}

我们指定,这个字段是主/明细关系的详细列表和主ID字段(foreignKey)MovieId细节表。

现在我们撤销所有在MovieRepository.cs的更改:

private class MySaveHandler : SaveRequestHandler<MyRow> { }
private class MyDeleteHandler : DeleteRequestHandler<MyRow> { }
private class MyRetrieveHandler : RetrieveRequestHandler<MyRow> { }

我们只能在MovieCastRow.cs稍作改动。选择PersonFullname检索(就像我们在MyRetrieveHandler手动做的):

[DisplayName("Actor/Actress"), 
 Expression("(jPerson.Firstname + ' ' + jPerson.Lastname)")]
[MinSelectLevel(SelectLevel.List)]
public String PersonFullname
{
    get { return Fields.PersonFullname[this]; }
    set { Fields.PersonFullname[this] = value; }
}

这将确保PersonFullname选择字段检索。否则,它不会被默认加载为表的选择字段。

This ensures that PersonFullname field is selected on retrieve. Otherwise, it wouldn't be loaded as only table fields are selected by default.

现在构建您的项目,你就会看到相同的功能使用更少的代码。

MasterDetailRelationAttribute触发它们(自动)行为,MasterDetailRelationBehavior拦截检索/保存/删除处理程序和方法重载之前并且执行类似的操作。

我们做了同样的事情,但这次是声明,而不是命令式地(应该做什么,而不是如何去做)

https://en.wikipedia.org/wiki/Declarative_programming

以下章节我们将看到如何编写自己的请求处理程序的行为。

3.1.10在Person对话框列出Movies

要显示一个人再电影总扮演的角色,我们将添加一个选项卡PersonDialog

默认情况下所有的实体对话框(我们使用到目前为止,这源于EntityDialog)用EntityDialog 模板在MovieTutorial.Web/Views/Templates/EntityDialog.Template.html:

<div class="s-DialogContent"><div id="~_Toolbar" class="s-DialogToolbar"></div><div class="s-Form"><form id="~_Form" action=""><div class="fieldset ui-widget ui-widget-content ui-corner-all"><div id="~_PropertyGrid"></div><div class="clear"></div></div></form></div></div>

这个模板工具栏包含一个占位符(~ _Toolbar)形式(~ _Form)和PropertyGrid(* ~ _PropertyGrid)。

~ _是一个特殊的前缀,它在运行时被替换为一个惟一的对话ID。这确保了对象在一个对话框的两个实例不会有相同的ID值。

EntityDialog模板对话框都是共享的,所以我们不会修改PersonDialog添加一个选项卡。

定义一个标签PersonDialog的模板

用下面的内容创建一个新文件Modules/MovieDB/Person/PersonDialog.Template.html:

<div id="~_Tabs" class="s-DialogContent"><ul><li><a href="#~_TabInfo"><span>Person</span></a></li><li><a href="#~_TabMovies"><span>Movies</span></a></li></ul><div id="~_TabInfo" class="tab-pane s-TabInfo"><div id="~_Toolbar" class="s-DialogToolbar"></div><div class="s-Form"><form id="~_Form" action=""><div class="fieldset ui-widget ui-widget-content ui-corner-all"><div id="~_PropertyGrid"></div><div class="clear"></div></div></form></div></div><div id="~_TabMovies" class="tab-pane s-TabMovies"><div id="~_MoviesGrid"></div></div></div>

我们这里使用的语法是特定于jQuery UI tabs小部件。它需要一个UL元素的标签列表链接指向选项卡窗格div(.tab-pane)。

当EntityDialog发现一个div ID ~ _Tabs的模板,它会自动初始化一个标签窗口小部件。

模板文件的命名是很重要的。它必须以.Template. html结尾。这个扩展名文件在客户端通过一个动态脚本都可用。

模板文件的文件夹将被忽略,但模板必须在模块或视图/模板目录。

默认情况下,所有模板化部件(EntityDialog也来源于TemplatedWidget类),用他们的名称寻找模板。因此PersonDialog寻找PersonDialog.Template.html的名称的模板。

但是,因为之前不存在,继续搜索基类和后备模板EntityDialog.Template.html。

现在,我们有一个在PersonDialog的选项卡:

018c560e-f5ac-4c4c-a1ba-bea345f51569

同时,我注意到Person 的链接仍在MovieDB下面,我们忘了删除MovieCast链接。我现在修复…

创建PersonMovieGrid

但目前电影选项卡是空的。我们需要定义一个网格与合适的列,并将其放在该标签页。

首先,在文件PersonMovieColumns声明我们将使用的列的网格,。

namespace MovieTutorial.MovieDB.Columns
{
    using Serenity.ComponentModel;
    using System;

    [ColumnsScript("MovieDB.PersonMovie")]
    [BasedOnRow(typeof(Entities.MovieCastRow))]
    public class PersonMovieColumns
    {
        [Width(220)]
        public String MovieTitle { get; set; }
        [Width(100)]
        public Int32 MovieYear { get; set; }
        [Width(200)]
        public String Character { get; set; }
    }
}
接下来PersonGrid.cs旁边的文件里面定义一个PersonMovieGrid类:
namespace MovieTutorial.MovieDB
{
    using jQueryApi;
    using Serenity;

    [ColumnsKey("MovieDB.PersonMovie"), IdProperty(MovieCastRow.IdProperty)]
    [LocalTextPrefix(MovieCastRow.LocalTextPrefix), Service(MovieCastService.BaseUrl)]
    public class PersonMovieGrid : EntityGrid<MovieCastRow>
    {
        public PersonMovieGrid(jQueryObject container)
            : base(container)
        {
        }
    }
}

我们会使用MovieCast服务,来列出一个人扮演的电影。

最后一步是创建这个PersonDialog.cs网格:

namespace MovieTutorial.MovieDB
{
    using jQueryApi;
    using Serenity;
    using System.Collections.Generic;

    [IdProperty(PersonRow.IdProperty), NameProperty(PersonRow.Fields.Fullname)]
    [FormKey(PersonForm.FormKey), LocalTextPrefix(PersonRow.LocalTextPrefix),
     Service(PersonService.BaseUrl)]
    public class PersonDialog : EntityDialog<PersonRow>
    {
        private PersonMovieGrid moviesGrid;

        public PersonDialog()
        {
            moviesGrid = new PersonMovieGrid(this.ById("MoviesGrid"));

            tabs.OnActivate += (e, i) => this.Arrange();
        }
    }
}

记住,在我们的模板有一个div id ~ _MoviesGrid下电影选项卡窗格。我们在那个网格div创建了PersonMovie。

this.ById(“MoviesGrid”)是一种特殊的方法,模板化小部件。$(' # MoviesGrid ')不会在这里工作,作为div实际上有一些ID像PersonDialog17_MoviesGrid.~ _模板替换为一个独特的容器小部件ID。

我们还附加到jQuery UI tabs OnActivate事件,并要求安排的对话框的方法。这是与SlickGrid解决一个问题,当它最初创建在无形的选项卡中。安排触发relayout SlickGrid来解决这个问题。

好了,现在我们可以看到电影中的电影列表选项卡,但是很奇怪:

03b6d56d-af3c-40a0-9f56-445f2fdc2371

为Person过滤Movies

不,Carrie-Anne莫斯没有扮演三个角色。这个表格显示所有现在得电影演员记录,因为我们还没有告诉过滤器应该申请什么。

PersonMovieGrid应该知道它显示电影的人记录。所以,我们添加一个PersonID属性到网格。这个PersonID应该通过以某种方式为过滤列表服务。

namespace MovieTutorial.MovieDB
{
    using jQueryApi;
    using Serenity;
    using System.Collections.Generic;

    [ColumnsKey("MovieDB.PersonMovie"), IdProperty(MovieCastRow.IdProperty)]
    [LocalTextPrefix(MovieCastRow.LocalTextPrefix), Service(MovieCastService.BaseUrl)]
    public class PersonMovieGrid : EntityGrid<MovieCastRow>
    {
        public PersonMovieGrid(jQueryObject container)
            : base(container)
        {
        }

        protected override List<ToolButton> GetButtons()
        {
            return null;
        }

        protected override string GetInitialTitle()
        {
            return null;
        }

        protected override bool UsePager()
        {
            return false;
        }

        protected override bool GetGridCanLoad()
        {
            return personID != null;
        }

        private int? personID;

        public int? PersonID
        {
            get { return personID; }
            set
            {
                if (personID != value)
                {
                    personID = value;
                    SetEquality(MovieCastRow.Fields.PersonId, value);
                    Refresh();
                }
            }
        }
    }
}

We hold the person ID in a private variable. When it changes, we also set a equality filter for PersonId field using SetEquality method (which will be sent to list service), and refresh to see changes.

我们在私有变量有个 person ID。当它改变时,我们也为PersonId字段设置一个对等的过滤器使用SetEquality方法(将被发送到列表服务),并且刷新看到变化。

重写GetGridCanLoad方法允许我们控制网格可以调用列表服务。

如果我们不重写它,当我们创建一个新的Person,网格将加载所有电影演员记录,因为没有一个PersonID(它是null)。

我们通过重写三种方法也做了三个表面的改变,首先移除工具栏按钮,第二,从网格删除标题(如标签标题就够了)第三,删除分页功能(一个人不能有一百万部电影对吧?)。

在Serenity 1.6.5介绍了SetEquality方法

在PersonDialog给PersonMovieGrid设置PersonID

如果没有设置网格PersonID属性,它永远是零,没有记录会被加载。我们应该在对话框设置它:

namespace MovieTutorial.MovieDB
{
    using jQueryApi;
    using Serenity;
    using System.Collections.Generic;

    [IdProperty(PersonRow.IdProperty), NameProperty(PersonRow.Fields.Fullname)]
    [FormKey(PersonForm.FormKey), LocalTextPrefix(PersonRow.LocalTextPrefix), 
     Service(PersonService.BaseUrl)]
    public class PersonDialog : EntityDialog<PersonRow>
    {
        private PersonMovieGrid moviesGrid;

        public PersonDialog()
        {
            moviesGrid = new PersonMovieGrid(this.ById("MoviesGrid"));

            tabs.OnActivate += (e, i) => this.Arrange();
        }

        protected override void AfterLoadEntity()
        {
            base.AfterLoadEntity();

            moviesGrid.PersonID = (int?)this.EntityId;
        }
    }
}

一个实体或一个新的实体后加载到对话框的时候AfterLoadEntity会被调用, this.EntityId引用当前加载实体得identity值,在新记录模式下,它是空的。

AfterLoadEntity和 LoadEntity可能在对话框得生命周期中被调用几次,所以避免在这些事件里面创建一些子对象,否则你将会创建对象的多个实例。这就是为什么我们在构造函数对话框创建了网格。

fcad5b11-c802-40cd-b660-2cee6740a5b3

修复电影标签尺寸

您可能已经注意到,当你切换到电影选项卡中,对话框会少一点的高度。这是因为对话框设置为默认自动高度和200 px表格。当你切换到电影选项卡,表单被隐藏,所以电影对话框适应网格高度。

编辑 s-PersonDialog css in site.less:

.s-PersonDialog {
    > .size { .widthAndMin(650px); .heightAndMin(400px); }
    .dialog-styles(@h: auto, @l: 150px, @e: 400px);
    .s-PropertyGrid .categories { height: 260px; }
    .ui-dialog-content { overflow: hidden; }
    .tab-pane.s-TabMovies { padding: 5px; }
    .s-PersonMovieGrid > .grid-container { height: 315px; }
}

3.1.11添加主键和画廊图片

添加一个主图像和多个画廊图片电影和人记录,首先需要开启迁移:

using FluentMigrator;
using System;

namespace MovieTutorial.Migrations.DefaultDB
{
    [Migration(20151115202100)]
    public class DefaultDB_20151115_202100_PrimaryGalleryImages : Migration
    {
        public override void Up()
        {
            Alter.Table("Person").InSchema("mov")
                .AddColumn("PrimaryImage").AsString(100).Nullable()
                .AddColumn("GalleryImages").AsString(Int32.MaxValue).Nullable();

            Alter.Table("Movie").InSchema("mov")
                .AddColumn("PrimaryImage").AsString(100).Nullable()
                .AddColumn("GalleryImages").AsString(Int32.MaxValue).Nullable();
        }

        public override void Down()
        {
        }
    }
}

T然后修改 MovieRow.cs 和 PersonRow.cs:

namespace MovieTutorial.MovieDB.Entities
{
    // ...
    public sealed class PersonRow : Row, IIdRow, INameRow
    {

        [DisplayName("Primary Image"), Size(100), 
         ImageUploadEditor(FilenameFormat = "Person/PrimaryImage/~")]
        public string PrimaryImage
        {
            get { return Fields.PrimaryImage[this]; }
            set { Fields.PrimaryImage[this] = value; }
        }

        [DisplayName("Gallery Images"), 
         MultipleImageUploadEditor(FilenameFormat = "Person/GalleryImages/~")]
        public string GalleryImages
        {
            get { return Fields.GalleryImages[this]; }
            set { Fields.GalleryImages[this] = value; }
        }

        // ...

        public class RowFields : RowFieldsBase
        {
            // ...
            public readonly StringField PrimaryImage;
            public readonly StringField GalleryImages;
            // ...
        }
    }
}
namespace MovieTutorial.MovieDB.Entities
{
    // ...
    public sealed class MovieRow : Row, IIdRow, INameRow
    {
        [DisplayName("Primary Image"), Size(100), 
         ImageUploadEditor(FilenameFormat = "Movie/PrimaryImage/~")]
        public string PrimaryImage
        {
            get { return Fields.PrimaryImage[this]; }
            set { Fields.PrimaryImage[this] = value; }
        }

        [DisplayName("Gallery Images"), 
         MultipleImageUploadEditor(FilenameFormat = "Movie/GalleryImages/~")]
        public string GalleryImages
        {
            get { return Fields.GalleryImages[this]; }
            set { Fields.GalleryImages[this] = value; }
        }

        // ...
        public class RowFields : RowFieldsBase
        {
            // ...
            public readonly StringField PrimaryImage;
            public readonly StringField GalleryImages;
            // ...
        }
    }
}

这里我们指定这些字段将由ImageUploadEditor和MultipleImageUploadEditor类型处理。

FilenameFormat指定上传文件的命名。例如,人的主要形象将上传在App_Data/upload/Person/PrimaryImage/ 下。

~ 在FilenameFormat末尾是一个自动命名方案{1:00000}/{0:00000000}_{2}快捷方式。

这里,参数{ 0 }替换的身份记录,例如PersonID。

参数{ 1 }是identity/ 1000。这是限制数量的文件存储在一个目录中是有用的。

参数{2} 是一个 unique 字符串像 6l55nk6v2tiyi,在区分每次上传文件时是有用的。这有助于避免在客户端缓存造成的问题。

因此,人主要的文件上传图片将位于一个路径是这样的:

> App_Data\upload\Person\PrimaryImage\00000\00000001_6l55nk6v2tiyi.jpg

你不需要遵循这一命名方案。你可以指定自己的格式(如PersonPrimaryImage_ { 0 } _ { 2 }。

下一步是将这些字段添加到表单(MovieForm.cs和PersonForm.cs):

namespace MovieTutorial.MovieDB.Forms
{
    //...public class PersonForm
    {
        public String Firstname { get; set; }
        public String Lastname { get; set; }
        public String PrimaryImage { get; set; }
        public String GalleryImages { get; set; }
        public DateTime BirthDate { get; set; }
        public String BirthPlace { get; set; }
        public Gender Gender { get; set; }
        public Int32 Height { get; set; }
    }
}
namespace MovieTutorial.MovieDB.Forms
{
    //...public class MovieForm
    {
        public String Title { get; set; }
        [TextAreaEditor(Rows = 3)]
        public String Description { get; set; }
        [MovieCastEditor]
        public List<Entities.MovieCastRow> CastList { get; set; }
        public String PrimaryImage { get; set; }
        public String GalleryImages { get; set; }
        [TextAreaEditor(Rows = 8)]
        public String Storyline { get; set; }
        public Int32 Year { get; set; }
        public DateTime ReleaseDate { get; set; }
        public Int32 Runtime { get; set; }
        public Int32 GenreId { get; set; }
        public MovieKind Kind { get; set; }
    }
}

我也修改Person 对话框css来加一点大小:

.s-PersonDialog {
    > .size { .widthAndMin(700px); .heightAndMin(600px); }
    .dialog-styles(@h: auto, @l: 150px, @e: 450px);
    .s-PropertyGrid .categories { height: 460px; }
    .ui-dialog-content { overflow: hidden; }
    .tab-pane.s-TabMovies { padding: 5px; }
    .s-PersonMovieGrid > .grid-container { height: 515px; }
}

这是我们现在看到的:

6d326c17-71af-4b5f-b575-84a94d2d29f9

ImageUploadEditor文件名直接存储为字符串字段,而MultipleImageUpload编辑器以JSON数组的格式在一个string字段存储文件名字。

posted @ 2016-04-02 12:04  zfanlin  阅读(5296)  评论(1编辑  收藏  举报