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.Web 和MovieTutorial.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.
這里已經有三個遷移了。遷移就像一個操縱數據庫結構的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 目錄下。
設置項目位置
當你首次運行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 文件中的連接字符串連接到下拉列表。可能有 Default 和Northwind在里面, 選擇 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來打開我們第一個生成的頁面。
現在試着添加一個新的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; }
}
//...
}
}
現在生成解決方案並運行應用程序。你會看到字段標題在網格和對話框中改變了。
列標題中有"...",當列不夠寬,雖然其提示顯示完整標題。我們將看到如何處理這很快。
重寫列標題和寬
迄今為止一切都很好,如果我們想要顯示網格 (列) 或對話框 (窗體) 中的另一個標題。我們可以重寫它相應的定義文件。
讓我們先在列上面做。在 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 也是可用的)。
重新生成和運行:
表單字段相同,但是列標題已經改變了。
如果我們想要重寫窗體字段標題,我們在 MovieForm.cs做類似的步驟。
為Description 和Storyline 改變編輯器類型說明
Description 和Storyline 字段可以相比標題字段長一點,所以,讓我們將他們編輯器類型更改為文本區域。
在MovieColumns.cs 和MovieRow.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)有更長的字段。
在重新創建和運行之后我們得到了這個:
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
{
//..
我們也改的導航項目圖標為 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 數據庫菜單的只有一個導航項目。
第一級別的鏈接和導航菜單先按照它們的顯示順序排序,然后第二個層級按照他們的兄弟姐妹之間的聯系排序。
Visual studio 的一些問題故障排除
萬一你沒有注意到已經,當運行您的網站時Visual Studio 不讓你修改代碼。當你停止調試的時候您的網站也會停止,所以您不能瀏覽器窗口保持打開狀態並重建后刷新。
要解決這個問題,我們需要禁用編輯,繼續 (不知道為什么)。
右鍵點擊MovieTutorial.Web project,點擊屬性 Properties,在 Web 標簽, 取消勾選Enable Edit And Continue 在Debuggers下面。
此外,在您的網站,頂部的藍色進度欄 (即 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復制和粘貼一些。
如果你不想浪費您的時間進入該示例數據,下面的鏈接可以作為一個遷移使用 ︰
假如我們在搜索框中輸入我們能看到兩個電影被過濾了: The Good, the Bad and the Ugly 和 The 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 ,也搜索Description 和Storyline 。要做到這一點,您需要將快速搜索特性也添加到這些字段,如下所示 ︰
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 條目:
快速搜索特性默認情況下是用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" }
};
}
}
///...
}
現在,我們得到了一個快速搜索輸入下拉框。
事先與示例不同,我們修改了服務器端代碼,這一次我們做腳本方面的修改,實際上是對 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 分類。
給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 並且依照下面的參數生成代碼:
重新生成解決方案並且跑起來,我們將會得到一個像這樣的新頁面。
就像你在截圖里面看到的那樣,它在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')
你會得到像下面這樣的:
這里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字段。
在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 在網格中顯示了
標記它可以用來定義一個新的 Genre Inplace
當我們為movies示例設置 genre , 我們注意到 The Good, the Bad and the Ugly 是 Western但是在 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 :
這里我們也看到我們可以從另一個頁面使用一個對話框(GenreDialog)電影頁面。在Serenity的應用程序中,所有客戶端對象(對話框、網格、編輯、格式器等)是獨立的可重用的組件(部件)不綁定到任何頁面。
也可以開始鍵入類型編輯器,它將為您提供一個選項來添加一個新類型。
重新運行 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.Core 和Serenity.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 表生成代碼:
改變性別為枚舉
性別列在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聲明。
在我們啟動項目之后,我們能進入角色。
聲明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表生成代碼:
主/詳細編輯邏輯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,Description 和Storyline 之間的字段,添加一個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")] 但是誰喜歡硬編碼字符串呢?反正不是我...
現在構建和啟動應用程序。電影打開一個對話框,你就會得到我們的新編輯器:
好吧,看起來容易,但是老實說,我們甚至沒有一半的方法。
新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按鈕。
現在,什么都不做,而是添加按鈕顯示一個對話框。
這個對話框需要一些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看起來好一點:
修復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; }
}
}
重建項目,演員網格具有更好的列:
現在嘗試添加一個演員,例如,基努·里維斯/ Neo:
為什么演員列是空的? ?
解決空演員列問題
記住,我們正在編輯內存。這里不涉及服務調用。因此,網格顯示任何對話框發送回它的實體。
當您單擊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的選項卡:
同時,我注意到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來解決這個問題。
好了,現在我們可以看到電影中的電影列表選項卡,但是很奇怪:
為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可能在對話框得生命周期中被調用幾次,所以避免在這些事件里面創建一些子對象,否則你將會創建對象的多個實例。這就是為什么我們在構造函數對話框創建了網格。
修復電影標簽尺寸
您可能已經注意到,當你切換到電影選項卡中,對話框會少一點的高度。這是因為對話框設置為默認自動高度和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; }
}
這是我們現在看到的:
ImageUploadEditor文件名直接存儲為字符串字段,而MultipleImageUpload編輯器以JSON數組的格式在一個string字段存儲文件名字。