ptg 1004 CHAPTER 21 Data Access with WCF Data Services .AddQueryOption(“playerId”, 1) .Expand(“Weapon”) .Expand(“Player”) .Expand(“Creature”) orderby kill.KillTime descending select kill; killsRepeater.DataSource = q; killsRepeater.DataBind(); } The first thing we do in this code is create an instance of the service proxy, which is a generated class that inherits from the System.Data.Services.Client.DataServiceContext class. This class exposes properties for each of the entity sets exposed by the service. In our case, we have players, weapons, and creatures and foreign key relationships that enable them all to link to the kill log. (A Kill is another entity exposed by the service.) The custom service context client proxy takes as a constructor parameter a Uri that contains the address of the service. In a production scenario, you might use service discovery, a service registry, or Web.config files to store the URLs of the services. The next thing we do is call CreateQuery<Kill>(“GetMyKills”). There’s quite a bit of func- tionality packed into this one method call. CreateQuery() doesn’t actually talk to the service; it is merely asking us for the shape of the rows that will be returned from the query (the Kill class) and for either the name of a service method to invoke or the name of an entity set to query. If we had simply supplied the string ”Kills” to the method, we would be querying the Kills entity set directly and not invoking a custom method. Next you can see that the output of the CreateQuery() method is something queryable; so we can chain method calls onto this method in a “fluent” style. We use the Expand() method to tell the WCF Data Service that for every Kill we return, also expand the speci- fied relationship properties. This enables us to bring over all the data related to an entity in a single method call rather than making a bunch of chatty calls with high overhead and latency. In our case, we want the names of the weapons and creatures that were killed. If we wanted, we could supply virtually any additional LINQ queries to the end of our statement, but in this case we were happy with sorting the kills in descending order by time so that the players see their most recent kill first. At this point, the service has still not been queried. All we’re doing up to this point is build- ing an in-memory expression tree. The service will not actually be called until we, directly or indirectly, invoke GetEnumerator() on the query object. For the code in Listing 21.1, we won’t make the actual network call until somewhere inside the DataBind() method of the repeater control. Figure 21.2 shows what the MyKills.aspx page looks like when fed data from the WCF Data Service. From the Library of Wow! eBook ptg 1005 Using Data Services with a Service Reference 21 The great part about using WCF Data Services is that, with a few minor exceptions, the classes coming back from the service containing results are just specialized C# classes. This means if I want to know the name of the weapon used in a particular kill (and I’ve pre- expanded the property chain), I can refer to kill.Weapon.Name. Likewise, to find the name of the creature killed, I can refer to kill.Creature.Name. This property chaining syntax still works with ASP.NET’s binding syntax and templating system. The code in Listing 21.2 shows MyKills.aspx; you can see how the DataBinder.Eval() calls work perfectly well with nested properties returned on the entity objects from the WCF Data Service. LISTING 21.2 MyKills.aspx <form id=”form1” runat=”server”> <div> <asp:Repeater ID=”killsRepeater” runat=”server”> <ItemTemplate> [<%# DataBinder.Eval(Container.DataItem, “KillTime”) %>] <a href=’Player.aspx?playerId= <%# DataBinder.Eval(Container.DataItem, “PlayerID”) %>’> <%# DataBinder.Eval(Container.DataItem, “Player.Name”) %></a> fragged <a href=’Create.aspx?creatureId= <%# DataBinder.Eval(Container.DataItem, “CreatureID”) %>’> <%# DataBinder.Eval(Container.DataItem, “Creature.Name”) %></a> with a <a href=’Weapon.aspx?weaponId= <%# DataBinder.Eval(Container.DataItem, “WeaponID”) %>’> <%# DataBinder.Eval(Container.DataItem, “Weapon.Name”) %></a> (<%# DataBinder.Eval(Container.DataItem, “Notes”) %>) <br /> </ItemTemplate> </asp:Repeater> </div> </form> FIGURE 21.2 Displaying “My Kills” with data from WCF Data Service. From the Library of Wow! eBook ptg 1006 The main thing to take away from this code listing is that we can use the dot notation in the data binder’s Eval() method to access and render nested properties on the bound item. The moral of this story is that whether you access a WCF Data Service because your infra- structure prevents direct SQL access or because another team has decided to expose its data that way, it’s incredibly simple to create a service reference and start working with the data on that service. Also keep in mind that WCF Data Services aren’t just simple query services; you can insert, delete, and update as well, and data services can even be configured to support optimistic concurrency checking, which comes in handy for high- volume intranet applications. To add a new creature to the system and update a kill using the service reference, we can just use the code shown in Listing 21.3. When querying data, the web service is not contacted until code invokes GetEnumerator() on the query. When modifying data, the change requests are not sent to the service until you invoke the SaveChanges() method on the service context. LISTING 21.3 Adding and Updating Entities with a WCF Data Service Proxy ZombieKillaService.ZombieKillaContainer svc = new ZombieKillaService.ZombieKillaContainer( new Uri(“http://localhost:5000/ZombieKilla.svc”)); svc.MergeOption = System.Data.Services.Client.MergeOption.PreserveChanges; ZombieKillaService.Creature creature = new ZombieKillaService.Creature() { Armor = 215, Hitpoints = 512, Name = “Fantastically Big Zombie” }; svc.AddToCreatures(creature); svc.SaveChanges(); Response.Write(“Created a new creature with ID of “ + creature.ID.ToString()); ZombieKillaService.Player player = svc.Players.FirstOrDefault(); ZombieKillaService.Weapon weapon = svc.Weapons.FirstOrDefault(); ZombieKillaService.Kill kill = new ZombieKillaService.Kill() { KillTime = DateTime.Now, PlayerID = player.ID, CreatureID = creature.ID, WeaponID = weapon.ID, Notes = “Awesome shot!” }; svc.AddToKills(kill); svc.SaveChanges(); CHAPTER 21 Data Access with WCF Data Services From the Library of Wow! eBook ptg 1007 Using Data Services with a Data Context 21 From this listing, you can see that the service reference proxy gives us classes that we can instantiate to represent any of the entities hosted by the data service. In addition, we also get methods such as AddToCreatures() that queue up all the work necessary to add an entity to an entity set on the server. We can also query for a specific item, make changes to it (or items related to it), and save those changes as well. You can do a few hundred other things with the data service proxy, but this is an ASP.NET book so we’re going to stick to the basics for this chapter. Using Data Services with a Data Context In the previous section, we built a sample application that displays the kill log from a fictional video game called Zombie Killa. This kill log is exposed as a WCF Data Service, and we communicated with that service through an automatically generated proxy class. In this section, we take a look at how we can talk to the WCF Data Service without using any generated code or classes by using the DataServiceContext class directly. As with the generated proxy, we create an instance of the proxy by providing it with the URL of the WCF Data Service. After that, we can create our own queries (and make changes, inserts, and deletes) by invoking various methods on the context. In the code in Listing 21.4, we call CreateQuery() and pass it the name of the entity set we want to query: Players. LISTING 21.4 Querying a Data Service Manually with a DataServiceContext // NOTE: Not using auto-generated client proxy, using generic client with POCO View Models! DataServiceContext ctx = new DataServiceContext( new Uri(“http://localhost:5000/ZombieKilla.svc”)); ctx.IgnoreMissingProperties = true; var q = from ViewModels.Player player in ctx.CreateQuery<ViewModels.Player>(“Players”) .Expand(“Kills”).Expand(“Kills/Weapon”).Expand(“Kills/Creature”) where player.ID == Int32.Parse(Request[“playerId”]) select player; ViewModels.Player p = q.FirstOrDefault(); StringBuilder sb = new StringBuilder(); sb.Append(p.Name + “<br/>”); foreach (ViewModels.Kill kill in p.Kills) { sb.AppendFormat(“ Killed {1} with a {2} ({3})<br/>”, kill.KillTime, kill.Creature.Name, kill.Weapon.Name, kill.Notes); } placeHolder.Controls.Add(new LiteralControl(sb.ToString())); From the Library of Wow! eBook ptg 1008 The calls to Expand() differ slightly from the previous samples. The beauty of WCF Data Service relationship expansion is that we can expand multiple levels. So, not only can I ask for a player, but I can also ask for that player’s kills, and, for each of those kills I can obtain the weapon and creature used for that kill. All this comes over the wire as a single response; I don’t need to make multiple calls to get all the referential and lookup data. Finally, the code calls FirstOrDefault() to actually send the query over the wire. The DataServiceContext class is so powerful that it knows how to convert the information in the payload from the service into the data type required by the client. When we create the query, we pass it the type ViewModels.Player. This is a Plain Old CLR Object (POCO) class that we created that is little more than a code-based schema that tells the service how to shape the response. Listing 21.5 shows the code for the ViewModels.Player class and the ViewModels.Kill class. LISTING 21.5 The ViewModels.Player Class and ViewModels.Kill Class public class Player { public int ID { get; set; } public string Name { get; set; } public ICollection<Kill> Kills { get; set; } } public class Kill { public int ID { get; set; } public int PlayerID { get; set; } public int CreatureID { get; set; } public DateTime KillTime { get; set; } public string Notes { get; set; } public Player Player { get; set; } public Creature Creature { get; set; } public Weapon Weapon { get; set; } } If you find that you create or refresh a client-side proxy every time the entity definitions change on the service, creating your own view models might be a better idea. This gives you the added benefit of placing all these view models in a shared assembly that can be used by multiple web applications or multiple service clients. Another advantage of the view model approach using the raw data service context is that you can decorate these view models with any attributes you want, including those that might be compatible with various client-side validation frameworks or other custom uses for your ASP.NET or ASP.NET MVC applications. With an auto-generated proxy, any changes you make or decorations you add can be wiped out every time you refresh the reference. CHAPTER 21 Data Access with WCF Data Services From the Library of Wow! eBook ptg 1009 Summary 21 The real power here is that you can choose whether you want to access a raw data service context or one created by adding a service reference. Which option you choose and when should be based on how frequently your service entities change and how much extra functionality you want to put into the client-side objects. Summary In this chapter you learned a little bit about WCF Data Services and the OData protocol. There are countless strategies for getting data in and out of your ASP.NET application ranging from traditional raw SQL access to service-based data access such as WCF Data Services. WCF Data Services provide developers with a platform-independent way of querying and manipulating data without needing to know the specifics of the underlying schema or even the underlying data store. Whether you choose to access a WCF Data Service using a raw data context, a generated data context wrapper, or even through a raw HTTP request, these services provide incredi- ble value and can dramatically increase productivity, code reuse, and even scalability of distributed applications. For more information on WCF Data Services, check out MSDN’s page for them at http://msdn.microsoft.com/en-us/data/bb931106.aspx. From the Library of Wow! eBook ptg This page intentionally left blank From the Library of Wow! eBook ptg CHAPTER 22 Using the Navigation Controls IN THIS CHAPTER . Understanding Site Maps . Using the SiteMapPath Control . Using the Menu Control . Using the TreeView Control . Building a SQL Hierarchical Data Source Control . Summary In this chapter, you learn how to use the SiteMapPath, Menu, and TreeView controls. You can use all three of these controls to enable users to navigate your website. Furthermore, you can use the Menu and TreeView controls independently of website navigation. You can bind these two controls to other data sources such as XML documents or database data. This chapter explores different methods of binding the Menu and TreeView controls to different data sources and shows you how to format the rendered output of both of these controls. You also learn how to take advantage of Ajax when working with the TreeView control. In the final section, we build a SqlHierarchicalDataSource control, which enables you to bind controls such as the TreeView and Menu controls to hierarchical database data. Understanding Site Maps Before you learn about the navigation controls, you first need to understand Site Maps. All three navigation controls use Site Maps to retrieve navigation information. A Site Map enables you to represent the navigational relationships between the pages in an application, independent of the actual physical relationship between pages as stored in the file system. Site Maps use the provider model. In the next chapter, you learn how to create custom Site Map providers to store Site Maps in custom data stores such as database tables. The examples in this chapter take advantage of the default XML Site Map provider, which enables you to store a Site Map in an XML file. From the Library of Wow! eBook ptg 1012 CHAPTER 22 Using the Navigation Controls By default, the navigation controls assume the existence of an XML file named Web.sitemap, which is located in the root of your application. For example, Listing 22.1 contains a simple Site Map. LISTING 22.1 Web.sitemap <?xml version=”1.0” encoding=”utf-8” ?> <siteMap xmlns=”http://schemas.microsoft.com/AspNet/SiteMap-File-1.0” > <siteMapNode url=”~/Default.aspx” title=”Home” description=”The home page of the Website”> <! Product Nodes > <siteMapNode title=”Products” description=”Website products”> <siteMapNode url=”~/Products/FirstProduct.aspx” title=”First Product” description=”The first product” /> <siteMapNode url=”~/Products/SecondProduct.aspx” title=”Second Product” description=”The second product” /> </siteMapNode> <! Services Nodes > <siteMapNode title=”Services” description=”Website services”> <siteMapNode url=”~/Service/FirstService.aspx” title=”First Service” description=”The first service” /> <siteMapNode url=”~/Products/SecondService.aspx” title=”Second Service” description=”The second service” /> </siteMapNode> </siteMapNode> </siteMap> From the Library of Wow! eBook ptg 1013 Using the SiteMapPath Control A Site Map file contains <siteMapNode> elements. There can be only one top-level node. In the case of Listing 22.1, the top-level node represents the website’s home page. A <siteMapNode> supports three main attributes: . title—A brief title that you want to associate with a node. . description—A longer description that you want to associate with a node. . url—A URL that points to a page or other resource. The url attribute is not required. Both the Products and Services nodes do not include a url attribute because these nodes do not represent pages to which you can navigate. Each <siteMapNode> can contain any number of child nodes. In Listing 22.1, both the Products and Services nodes include two child nodes. The Site Map in Listing 22.1 represents a website that has the following folder and page structure: Default.aspx Products FirstProduct.aspx SecondProduct.aspx Services FirstService.aspx SecondService.aspx The navigational structure of a website as represented by a Site Map is not required to have any relationship to the navigational structure of a website as stored in the file system. You can create any relationship between the nodes in a Site Map that you want. Using the SiteMapPath Control The SiteMapPath control enables you to navigate easily to any parent page of the current page. It displays the standard breadcrumb trail that you see on many popular websites (see Figure 22.1). You can use the SiteMapPath control simply by declaring the control in a page. The control automatically uses the Web.sitemap file located in the root of your application. For example, the page in Listing 22.2 includes the SiteMapPath control (see Figure 22.2). 22 From the Library of Wow! eBook . nodes. The Site Map in Listing 22.1 represents a website that has the following folder and page structure: Default.aspx Products FirstProduct.aspx SecondProduct.aspx Services FirstService.aspx SecondService.aspx The. <siteMapNode title=”Products” description=”Website products”> <siteMapNode url=”~/Products/FirstProduct.aspx” title=”First Product” description=”The first product” /> <siteMapNode. <siteMap xmlns=”http://schemas.microsoft.com/AspNet/SiteMap-File-1.0” > <siteMapNode url=”~/Default.aspx” title=”Home” description=”The home page of the Website”> <! Product Nodes >