在諸多新改進之中,Visual Studio 2010 引入了用戶期待已久的實體框架 4.0 和 WCF 數據服務 4.0(以前稱為 ADO.NET 數據服務),這兩項功能綜合起來,簡化了您建立數據模型、使用數據和生成數據的方式。
實體框架 4.0 (EF 4.0) 致力於啟用和簡化兩種主要方案:以域為中心的應用程序開發和傳統以數據為中心的“基於數據的窗體設計”。它引入了諸如模型優先開發等功能(該功能允許您創建模型並為您生成自定義 T-SQL);對持久化透明的支持;外鍵;延遲加載以及實體的自定義代碼生成。
WCF 數據服務 4.0 致力於對開放數據協議 (odata.org) 及其新功能的更新,其中包括 Windows Presentation Foundation (WPF) 和 Silverlight 的雙向數據綁定、行計數、服務器驅動的分頁、增強的二進制大對象支持以及對投影的支持。
我將使用一個簡單的網絡日志應用程序 (MyBlog) 來探討 EF 和 WCF 數據服務中的新功能,並說明這些技術如何協同工作來簡化建立數據模型和使用數據的方式。此示例應用程序將包含一個提供博客文章只讀視圖的 ASP.NET Web 應用程序,以及一個允許博客所有者編輯文章的 Silverlight 博客管理員客戶端。在應用程序開頭,我將首先使用模型創建一個實體數據模型 (EDM),然後生成數據庫以及用於與該數據庫交互的代碼。此示例還將使用 Silverlight 3 CTP 3 的 ADO.NET 數據服務更新。
EF 4.0 入門
我將先探討 ASP.NET Web 應用程序項目。為了開始使用 EF,我使用“添加新項”向導來添加 ADO.NET EDM,並選擇一個我同樣也稱為 BlogModel 的空模型。通過右鍵單擊空設計器圖面並選擇“屬性”,您可以看到默認實體容器名稱,在本例中為 BlogModelContainer。首先,我將該名稱更改為 BlogContext,然後將創建模型。
MyBlog 需要三個實體,我分別將其命名為 Blog、Post 和 Tag,如圖 1 中所示。為了創建這些實體,我將一個實體從工具箱拖到設計圖面,然後單擊鼠標右鍵並選擇“屬性”以編輯實體屬性。在其中每個實體上,我還將需要一些標量屬性(右鍵單擊實體並選擇“添加”|“標量屬性”)。
圖 1 Blog、Post 和 Tag 實體以及關聯的屬性設置
EF 4.0 中的外鍵支持
接下來,我將添加這些實體之間的關系。右鍵單擊設計圖面,並選擇“添加”|“關聯”,如圖 2 中所示。EF 現在支持外鍵,從而允許在實體上包括外鍵屬性。請注意,添加關系的操作向 Post 實體中添加了 BlogBlogID 屬性(即外鍵)。
圖 2 Blog、Post 和 Tag 實體之間的關聯
通過在實體上包括外鍵屬性,將可簡化許多鍵編碼模式,其中包括數據綁定、動態數據、並發控制和 n 層開發。例如,如果我正在對顯示產品的網格進行數據綁定操作,並且在網格中有 CategoryID(一個外鍵值),但沒有對應的 Category 對象,則 EF 中的外鍵支持意味著我不再需要進行查詢來單獨地拉回 Category 對象。
EF 4.0 的模型優先
既然生成了模型(請參見圖 3),那麼應用程序將需要數據庫。在本例中,MyBlog 是一個新應用程序,尚沒有數據庫。我不想自己創建數據庫;而是希望讓程序為我創建 – 而我能夠做到。利用 EF 4.0 中的模型優先,Visual Studio 現在不僅能為實體生成自定義代碼,而且能基於剛剛創建的模型來生成數據庫。
圖 3 Blog 模型
首先,我需要創建將向其應用所生成的架構的空數據庫。為此,我打開服務器資源管理器,右鍵單擊“數據連接”節點,並選擇“創建新的 SQL Server 數據庫”(請參見圖 4)。創建空數據庫後,我右鍵單擊模型設計圖面,並選擇“從模型中生成數據庫”。完成“生成數據庫”向導的各個步驟後,將會創建 BlogModel.edmx.sql 文件。在此新文件處於打開狀態時,只需右鍵單擊該文件並執行 SQL 腳本即可為我的數據庫創建架構。
圖 4 創建新的空數據庫並從 EDM 中生成數據庫架構
EF 4.0 的自定義代碼生成
此時,可以進行多個後續步驟,其中一個步驟就是使用 T4 模板自定義 EF 所生成的代碼。在 Visual Studio 2008 SP1 中,盡管 EF 提供了一些用於自定義代碼生成的掛鉤,但使用起來卻相對不靈活和困難。EF 4.0 現在利用 T4 模板,從而提供了更簡單、更靈活並且更強大的方式來自定義生成的代碼。
若要將新的 T4 模板添加到項目,請右鍵單擊實體設計器圖面,並選擇“添加代碼生成項”。從此處選擇任何當前已安裝的模板作為起點,或查看聯機庫中可用的模板。為了使用默認 EF 模板作為此項目的起點,我將選擇 ADO.NET EntityObject 生成器模板;默認情況下,該模板名為 Model1.tt。通過采用這種方式添加代碼生成模板,EF 將自動為模型禁用默認代碼生成。生成的代碼被從 BlogModel.Designer.cs 中刪除,現在存在於 Model1.cs 中。此時,可以編輯模板以自定義它將生成的實體,並且每次保存 .tt 文件時,都將重新生成相關代碼。有關編輯和使用 EF 4.0 的 T4 模板的詳細信息,請訪問 ADO.NET 團隊博客,網址為 blogs.msdn.com/adonet。
將 POCO 實體用於 EF 4.0
Visual Studio 2008 SP1 對實體類有很多限制,這至少可以說,會使生成真正沒有持久性問題的類非常困難。EF 4.0 中最熱門的功能之一是:能夠為使用 EF 的實體創建 Plain Old CLR Object (POCO) 類型,並且沒有 Visual Studio 2008 SP1 中的那種限制。
我們再回到 MyBlog 示例。我將為三個實體(Blog、Post 和 Tag)創建 POCO 對象。首先,需要禁用代碼生成,並且我需要刪除在上一節中添加的 .tt 文件。若要查看模型的屬性,請右鍵單擊實體設計器圖面。如圖 5 中所示,有一個名為“Code Generation Strategy”的屬性,該屬性需要設置為“None”以禁用代碼生成。
圖 5“Code Generation Strategy”屬性
請注意,當您將“代碼生成項”(T4 模板)添加到項目時,此屬性將自動設置為“None”。如果項目當前包括 .tt 文件,您將需要在使用 POCO 對象之前將該文件刪除。從此處可以添加 POCO 對象的類(Blog.cs、Post.cs 和 Tag.cs),如圖 6、圖 7 和圖 8 中所示。
圖 6 Blog 實體的 POCO 對象
public class Blog
{
public intBlogID
{
get;
set;
}
public string Name
{
get;
set;
}
public string Owner
{
get;
set;
}
public List<Post> Posts
{
get { return _posts; }
set { _posts = value; }
}
List<Post> _posts = new List<Post>();
}
圖 7 Tag 實體的 POCO 對象
public class Tag
{
public int TagID
{
get;
set;
}
public string Name
{
get;
set;
}
public List<Post> Posts
{
get { return _posts; }
set { _posts = value; }
}
List<Post> _posts = new List<Post>();
}
圖 8 Post 實體的 POCO 對象
public class Post
{
public int PostID
{
get;
set;
}
public DateTime CreatedDate
{
get;
set;
}
public DateTime ModifiedDate
{
get;
set;
}
public string Title
{
get;
set;
}
public string PostContent
{
get;
set;
}
public Blog Blog
{
get;
set;
}
public int BlogBlogID
{
get;
set;
}
public Boolean Public
{
get;
set;
}
public List<Tag> Tags
{
get { return _tags; }
set { _tags = value; }
}
private List<Tag> _tags = new List<Tag>();
}
最後,我需要創建上下文類,該類與通過默認代碼生成而生成的 ObjectContext 實現非常類似,但我將其稱為 BlogContext。該類將從 ObjectContext 類繼承。上下文是持久性調用類。它將允許查詢復合、將實體具體化,並將更改保存回數據庫(請參見圖 9)。
圖 9 BlogContext
public class BlogContext : ObjectContext
{
public BlogContext()
: base("name=BlogContext", "BlogContext")
{
}
public ObjectSet<Blog> Blogs
{
get
{
if (_Blogs == null)
{
_Blogs =
base.CreateObjectSet<Blog>("Blogs");
}
return _Blogs;
}
}
private ObjectSet<Blog> _Blogs;
public ObjectSet<Post> Posts
{
get
{
if (_Posts == null)
{
_Posts =
base.CreateObjectSet<Post>("Posts");
}
return _Posts;
}
}
private ObjectSet<Post> _Posts;
public ObjectSet<Tag> Tags
{
get
{
if (_Tags == null)
{
_Tags = base.CreateObjectSet<Tag>("Tags");
}
return _Tags;
}
}
private ObjectSet<Tag> _Tags;
}
延遲加載
在 Visual Studio 2008 SP1 中,EF 支持兩種基本的相關實體加載方式:使用 Load 方法顯式加載相關實體,或使用 Include 方法在查詢內自願加載相關實體,這兩種方式都能確保只有在明確指示應用程序命中數據庫時,應用程序才會命中數據庫。EF 4.0 中現在另一個最熱門的功能是延遲加載。如果不需要執行顯式加載,您可以使用延遲加載(也稱為延期加載)在首次訪問導航屬性時加載相關實體。在 Visual Studio 2010 中,這一點是通過使導航屬性成為虛擬屬性而實現的。
在 MyBlog 示例中,Blog.cs 和 Tag.cs 中的公共 List<Post> Posts 屬性都將成為公共虛擬 List<Post> Posts,而 Post.cs 中的公共 List<Tag> Tags 屬性將成為公共虛擬 List<Tag> Tags。EF 隨後將在運行時創建一個知道如何執行加載的代理類型,這樣將無需另外更改代碼。但是,由於 MyBlog 示例使用 WCF 數據服務通過開放數據協議 (OData) 服務公開實體,因此應用程序不使用延遲加載。
在 Visual Studio 2010 中創建 WCF 數據服務
MyBlog 利用 WCF 數據服務提供的接近於完整的解決方案通過 EDM 提供 OData 服務,並包括一個使用 OData 服務的 Silverlight 博客管理員客戶端。開放數據協議是一種數據共享標准,針對數據使用者(客戶端)和生成者(服務),該標准打破了信息孤立的情況,並形成了一個強大、可互操作的生態系統,從而使更多應用程序能夠利用更廣泛的數據集。
在設置了 EDM 和數據庫後,向應用程序中添加新的 WCF 數據服務將很簡單;我使用“添加新項”向導添加了一個 WCF 數據服務(我將其稱為 BlogService)。這將生成 BlogService.svc 文件,該文件表示服務的框架,並將通過隨之前創建的上下文一起提供而指向 EDM。由於默認情況下服務處於完全鎖定狀態,因此必須使用 config.SetEntitySetAccessRule 明確允許通過服務訪問實體集。為此,將為提供的每個 EntitySet 設置一條規則,如圖 10 中所示。
圖 10 BlogService.svc
public class BlogService : DataService<BlogContext>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
// TODO: set rules to indicate which entity sets and service
// operations are visible, updatable, etc.
// Examples:
config.SetEntitySetAccessRule("Blogs", EntitySetRights.All);
config.SetEntitySetAccessRule("Posts", EntitySetRights.All);
config.SetEntitySetAccessRule("Tags", EntitySetRights.All);
// config.SetServiceOperationAccessRule("MyServiceOperation",
// ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion =
DataServiceProtocolVersion.V2;
}
}
(注意:如果您下載本文的示例代碼,您將注意到,該代碼使用非常簡單的 Forms 身份驗證方案來保護網站的安全,而其余示例也將使用該方案來基於當前登錄的用戶篩選數據。不過,由於實現 Forms 身份驗證超出了本文范圍,因此不在此處詳細論述。)
在服務已啟動並正在運行的情況下,下一步是基於當前登錄的用戶篩選結果,以便只會返回該用戶擁有的博客。可通過添加查詢偵聽器(如圖 11 中所示)來限制查詢返回的實體,從而實現這一點。
圖 11 查詢偵聽器
// returns only public posts and posts owned by the current user
[QueryInterceptor("Posts")]
public Expression<Func<Post, bool>>OnPostQuery()
{
return p =>p.Public == true ||
p.Blog.Owner.Equals(HttpContext.Current.User.Identity.Name);
}
// returns only the blogs the currently logged in user owns
[QueryInterceptor("Blogs")]
public Expression<Func<Blog, bool>>OnBlogQuery()
{
return b =>
b.Owner.Equals(HttpContext.Current.User.Identity.Name);
}
在 Silverlight 中使用 WCF 數據服務
有關構建 Silverlight UI 的詳細信息超出了本文范圍,因此我將忽略其中部分詳細信息。但在深入探討如何將數據服務與 Silverlight 應用程序掛接之前,我將向項目中添加一個新的 Silverlight 應用程序,其中包含默認 Silverlight 頁面 MainPage.xaml。我將向其中添加基本的 DataGrid、ComboBox、Button 和幾個標簽。在框架 Silverlight 應用程序准備就緒後(請參見圖 12),我們可以掛接數據服務。
圖 12 MyBlog SilverlightAdministrator 應用程序的基本布局
首先,Silverlight 應用程序需要一些對象,這些對象表示數據服務定義的每個實體。為此,可使用 Visual Studio 中的“添加服務引用”向導為數據服務自動生成客戶端類。(請注意,為了使用“添加服務引用”,我需要暫時禁用在服務上實現的授權檢查,以便“添加服務引用”擁有服務的完全訪問權限。我將“添加服務引用”向導指向服務的基本 URI,在 MyBlog 中為 localhost:48009/BlogService.svc)。
WCF 數據服務 4.0 中的數據綁定
WCF 數據服務 4.0 中改進的數據綁定支持向客戶端庫中添加了一種新集合類型 DataServiceCollection,從而擴展了 ObservableCollection。但是,在 Silverlight 3 中,向項目中添加服務引用時,默認情況下數據綁定處於禁用狀態。因此,若要利用 WCF 數據服務中新的數據綁定功能,將需要啟用數據綁定,並需要更新服務引用。從解決方案資源管理器中,單擊“顯示所有文件”按鈕,並展開“服務引用”節點下的 “BlogService”項。雙擊 Reference.datasvc 映射文件,並將 Parameters 元素替換為如下所示的 XML 片段:
<Parameters>
<Parameter Name="UseDataServiceCollection" Value="true" />
<Parameter Name="Version" Value="2.0" />
</Parameters>
如果將 UseDataServiceCollection 參數設置為 true,則會自動生成實現 INotifyPropertyChanged 和 INotifyCollectionChanged 接口的客戶端類型。這意味著,對 DataServiceCollection 的內容或集合中的實體所做的任何更改都會反映在客戶端上下文上。它還意味著,如果重新查詢集合中的某個實體,則對該實體所做的任何更改都會反映在 DataServiceCollection 內的實體中。並且它意味著,由於 DataServiceCollection 實現標准綁定接口,因此它可作為 DataSource 綁定到大多數 WPF 和 Silverlight 控件。
返回到 MyBlog 示例,下一步是通過創建新的 DataServiceContext 來創建與服務的連接,並使用該連接來查詢服務。圖 13 包括 MainPage.xaml 和 MainPage.xaml.cs 並顯示新的 DataServiceContext 的創建,同時在服務中查詢所有博客(在本例中,服務將返回登錄用戶擁有的所有博客),並將這些博客綁定到 Silverlight 應用程序上的 ComboBox。
圖 13 MainPage.xaml 和 MainPage.xaml.cs
MainPage.xaml
<Grid x:Name="LayoutRoot" Background="White" Width="618">
<data:DataGrid Name="grdPosts" AutoGenerateColumns="False"
Height="206" HorizontalAlignment="Left"Margin="17,48,0,0"
VerticalAlignment="Top" Width="363" ItemsSource="{Binding Posts}">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="Title" Binding="{Binding Title}"/>
<data:DataGridCheckBoxColumn Header="Public"
Binding="{Binding Public}"/>
<data:DataGridTextColumn Header="Text"
Binding="{Binding PostContent}"/>
</data:DataGrid.Columns>
</data:DataGrid>
<Button Content="Save" Height="23" HorizontalAlignment="Left"
Margin="275,263,0,0" Name="btnSave" VerticalAlignment="Top"
Width="75" Click="btnSave_Click_1" />
<ComboBox Height="23" HorizontalAlignment="Left"
Margin="86,11,0,0" Name="cboBlogs" VerticalAlignment="Top"
Width="199" ItemsSource="{Binding}" DisplayMemberPath="Name"
SelectionChanged="cboBlogs_SelectionChanged" />
<dataInput:Label Height="50" HorizontalAlignment="Left"
Margin="36,15,0,0" Name="label1"
VerticalAlignment="Top"Width="100" Content="Blogs:" />
<dataInput:Label Height="17" HorizontalAlignment="Left"
Margin="17,263,0,0" Name="lblCount" VerticalAlignment="Top"
Width="200" Content="Showing 0 of 0 posts"/>
<Button Content="Load More Posts" Height="23" HorizontalAlignment="Left" Margin="165,263,0,0" Name="btnMorePosts"
VerticalAlignment="Top" Width="100" Click="btnMorePosts_Click" />
</Grid>
MainPage.xaml.cs
public MainPage()
{
InitializeComponent();
svc = new BlogContext(new Uri("/BlogService.svc", UriKind.Relative));
blogs = new DataServiceCollection<Blog>(svc);
this.LayoutRoot.DataContext = blogs;
blogs.LoadCompleted +=
new EventHandler<LoadCompletedEventArgs>(blogs_LoadCompleted);
var q = svc.Blogs.Expand("Posts");
blogs.LoadAsync(q);
}
void blogs_LoadCompleted(object sender, LoadCompletedEventArgs e)
{
if (e.Error == null)
{
if (blogs.Count> 0)
{
cboBlogs.SelectedIndex = 0;
}
}
}
為了綁定 DataGrid,添加了一個 cboBlogs_SelectionChanged() 方法:
private void cboBlogs_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.grdPosts.DataContext = ((Blog)cboBlogs.SelectedItem);
}
ComboBox 中的當前選定項每次發生變化時,都將調用此方法。
要掛接到 Silverlight 應用程序上的最後一項是“Save”按鈕,該按鈕是通過添加對 DataServiceContext 調用 SaveChanges 的 btnSave_Click 方法來啟用的,如圖 14 中所示。
圖 14 將更改保存回數據庫
private void btnSave_Click_1(object sender, RoutedEventArgs e)
{
svc.BeginSaveChanges(SaveChangesOptions.Batch, OnChangesSaved, svc);
}
private void OnChangesSaved(IAsyncResult result)
{
var q = result.AsyncState as BlogContext;
try
{
// Complete the save changes operation
q.EndSaveChanges(result);
}
catch (Exception ex)
{
// Display the error from the response.
MessageBox.Show(ex.Message);
}
}
服務器驅動的分頁
通常需要限制服務器針對給定查詢將返回的結果總數,以免應用程序意外地拉回數量過大的數據。利用 WCF 數據服務 4.0 中服務器驅動的分頁,服務作者能夠在 InitializeService 方法中針對每個實體集合設置 SetEntitySetPageSize 屬性,從而按集合限制服務為每個請求返回的實體總數。除了限制為每個請求返回的實體數之外,數據服務還將為客戶端提供“下一個鏈接”- 一個指定客戶端要如何檢索集合中的下一組實體的 URI,采用 AtomPub<link rel=”next”> 元素的形式。
返回到 MyBlog 示例,我將在我的服務上為 Posts EntitySet 將 SetEntitySetPageSize 屬性設置為五個結果:
config.SetEntitySetPageSize("Posts", 5);
在服務中查詢 Posts 時,這會限制返回的實體數。我在此處將 SetEntitySetPageSize 屬性設置為一個小數字來闡釋該功能的工作方式;通常應用程序會設置一個大多數客戶端將不會達到的限制(實際上,客戶端會使用 $top 和 $skip 來隨時控制請求的數據量)。
我還將向應用程序中添加一個新按鈕,以允許用戶從服務中請求 Posts 的下一頁。以下代碼段顯示訪問下一組 Posts 的 btnMorePosts_Click 方法:
private void btnMorePosts_Click(object sender, RoutedEventArgs e)
{
Blog curBlog = cboBlogs.SelectedItem as Blog;
curBlog.Posts.LoadCompleted += new
EventHandler<LoadCompletedEventArgs>(Posts_LoadCompleted);
curBlog.Posts.LoadNextPartialSetAsync();
}
行計數
Visual Studio 2008 SP1 中的 ADO.NET 數據服務發布之後,其中一個最熱門的功能是能夠確定集合中的實體總數,而無需從數據庫中拉回所有這些實體。在 WCF 數據服務 4.0 中,我們增加了“行計數”功能來實現此目的。
在客戶端上創建查詢時,可以調用 IncludeTotalCount 方法以在響應中包括計數標記。隨後可以使用 QueryOperationResponse 對象上的 TotalCount 屬性來訪問值,如圖 15 中所示。
圖 15 使用行計數
private void cboBlogs_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Blog curBlog = this.cboBlogs.SelectedItem as Blog;
this.grdPosts.DataContext = curBlog;
var q = (from p in svc.Posts.IncludeTotalCount()
where p.BlogBlogID == curBlog.ID
select p) as DataServiceQuery<Post>;
curBlog.Posts.LoadCompleted += new
EventHandler<LoadCompletedEventArgs>(Posts_LoadCompleted);
curBlog.Posts.LoadAsync(q);
}
void Posts_LoadCompleted(object sender, LoadCompletedEventArgs e)
{
if (e.Error == null)
{
Blog curBlog = cboBlogs.SelectedItem as Blog;
totalPostCount = e.QueryOperationResponse.TotalCount;
string postsCount = string.Format("Displaying {0} of {1} posts",
curBlog.Posts.Count, totalPostCount);
this.lblCount.Content = postsCount;
curBlog.Posts.LoadCompleted -= Posts_LoadCompleted;
}
}
投影
WCF 數據服務 4.0 中另一項最熱門的功能是“投影”,即能夠指定要從查詢中返回的實體屬性的子集,從而允許應用程序針對帶寬消耗和內存占用進行優化。在 Visual Studio 2010 中,數據服務 URI 格式已擴展為包括 $select 查詢選項,從而使客戶端能夠指定要由查詢返回的屬性的子集。例如,對於 MyBlog,我可以使用以下 URI 查詢所有 Posts 並僅投射 Title 和 PostContent:BlogService.svc/Posts?$select=Title,PostContent。在客戶端上,您現在也可以使用 LINQ 進行包含投影的查詢。
下載代碼示例:http://code.msdn.microsoft.com/mag201004VSData