咱們來用Serenity建立一個和IMDB類似的編輯界面的站點。javascript
你能在下面的站點找到教程的源代碼:css
https://github.com/volkanceylan/Serenity-Tutorials/tree/master/MovieTutorialhtml
在Visual Studio 點擊 File -> New Project. 確保你選擇了 Serene template. 輸入 MovieTutorial 做爲名稱點擊 OK.java
在解決方案資源管理器中, 你應該看到兩個名稱爲MovieTutorial.Web 和MovieTutorial.Script工程文件。git
確保 MovieTutorial.Web 是啓動項目 (會被加粗), 假如沒有,點擊 設爲啓動項目github
Serenity 應用程序一般有至少兩個項目。一個用於服務器端代碼+ css等靜態資源文件,圖片等。(MovieTutorial.Web),一個用於客戶端代碼(MovieTutorial.Script)。web
MovieTutorial。腳本看起來像一個普通的c#類庫,可是它所包含的代碼實際上都使用Saltarelle編譯爲Javascript。sql
它的輸出(MovieTutorial.Script.js)將複製到文件夾/網站/腳本MovieTutorial.Web之下。因此在運行時,只有MovieTutorial。使用Web項目。數據庫
默認狀況下,當你按F5運行Web項目,Visual Studio只會構建MovieTutorial 。express
這也能夠經過設置改變在 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。此參數指定與該版本號 (遷移密鑰) 遷移已經在此數據庫上執行。
一般狀況下,你不須要每一個遷移後作這個檢查。在這裏咱們展現這些解釋去哪裏找,以防萬一你在未來會有任何麻煩。
在你肯定數據庫中已經存在表後,咱們將會用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,例如 MyMovies,用它替換 MovieTutorial。
一旦你色字了這個值而且在第一頁生成了,你不必再次設置。 這個選項被儲存在Serenity.CodeGenerator.config在你的解決方案文件夾。
這個值時必填的由於 Sergen 將會在你的項目中包含生成的文件
將根命名空間選項設置爲使用解決方案名稱,例如 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是一個在 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.
在 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,咱們須要修改生成的代碼。咱們將看到如何作。
在咱們的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 字段能夠相比標題字段長一點,因此,讓咱們將他們編輯器類型更改成文本區域。
在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),因此咱們有一個垂直滾動條。讓咱們將其移除。
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
當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 不讓你修改代碼。當你中止調試的時候您的網站也會中止,因此您不能瀏覽器窗口保持打開狀態並重建後刷新。
要解決這個問題,咱們須要禁用編輯,繼續 (不知道爲何)。
右鍵點擊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 和往後會默認設置,因此你不可能會遇到此問題
如下各節中,咱們須要一些示例數據。咱們能夠從 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 代碼進行修改。
在前面的例子中,咱們硬編碼了像 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" } }; } } }
假如咱們想要在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() { } } }
如今咱們給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 } }
由於咱們再也不用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 表單沒有變化。即便咱們添加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 分類。
由於 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將會做爲電影類型的預加載。
爲了放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 表中保存他。可是如今咱們僅僅把它看做是單一的,咱們將在後面看到怎麼把它改爲多個。
再次在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")] //...
如今讓咱們添加一些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() { } } }
由於咱們以前用 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進來的字段。
讓咱們添加 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 字段改變編輯器類型。
爲了給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")]
這一步是爲了展現如何檢查是否一個查找的客戶端腳本可用。
有兩個地方給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字段。
當前, 電影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 在網格中顯示了
當咱們爲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 模板
假如你在用的是Serene1.6.0以前的版本,你可能抱怨重複的特性,你僅僅須要刪除 GenreGrid.cs裏面的GenreRow分部類。
當我開始寫這個教程的時候,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 ,由於演員/女演員可能變爲導演、編劇和其餘的。
是時候建立這兩個表的遷移了:
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 表中應該是一個枚舉,在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模板。
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聲明。
在咱們啓動項目以後,咱們能進入角色。
在編輯對話框的標題,顯示人的名字(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 //...
之後咱們將用它來編輯電影演員。
用sergen給MovieCast表生成代碼:
到目前爲止,咱們爲每一個表建立了一個頁面,這個頁面和編輯的記錄列表。這一次咱們要使用不一樣的策略。
咱們會爲電影演員列表在電影編輯對話框,容許他們的電影。同時,演員與電影實體在一個事務中一塊兒將被保存。
所以,編輯會在內存中,當用戶按下保存按鈕在電影的對話框中,電影和演員一箭將被保存到數據庫(一個事務)。
有可能獨立編輯演員,咱們只是想代表這是能夠作到的。
對於某些類型的主/詳細記錄訂單/細節,細節不該該容許編輯獨立緣由一致性。Serene 已經有一個樣本對這種Northwind/Order編輯對話框。
你可能不須要這一步,可是當我開始本教程在平靜的訂單/細節編輯示例以前,我必須從最近的三個類模板。
這只是一個從最近的一個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,並再次構建(我不得不從新運行模板)
打開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按鈕不起做用,須要定義一個對話框,網格列不是我想他們和字段和按鈕標題並非很是用戶友好……
也由於這不是一個綜合功能(尚未),我必須處理更多的管道如加載和保存在服務器端。
獲得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字段)。
咱們有 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包來保持教程是最新的。
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; } }
對話框還有標題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目前使用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。
當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屬性。
打開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)。
咱們尚未完成。當在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.如今構建解決方案,最後咱們列出/編輯演員了。
當你試圖刪除電影實體,你會獲得外鍵錯誤。在建立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 +集成的功能(至少在服務器端),而不是手動覆蓋保存/檢索和刪除處理程序,我將使用一個新的特性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
如下章節咱們將看到如何編寫本身的請求處理程序的行爲。
要顯示一我的再電影總扮演的角色,咱們將添加一個選項卡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添加一個選項卡。
用下面的內容建立一個新文件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連接。我如今修復…
但目前電影選項卡是空的。咱們須要定義一個網格與合適的列,並將其放在該標籤頁。
首先,在文件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來解決這個問題。
好了,如今咱們能夠看到電影中的電影列表選項卡,可是很奇怪:
不,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方法
若是沒有設置網格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; } }
添加一個主圖像和多個畫廊圖片電影和人記錄,首先須要開啓遷移:
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字段存儲文件名字。