共用方式為


使用商務規則驗證建置模型

Microsoft 提供

下載 PDF

這是免費的 "NerdDinner" 應用程式教學課程的第 3 個步驟,詳細介紹了如何使用 ASP.NET MVC 1 建置一個小型但完整的 Web 應用程式。

步驟 3 示範如何建立模型,以用於查詢和更新 NerdDinner 應用程式的資料庫。

如果使用 ASP.NET MVC 3,建議遵循 MVC 3 使用者入門MVC Music 市集教學課程。

NerdDinner 步驟 3:建置模型

在模型檢視控制器架構中,「模型」一詞是指代表應用程式資料的物件,以及整合驗證和商務規則的對應網域邏輯。 此模型在許多方面都是以 MVC 為基礎的應用程式的「核心」,正如我們稍後將看到的,它從根本上驅動了它的行為。

ASP.NET MVC 架構支援使用任何資料存取技術,而開發人員可以選擇各種豐富的 .NET 資料選項來實作其模型,包括:LINQ to Entities、LINQ to SQL、NHibernate、LLBLGen Pro、SubSonic、WilsonORM,或只是原始 ADO.NET DataReaders 或 DataSets。

對於 NerdDinner 應用程式,我們將使用 LINQ to SQL 來建立與資料庫設計相當密切的簡單模型,並新增一些自訂驗證邏輯和商務規則。 接著,我們將實作存放庫類別,以協助從應用程式的其餘部分擷取資料持續性實作,並讓我們輕鬆地進行單元測試。

LINQ to SQL

LINQ to SQL 是隨附於 .NET 3.5 的 ORM (物件關聯對應工具)。

LINQ to SQL 可輕鬆地將資料庫資料表對應至我們可編碼的 .NET 類別。 在 NerdDinner 應用程式中,我們將用它來將資料庫中的 Dinners 和回覆資料表對應至 Dinner 和回覆類別。 Dinners 和回覆資料表的資料行,會對應到 Dinner 和回應類別的屬性。 每個 Dinner 和回覆物件都會代表資料庫中 Dinners 或回覆資料表內的個別資料列。

LINQ to SQL 可讓我們不需要手動建構 SQL 陳述式,來使用資料庫資料擷取和更新 Dinner 和回覆物件。 相反地,我們將定義 Dinner 和回覆類別,並描述它們如何對應到資料庫,以及它們之間的關係。 LINQ to SQL 接著會在執行階段負責產生適當的 SQL 執行邏輯,讓我們在與它們互動和使用時能夠順利運作。

我們可以使用 VB 和 C# 內的 LINQ 語言支援,撰寫從資料庫擷取 Dinner 和回覆物件的表達性查詢。 這可將我們需要撰寫的資料程式碼數量降到最低,並允許我們建置真正簡潔的應用程式。

將 LINQ to SQL 類別新增到我們的專案

我們將從以滑鼠右鍵按一下專案內的 [模型] 資料夾開始,然後選取新增 -> 新項目功能表命令:

[模型] 資料夾的螢幕擷取畫面。新項目已醒目提示。模型已醒目提示並選取。

這將開啟 [新增新項目] 對話方塊。 我們將依 [資料] 類別進行篩選,並選取其內的 [LINQ to SQL 類別] 範本:

[新增新項目] 對話方塊的螢幕擷取畫面。資料已醒目提示。L I N Q to S Q L 類別已選取並醒目提示。

我們會將專案命名為 "NerdDinner",然後按一下 [新增] 按鈕。 Visual Studio 會在 \Models 目錄下新增 NerdDinner.dbml 檔案,然後開啟 LINQ to SQL 物件關聯式設計工具:

Visual Studio 中 [Nerd Dinner] 對話方塊的螢幕擷取畫面。已選取 Nerd Dinner dot d b m l 檔案。

使用 LINQ to SQL 建立資料模型類別

LINQ to SQL 可讓我們從現有的資料庫結構描述快速建立資料模型類別。 方式為,在 [伺服器總管] 中開啟 NerdDinner 資料庫,然後選取我們想要建立模型的 [資料表]:

伺服器總管的螢幕擷取畫面。資料表已展開。Dinners 和回覆已醒目提示。

然後,我們可以將資料表拖曳到 LINQ to SQL 設計工具介面上。 當我們執行此操作時,LINQ to SQL 將使用資料表的結構描述 (具有對應到資料庫資料表行的類別屬性) 自動建立 Dinner 和回覆類別:

[Nerd Dinner] 對話方塊的螢幕擷取畫面。顯示 Dinner 和回覆類別。

根據預設,LINQ to SQL 設計工具會在根據資料庫結構描述建立類別時,自動「複數化」資料表和資料行名稱。 例如:上述範例中的 [Dinners] 資料表產生了 [Dinner] 類別。 這個類別命名有助於讓我們的模型與 .NET 命名慣例保持一致,而且我通常會發現讓設計工具修正此問題變得方便 (尤其是在新增許多資料表時)。 不過,只要您不喜歡設計工具產生的類別或屬性名稱,都可以將其覆寫並變更成您想要的任何名稱。 方式為,編輯設計工具內的實體/屬性名稱,或透過屬性方格將其修改。

根據預設,LINQ to SQL 設計工具也會檢查資料表的主索引鍵/外部索引鍵關聯性,並據此自動在其所建立的不同模型類別之間建立預設的「關聯性關聯」。 例如,當我們將 Dinners 和回覆資料表拖曳到 LINQ to SQL 設計工具時,會根據回覆資料表對 Dinners 資料表有外部索引鍵的事實來推斷兩者之間的一對多關聯性 (這由設計工具中的箭號表示):

Dinner 和回覆資料表的螢幕擷取畫面。箭號已醒目提示,並指向 Dinner 屬性樹狀目錄和回覆屬性樹狀目錄。

上述關聯會導致 LINQ to SQL 將強型別的 [Dinner] 屬性新增至回覆類別,開發人員可用來存取與指定回覆相關聯的 Dinner。 它也會導致 Dinner 類別具有 [回覆] 集合屬性,可讓開發人員擷取和更新與特定 Dinner 相關聯的回覆物件。

當我們建立新的回覆物件,並將其新增至 Dinner 的 [回覆] 集合時,您可以在 Visual Studio 中看到 Intellisense 的範例。 請注意 LINQ to SQL 如何在 Dinner 物件上自動新增 [回覆] 集合:

Visual Studio 中 Intellisense 的螢幕擷取畫面。回覆已醒目提示。

藉由將回覆物件新增至 Dinner 的 [回覆] 集合,我們會告知 LINQ to SQL 將 Dinner 與資料庫中回覆資料列之間的外部索引鍵關聯性:

回覆物件的螢幕擷取畫面,以及 Dinner 的 [回覆] 集合。

如果不喜歡設計工具建立模型或命名資料表關聯的方式,可以將其覆寫。 只要按一下設計工具內的關聯箭號,即可透過屬性方格存取其屬性,將其重新命名、刪除或修改。 不過,對於我們的 NerdDinner 應用程式,預設關聯規則適用於我們正在建置的資料模型類別,而且我們只能使用預設行為。

NerdDinnerDataContext 類別

Visual Studio 會自動建立 .NET 類別,代表使用 LINQ to SQL 設計工具所定義的模型和資料庫關聯性。 也會為每個新增至解決方案的 LINQ to SQL 設計工具檔案產生 LINQ to SQL DataContext 類別。 因為我們將 LINQ to SQL 類別項目命名為 "NerdDinner",因此所建立的 DataContext 類別稱為 "NerdDinnerDataContext"。 這個 NerdDinnerDataContext 類別是我們與資料庫互動的主要方式。

我們的 NerdDinnerDataContext 類別會公開兩個屬性:[Dinners] 和 [回覆],代表我們在資料庫內建立模型的兩個資料表。 我們可以使用 C# 來針對這些屬性撰寫 LINQ 查詢,以從資料庫查詢和擷取 Dinner 和回覆物件。

下列程式碼示範如何具現化 NerdDinnerDataContext 物件,並對其執行 LINQ 查詢,以取得未來發生的 Dinners 序列。 Visual Studio 在撰寫 LINQ 查詢時提供完整的 Intellisense,而從中傳回的物件為強型別,也支援 intellisense:

Visual Studio 的螢幕擷取畫面。描述已醒目提示。

除了允許我們查詢 Dinner 和回覆物件之外,NerdDinnerDataContext 也會自動追蹤我們後續對 Dinner 和回覆物件所擷取的任何變更。 我們可以使用這項功能輕鬆地將變更儲存回資料庫,而不需要撰寫任何明確的 SQL 更新程式碼。

例如,下列程式碼示範如何使用 LINQ 查詢從資料庫擷取單一 Dinner 物件、更新兩個 Dinner 屬性,然後將變更儲存回資料庫:

NerdDinnerDataContext db = new NerdDinnerDataContext();

// Retrieve Dinner object that reprents row with DinnerID of 1
Dinner dinner = db.Dinners.Single(d => d.DinnerID == 1);

// Update two properties on Dinner 
dinner.Title = "Changed Title";
dinner.Description = "This dinner will be fun";

// Persist changes to database
db.SubmitChanges();

上述程式碼中的 NerdDinnerDataContext 物件會自動追蹤我們對所擷取之 Dinner 物件所做的屬性變更。 當我們呼叫 "SubmitChanges()" 方法時,它會對資料庫執行適當的 SQL “UPDATE” 陳述式,以保留更新的值。

建立 DinnerRepository 類別

對於小型應用程式,有時可以讓 [控制器] 直接針對 LINQ to SQL DataContext 類別工作,並在 [控制器] 中內嵌 LINQ 查詢。 不過,當應用程式變大時,這種方法變得難以維護和測試。 這也可能導致我們在多個位置複製相同的 LINQ 查詢。

讓應用程式更容易維護和測試的其中一種方法,就是使用 [存放庫] 模式。 存放庫類別有助於封裝資料查詢和持續性邏輯,並從應用程式擷取資料持續性的實作詳細資料。 除了讓應用程式的程式碼更簡潔,使用存放庫模式可以更輕鬆地在未來變更資料儲存體體實作,而且有助於在不需要實際資料庫的情況下協助對應用程式進行單元測試。

針對 NerdDinner 應用程式,我們將使用下列簽章來定義 DinnerRepository 類別:

public class DinnerRepository {

    // Query Methods
    public IQueryable<Dinner> FindAllDinners();
    public IQueryable<Dinner> FindUpcomingDinners();
    public Dinner             GetDinner(int id);

    // Insert/Delete
    public void Add(Dinner dinner);
    public void Delete(Dinner dinner);

    // Persistence
    public void Save();
}

請注意:在本章後面,我們將從此類別中擷取 IDinnerRepository 介面,並在 [控制器] 上啟用相依性插入。 不過,首先,我們將從簡單開始,直接使用 DiningRepository 類別。

若要實作此類別,我們將以滑鼠右鍵按一下 [模型] 資料夾,然後選擇新增 -> 新項目功能表命令。 在 [新增新項目] 對話方塊中,我們將選取 [類別] 範本,並將檔案命名為 “DinnerRepository.cs”:

[模型] 資料夾的螢幕擷取畫面。[新增新項目] 已醒目提示。

然後,我們可以使用下列程式碼來實作 DinnerRepository 類別:

public class DinnerRepository {
 
    private NerdDinnerDataContext db = new NerdDinnerDataContext();

    //
    // Query Methods

    public IQueryable<Dinner> FindAllDinners() {
        return db.Dinners;
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        return from dinner in db.Dinners
               where dinner.EventDate > DateTime.Now
               orderby dinner.EventDate
               select dinner;
    }

    public Dinner GetDinner(int id) {
        return db.Dinners.SingleOrDefault(d => d.DinnerID == id);
    }

    //
    // Insert/Delete Methods

    public void Add(Dinner dinner) {
        db.Dinners.InsertOnSubmit(dinner);
    }

    public void Delete(Dinner dinner) {
        db.RSVPs.DeleteAllOnSubmit(dinner.RSVPs);
        db.Dinners.DeleteOnSubmit(dinner);
    }

    //
    // Persistence 

    public void Save() {
        db.SubmitChanges();
    }
}

使用 DinnerRepository 類別擷取、更新、插入和刪除

既然我們已建立 DinnerRepository 類別,讓我們看看一些程式碼範例,示範我們可以用它來完成的常見工作:

查詢範例

下列程式碼會使用 DinnerID 值擷取單一 Dinner:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

下列程式碼會擷取所有即將推出的 Dinners,並循環處理它們:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve all upcoming Dinners
var upcomingDinners = dinnerRepository.FindUpcomingDinners();

// Loop over each upcoming Dinner and print out its Title
foreach (Dinner dinner in upcomingDinners) {
   Response.Write("Title" + dinner.Title);
}

插入和更新範例

下列程式碼示範如何新增兩個新的 Dinners。 對資料庫存放庫的新增/修改,只有在呼叫其 "Save()" 方法後才會提交到資料庫中。 LINQ to SQL 會自動將所有變更包裹在一個資料庫交易中,因此當存放庫進行保存時,要麼所有變更都成功完成,要麼全部都不會發生。

DinnerRepository dinnerRepository = new DinnerRepository();

// Create First Dinner
Dinner newDinner1 = new Dinner();
newDinner1.Title = "Dinner with Scott";
newDinner1.HostedBy = "ScotGu";
newDinner1.ContactPhone = "425-703-8072";

// Create Second Dinner
Dinner newDinner2 = new Dinner();
newDinner2.Title = "Dinner with Bill";
newDinner2.HostedBy = "BillG";
newDinner2.ContactPhone = "425-555-5151";

// Add Dinners to Repository
dinnerRepository.Add(newDinner1);
dinnerRepository.Add(newDinner2);

// Persist Changes
dinnerRepository.Save();

下列程式碼會擷取現有的 Dinner 物件,並修改其上的兩個屬性。 當呼叫存放庫的 "Save()" 方法時,這些變更將會提交回資料庫。

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Update Dinner properties
dinner.Title = "Update Title";
dinner.HostedBy = "New Owner";

// Persist changes
dinnerRepository.Save();

下列程式碼會擷取 Dinner,然後將回覆新增至其中。 它使用 LINQ to SQL 為我們建立的 Dinner 物件上的 [回覆] 集合來執行此操作 (因為資料庫中兩者之間存在主索引鍵/外部索引鍵關聯性)。 當呼叫存放庫的 "Save()" 方法時,這項變更將作為一個新的回覆資料列保存回資料庫。

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Create a new RSVP object
RSVP myRSVP = new RSVP();
myRSVP.AttendeeName = "ScottGu";

// Add RSVP to Dinner's RSVP Collection
dinner.RSVPs.Add(myRSVP);

// Persist changes
dinnerRepository.Save();

刪除範例

下列程式碼會擷取現有的 Dinner 物件,然後將其標示為要刪除。 當呼叫存放庫的 "Save()" 方法時,刪除操作將會被提交回資料庫。

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Mark dinner to be deleted
dinnerRepository.Delete(dinner);

// Persist changes
dinnerRepository.Save();

整合驗證和商業規則邏輯與模型類別

整合驗證和商業規則邏輯是任何可搭配資料使用之應用程式的重要部分。

結構描述驗證

使用 LINQ to SQL 設計工具定義模型類別時,資料模型類別中的屬性資料類型會對應至資料庫資料表的資料類型。 例如:如果 Dinners 資料表中的 [EventDate] 資料行是 [datetime],LINQ to SQL 所建立的資料模型類別會是 [DateTime] 類型 (這是內建的 .NET 資料類型)。 這意味著,如果您嘗試從程式碼中指派一個整數或布林值給它,將會產生編譯錯誤,並且在執行階段如果嘗試隱式轉換一個無效的字串類型給它,會自動引發錯誤。

LINQ to SQL 也會在使用字串時自動為您處理逸出 SQL 值 ,這可協助您在使用 SQL 插入式攻擊時防範 SQL 插入式攻擊。

驗證和商務規則邏輯

結構描述驗證作為第一個步驟很有用,但還不夠。 大多數實際案例都需要能夠指定更豐富的驗證邏輯,這些邏輯可以跨越多個屬性、執行程式碼,而且通常會知道模型的狀態 (例如:正在建立 /已更新/已刪除,或在網域特定狀態內,例如 [已封存])。 有各種不同的模式和架構可用來定義和套用驗證規則至模型類別,而且有數個 .NET 架構可用來協助進行此操作。 您幾乎可以在 ASP.NET MVC 應用程式中使用它們中的任何一個。

針對 NerdDinner 應用程式,我們將使用一個相對簡單明瞭的模式,在 Dinner 模型物件上公開一個 IsValid 屬性和一個 GetRuleViolations() 方法。 IsValid 屬性會根據驗證和商務規則是否全部有效,傳回 true 或 false。 GetRuleViolations() 方法會傳回任何規則錯誤的清單。

我們將藉由將「部分類別」新增至專案,為 Dinner 模型實作 IsValid 和 GetRuleViolations()。 部分類別可用來將方法/屬性/事件新增至 VS 設計工具維護的類別 (例如 LINQ to SQL 設計工具所產生的 Dinner 類別),並協助避免工具弄亂我們的程式碼。 我們可以使用滑鼠右鍵按一下 \Models 資料夾,然後選取 [新增新項目] 功能表命令,將新的部分類別新增至專案。 然後,我們可以在 [新增新項目] 對話方塊中選擇 [類別] 範本,並將其命名為 Dinner.cs。

[模型] 資料夾的螢幕擷取畫面。已選取 [新增新項目]。Dinner dot c s 會寫入 [新增新項目] 對話方塊中。

按一下 [新增] 按鈕會將 Dinner.cs 檔案新增至專案,並在 IDE 中將其開啟。 然後,我們可以使用下列程式碼來實作基本規則/驗證強制執行架構:

public partial class Dinner {

    public bool IsValid {
        get { return (GetRuleViolations().Count() == 0); }
    }

    public IEnumerable<RuleViolation> GetRuleViolations() {
        yield break;
    }

    partial void OnValidate(ChangeAction action) {
        if (!IsValid)
            throw new ApplicationException("Rule violations prevent saving");
    }
}

public class RuleViolation {

    public string ErrorMessage { get; private set; }
    public string PropertyName { get; private set; }

    public RuleViolation(string errorMessage, string propertyName) {
        ErrorMessage = errorMessage;
        PropertyName = propertyName;
    }
}

上述程式碼的一些注意事項:

  • Dinner 類別前面會加上 "partial" 關鍵詞,這表示內含的程式碼會與 LINQ to SQL 設計工具所產生/維護的類別結合,並編譯成單一類別。
  • RuleViolation 類別是我們將新增至專案的協助程式類別,可讓我們提供有關規則違規的更多詳細資料。
  • Dinner.GetRuleViolations() 方法會導致我們的驗證和商務規則受到評估 (我們將很快實作它們)。 然後,它會傳回 RuleViolation 物件的序列,以提供任何規則錯誤的詳細資料。
  • Dinner.IsValid 屬性提供方便的協助程式屬性,指出 Dinner 物件是否有任何作用中的 RuleViolations。 開發人員可以隨時使用 Dinner 物件主動檢查它 (並且不會引發例外狀況)。
  • Dinner.OnValidate() 部分方法是 LINQ to SQL 提供的一個掛鉤,它允許我們在 Dinner 物件即將保留在資料庫中時隨時收到通知。 上述的 OnValidate() 實作可確保 Dinner 在儲存之前沒有 RuleViolations。 如果它處於無效狀態,則會引發例外狀況,這會導致 LINQ to SQL 中止交易。

此方法提供簡單的架構,讓我們將驗證和商務規則整合到其中。 現在讓我們將下列規則新增至 GetRuleViolations() 方法:

public IEnumerable<RuleViolation> GetRuleViolations() {

    if (String.IsNullOrEmpty(Title))
        yield return new RuleViolation("Title required","Title");

    if (String.IsNullOrEmpty(Description))
        yield return new RuleViolation("Description required","Description");

    if (String.IsNullOrEmpty(HostedBy))
        yield return new RuleViolation("HostedBy required", "HostedBy");

    if (String.IsNullOrEmpty(Address))
        yield return new RuleViolation("Address required", "Address");

    if (String.IsNullOrEmpty(Country))
        yield return new RuleViolation("Country required", "Country");

    if (String.IsNullOrEmpty(ContactPhone))
        yield return new RuleViolation("Phone# required", "ContactPhone");

    if (!PhoneValidator.IsValidNumber(ContactPhone, Country))
        yield return new RuleViolation("Phone# does not match country", "ContactPhone");

    yield break;
}

我們使用 C# 的 "yield return" 功能傳回任何 RuleViolations 序列。 上面的前六個規則檢查,只是強制我們的 Dinner 的字串屬性不能為 null 或空。 最後一條規則稍微有趣一些,它會呼叫 PhoneValidator.IsValidNumber() 協助程式方法,我們可以將其新增到專案中,以驗證 ContactPhone 的號碼格式是否與 Dinner 的國家/地區相符。

我們可以使用 .NET 的規則運算式支援來實作此電話驗證支援。 以下是一個簡單的 PhoneValidator 實作,我們可以將其新增到專案中,以便進行國家/地區特定的 Regex 模式檢查:

public class PhoneValidator {

    static IDictionary<string, Regex> countryRegex = new Dictionary<string, Regex>() {
           { "USA", new Regex("^[2-9]\\d{2}-\\d{3}-\\d{4}$")},
           { "UK", new Regex("(^1300\\d{6}$)|(^1800|1900|1902\\d{6}$)|(^0[2|3|7|8]{1}[0-9]{8}$)|(^13\\d{4}$)|(^04\\d{2,3}\\d{6}$)")},
           { "Netherlands", new Regex("(^\\+[0-9]{2}|^\\+[0-9]{2}\\(0\\)|^\\(\\+[0-9]{2}\\)\\(0\\)|^00[0-9]{2}|^0)([0-9]{9}$|[0-9\\-\\s]{10}$)")},
    };

    public static bool IsValidNumber(string phoneNumber, string country) {

        if (country != null && countryRegex.ContainsKey(country))
            return countryRegex[country].IsMatch(phoneNumber);
        else
            return false;
    }

    public static IEnumerable<string> Countries {
        get {
            return countryRegex.Keys;
        }
    }
}

處理驗證和商業規則違規

既然我們已新增上述驗證和商務規則程式碼,每當我們嘗試建立或更新 Dinner 時,就會評估並強制執行我們的驗證邏輯規則。

開發人員可以撰寫類似下面的程式碼,主動判斷 Dinner 物件是否有效,並擷取其中所有違規的清單而不會引發任何例外狀況:

Dinner dinner = dinnerRepository.GetDinner(5);

dinner.Country = "USA";
dinner.ContactPhone = "425-555-BOGUS";

if (!dinner.IsValid) {

    var errors = dinner.GetRuleViolations();
    
    // do something to fix the errors
}

如果我們嘗試以無效的狀態儲存 Dinner,當我們在 DinnerRepository 上呼叫 Save() 方法時,將會引發例外狀況。 這是因為 LINQ to SQL 會在儲存 Dinner 的變更之前自動呼叫 Dinner.OnValidate() 部分方法,而且我們已將程式碼新增至 Dinner.OnValidate() 以在 Dinner 中發生任何規則違規時引發例外狀況。 我們可以攔截此例外狀況,並被動擷取要修復的違規清單:

Dinner dinner = dinnerRepository.GetDinner(5);

try {

    dinner.Country = "USA";
    dinner.ContactPhone = "425-555-BOGUS";

    dinnerRepository.Save();
}
catch {

    var errors = dinner.GetRuleViolations();

    // do something to fix errors
}

由於我們的驗證和商務規則是在模型層內而不是在 UI 層內實作,因此會套用到應用程式內的所有案例並使用。 我們稍後可以變更或新增商務規則,並讓所有使用 Dinner 物件的程式碼都接受它們。

能夠靈活地在一個地方變更商務規則,而不會讓這些變更波及整個應用程式和 UI 邏輯,這是編寫良好的應用程式的標誌,也是 MVC 框架所鼓勵的好處。

後續步驟

我們現在有一個模型,可用來查詢和更新資料庫。

現在讓我們將一些可用來建置 HTML UI 體驗的控制器和檢視新增至專案。